Repository navigation
Expand file tree
/
Copy pathschema.ts
More file actions
688 lines (628 loc) · 31.5 KB
/
Copy pathschema.ts
File metadata and controls
688 lines (628 loc) · 31.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
/**
* The canonical CLDK analysis schema for TypeScript — the NATIVE model. The stages build this
* shape directly (schema v2, canonical-schema.md): one additive containment tree of nodes
* (`id` / `kind` / `span` / named child maps) that `analysis.json` and the Neo4j projection both
* emit. There is no second model and no emit-time reshape: what the builders construct is what
* the wire carries, minus the INTERNAL fields listed below (stripped at serialization).
*
* Mirrors python's `codeanalyzer/schema/py_schema.py` field-for-field on the invariant spine —
* `symbol_table{module → types{}/functions{}/fields{}}`, `type → callables{}/fields{}`,
* `callable → body{}` — and extends it at the leaves with TypeScript-native node kinds
* (interface / type_alias / enum / namespace) and typed fields (generics, modifiers, ...).
*
* Conventions:
* - A fact is PRESENT or ABSENT; never `null`. Optional fields are omitted, not nulled. The one
* sanctioned null is a `call` body node's `callee` at L1 (refined null→id at L2).
* - `id` fields are stamped per-run by `assignIds` (ids embed `--app-name`; the cached tree must
* stay app-name-free). Builders initialize them to "".
* - INTERNAL fields (never on the wire; serialize.ts strips them by key): `call_sites`,
* `config_accesses`, `abs_path`, `content_hash`, `last_modified`, `file_size`. They exist for
* the call-graph resolver, the dataflow join, and the analysis cache.
*
* All field names are snake_case so `JSON.stringify` emits keys the SDK Pydantic models parse.
*/
import { modulePrefixOf } from "./ids";
// ----------------------------------------------------------------------------------------------
// Span — the one universal attribute. `bytes` are UTF-8 BYTE offsets into the owning module's
// `source` (#179; the keystone's and python's meaning), so
// `Buffer.from(source, "utf8").subarray(bytes[0], bytes[1])` reproduces the node's text —
// `source.slice(...)` does NOT once a multibyte char precedes the node. `start`/`end` are
// [line, column], 1-based.
// ----------------------------------------------------------------------------------------------
export interface TSSpan {
start: [number, number]; // [line, column], 1-based
end: [number, number]; // [line, column], 1-based
bytes: [number, number]; // [startOffset, endOffset], UTF-8 byte offsets into module.source
}
// ----------------------------------------------------------------------------------------------
// Leaf models (wire shapes — flat line/col ints are part of the wire here)
// ----------------------------------------------------------------------------------------------
export interface TSImport {
module: string; // the module specifier, e.g. "./user" or "@nestjs/common"
name: string; // the imported binding (or "" for side-effect imports / "*" for namespace)
alias?: string;
// #182 (python `PyImport.resolved_module` parity): the project-relative file key the specifier
// resolves to under the importer's own tsconfig (`paths`, directory index, `.js` → `.ts`) —
// whether or not that module was emitted this run (`--skip-tests` / `--program` can exclude
// the target; the graph gates on presence, the JSON keeps the key). Re-stamped EVERY run,
// cached modules included (moduleResolution.ts). ABSENT for externals, builtins, and spellings
// that resolve to nothing.
resolved_module?: string;
is_type_only: boolean; // `import type { X } ...`
import_kind: "named" | "default" | "namespace" | "side_effect";
start_line: number;
end_line: number;
start_column: number;
end_column: number;
}
export interface TSExport {
module?: string; // re-export source, e.g. "./user"; absent for `export { x }`
name: string; // exported name ("*" for `export * from`)
alias?: string;
resolved_module?: string; // #182: as on TSImport; only a re-export (`module` set) can carry one
is_type_only: boolean;
export_kind: "named" | "default" | "namespace" | "re_export";
start_line: number;
end_line: number;
start_column: number;
end_column: number;
}
export interface TSComment {
content: string;
is_docstring: boolean; // JSDoc block attached to a declaration
start_line: number;
end_line: number;
start_column: number;
end_column: number;
}
export interface TSDecorator {
name: string; // the decorator as WRITTEN, e.g. "Get" or "http.route" (python parity)
// Import-table resolution of `name` (#151), e.g. "@nestjs/common.Get" — the module specifier kept
// verbatim, aliases mapped back to the exported name. ABSENT when the head is not an imported
// binding: a same-file declaration, a global, or a spelling nothing in the module can name. The
// checker is not consulted; there is no resolved-FQN tier above this.
qualified_name?: string;
positional_arguments: string[]; // raw source fragments
keyword_arguments: Record<string, string>; // object-literal args flattened to key→source
start_line: number;
end_line: number;
start_column: number;
end_column: number;
}
export interface TSTypeParameter {
name: string;
constraint?: string; // the `extends ...` clause text
default?: string; // the `= ...` clause text
}
export interface TSCallableParameter {
name: string;
// `<callable-id>@formal_in:<i>` — the L4 formal_in vertex carrying this parameter (#164; python
// #176 parity). Stamped per-run by stampBodyIds; a forward reference below level 4.
id?: string;
type?: string;
default_value?: string;
is_optional: boolean;
is_rest: boolean;
is_readonly: boolean; // parameter property `constructor(readonly x: T)`
accessibility?: string; // parameter property visibility (NestJS DI / TS shorthand)
decorators: TSDecorator[]; // param decorators (e.g. @Param('id'))
start_line: number;
end_line: number;
start_column: number;
end_column: number;
}
export interface TSOverloadSignature {
parameters: TSCallableParameter[];
return_type?: string;
type_parameters: TSTypeParameter[];
start_line: number;
end_line: number;
}
/**
* INTERNAL — a recorded call site. Never on the wire (the wire's view is the `call` node in the
* owning callable's `body{}`, built per-run by the l1Body pass). Kept on the callable because the
* call-graph resolver joins on it (span-matched to the AST) and the cache round-trips it.
* `callee_signature` is backfilled in place by the tsc resolver.
*/
export interface TSCallsite {
method_name: string;
receiver_expr?: string;
receiver_type?: string;
argument_types: string[];
arguments: string[]; // raw source text per argument — INTERNAL, feeds the config-use key match
type_arguments: string[]; // explicit call type args, foo<T>()
return_type?: string;
callee_signature?: string; // absent when recorded; backfilled by the resolver call graph
is_constructor_call: boolean; // `new X()`
is_optional_chain: boolean; // `a?.b()`
start_line: number;
start_column: number;
end_line: number;
end_column: number;
bytes: [number, number]; // UTF-8 byte offsets [start, end] into module.source (#179)
}
/** INTERNAL — a recognized configuration read (env root access). Never on the wire; the wire's
* view is the `config_access` node in the owning callable's `body{}` (built by the l1Body pass). */
export interface TSConfigAccess {
root: string; // "process.env" | "import.meta.env" | "Bun.env"
key?: string; // present when statically known
start_line: number;
start_column: number;
end_line: number;
end_column: number;
bytes: [number, number];
}
// ----------------------------------------------------------------------------------------------
// Body nodes — a callable's `body{}` map, keyed by local id (`line:col`, or `@tag` synthetic).
// L1: `call` and `config_access` nodes; L3 adds statements + @entry/@exit; L4 adds formal/actual
// param vertices.
// ----------------------------------------------------------------------------------------------
export interface TSBodyNode {
// The GLOBAL ordinal id `<callable-id>@<local>` — the same value :TSBodyNode merges on (#164;
// python #176 parity). Stamped per-run by stampBodyIds after each body emitter writes.
id?: string;
kind: string; // "call" | "config_access" | "statement" | "entry" | "exit" | "formal_in" | "actual_in" | …
span?: TSSpan;
callee?: string | null; // `call` nodes: null at L1, refined to an id at L2 (the one sanctioned null)
of?: string; // synthetic param vertices: the flowed name ("arg0", "$ret", a global path)
parent?: string; // actual_in/actual_out: the anchoring call-site statement's local id
// call-node attributes (copied from the recorded call site by the l1Body pass)
method_name?: string;
receiver_expr?: string;
receiver_type?: string;
argument_types?: string[];
type_arguments?: string[];
return_type?: string;
is_constructor_call?: boolean;
is_optional_chain?: boolean;
// config_access attributes (copied from the recorded access by the l1Body pass)
root?: string;
key?: string;
}
// ----------------------------------------------------------------------------------------------
// Intra-callable edge lists (L3/L4), bare local-id endpoints
// ----------------------------------------------------------------------------------------------
export interface TSCfgEdge {
src: string;
dst: string;
kind: string;
}
export interface TSCdgEdge {
src: string;
dst: string;
}
export interface TSDdgEdge {
src: string;
dst: string;
var?: string;
prov: string[]; // "reaching-defs" (L3 syntactic; sanctioned additive token) / "points-to" (L4)
}
export interface TSSummaryEdge {
src: string;
dst: string;
var?: string;
}
// ----------------------------------------------------------------------------------------------
// Field — module-level binding, class attribute / interface property, or enum member.
// One open-ish shape: each origin sets its own subset (matching what the origin declares).
// ----------------------------------------------------------------------------------------------
export interface TSField {
id: string; // `${parentId}/${name}` — stamped per-run by assignIds
kind: "field";
span?: TSSpan; // absent for constructor parameter properties
name: string;
type?: string;
// module/namespace variable
initializer?: string;
scope?: "module" | "namespace";
declaration_kind?: "const" | "let" | "var" | "using" | "unknown";
is_exported?: boolean;
// class attribute / interface property
comments?: TSComment[];
decorators?: TSDecorator[];
accessibility?: string;
is_static?: boolean;
is_readonly?: boolean;
is_optional?: boolean;
is_abstract?: boolean;
// enum member
value?: string; // initializer text or computed const value
}
// ----------------------------------------------------------------------------------------------
// Callable (function / method / constructor / accessor / arrow / function expression)
// ----------------------------------------------------------------------------------------------
export type TSCallableKind =
| "function"
| "method"
| "constructor"
| "getter"
| "setter"
| "arrow"
| "function_expression";
/**
* One way a callable or class is invoked from outside the application (#72; python #27 parity).
* A node may hold several — two route decorators, or a function that is both a task and a CLI
* command. `confidence` lets a consumer threshold on evidence quality rather than inheriting this
* analyzer's judgement.
*/
export interface TSEntrypoint {
framework: string;
confidence: "declared" | "certain" | "heuristic";
rule: string; // rules file `id:`, or an engine name
ruleset: string; // "shipped" | "user:<path>"
evidence?: string;
route?: string;
http_methods: string[];
via?: string; // can:// id of the routed node dispatching here
}
/**
* Coverage and failure record for the entrypoint pass (#72). The pass under-approximates by
* design, so silence is its failure mode — this is what makes a gap visible instead of
* indistinguishable from "this project has no entrypoints".
*/
export interface TSEntrypointReport {
frameworks_detected: string[];
rulesets: string[];
unresolved: Record<string, number>;
errors: string[];
}
export interface TSCallable {
id: string; // can:// containment id — stamped per-run by assignIds
kind: TSCallableKind;
span: TSSpan;
name: string;
signature: string; // e.g. src/user.UserService.getUser — the internal join key
comments: TSComment[];
decorators: TSDecorator[];
// Entrypoints (#72): stamped per-run by the entrypoint pass, like heritage — the cached tree lacks
// them, the wire always carries them. Empty until a rule matches (units 2-5).
entrypoints?: TSEntrypoint[];
is_entrypoint?: boolean;
parameters: TSCallableParameter[];
type_parameters: TSTypeParameter[];
return_type?: string;
cyclomatic_complexity: number;
accessibility?: string; // public | private | protected
is_static: boolean;
is_abstract: boolean;
is_async: boolean;
is_generator: boolean;
is_optional: boolean; // optional method `foo?()`
is_readonly: boolean;
is_exported: boolean;
is_ambient: boolean; // `declare`
is_implicit: boolean; // synthesized default constructor
accessor_kind?: string; // getter | setter
overload_signatures: TSOverloadSignature[];
body: Record<string, TSBodyNode>; // L1: `call` nodes (l1Body pass); L3+: full statements
callables?: Record<string, TSCallable>; // nested callables (closures) — present only when non-empty
types?: Record<string, TSType>; // nested (local) classes — present only when non-empty
cfg?: TSCfgEdge[]; // L3
cdg?: TSCdgEdge[]; // L3
ddg?: TSDdgEdge[]; // L3→L4
summary?: TSSummaryEdge[]; // L4
// INTERNAL (stripped from the wire)
abs_path: string; // ABSOLUTE file path of the declaration — the resolver's AST-index key
call_sites: TSCallsite[];
config_accesses: TSConfigAccess[]; // INTERNAL (stripped from the wire)
}
// ----------------------------------------------------------------------------------------------
// Type — one node with a single `kind`; the buckets it populates depend on that kind:
// class / interface → callables{} + fields{}; enum → fields{}; type_alias → (leaf);
// namespace → types{} + functions{} + fields{} (a sub-file scope, same buckets as a module).
// ----------------------------------------------------------------------------------------------
export type TSTypeKind = "class" | "interface" | "enum" | "type_alias" | "namespace";
export interface TSType {
id: string; // stamped per-run by assignIds
kind: TSTypeKind;
span: TSSpan;
name: string;
signature: string;
comments: TSComment[];
is_exported: boolean;
is_ambient: boolean;
// class / interface / enum / namespace members
callables?: Record<string, TSCallable>;
fields?: Record<string, TSField>;
types?: Record<string, TSType>; // namespace only
functions?: Record<string, TSCallable>; // namespace only
// class
decorators?: TSDecorator[];
// Entrypoints (#72): class only, stamped per-run — python stamps PyClass; an interface, enum,
// alias or namespace cannot be an entrypoint and never carries these.
entrypoints?: TSEntrypoint[];
is_entrypoint?: boolean;
base_classes?: string[]; // spine: union of extends + implements (signature strings)
implements_types?: string[]; // typed split: just the implemented interfaces
is_abstract?: boolean;
// class / interface / type_alias generics
type_parameters?: TSTypeParameter[];
// interface
call_signatures?: string[]; // raw text of call/construct signatures
index_signatures?: string[]; // raw text of `[key: string]: T`
// enum
is_const?: boolean;
// type_alias
aliased_type?: string; // the RHS type text
// Heritage projection (resolved-only), stamped per-run by the heritage pass:
extends_ids?: string[]; // resolved can:// id(s) of the extended class/interface(s)
implements_ids?: string[]; // resolved can:// id(s) of implemented interfaces (classes only)
}
// ----------------------------------------------------------------------------------------------
// Module (compilation unit / file) — a scope holding types, functions, and free bindings
// ----------------------------------------------------------------------------------------------
export interface TSModule {
id: string; // can://<app>/<lang>/<fileKey> — stamped per-run by assignIds
kind: "module";
span: TSSpan; // whole file
source: string; // full file text, once; every node's text slices off this
imports: TSImport[];
exports: TSExport[];
comments: TSComment[];
types: Record<string, TSType>; // classes/interfaces/enums/type-aliases/namespaces
functions: Record<string, TSCallable>; // free functions
fields: Record<string, TSField>; // module-level const/let/var
is_tsx: boolean;
is_declaration_file: boolean;
// INTERNAL — caching metadata (stripped from the wire)
content_hash?: string;
last_modified?: number;
file_size?: number;
/** INTERNAL (#72 unit 3): top-level call sites, for the `calls:` entrypoint tier. Stripped by emit.ts. */
call_sites?: TSCallsite[];
}
// ----------------------------------------------------------------------------------------------
// Repository-artifact layer (#101; parity with codeanalyzer-python PR #160 / spec
// 2026-08-27-artifacts-and-dependencies-design.md): recognized non-code files as nodes with
// LANGUAGE-NEUTRAL ids, plus evidence-tagged dependency records and the unresolved-import
// hygiene signal. Application-anchored, level-free — identical at every -a level. Capture is
// broad (every rules-matched file becomes a node, verbatim source, unbounded by decision);
// extraction is narrow (only dependency-manifest roles feed `dependencies` this unit).
// ----------------------------------------------------------------------------------------------
/** A configuration key flattened out of a config-bearing artifact (#101 unit B). */
export interface TSConfigKey {
id: string; // `${artifactId}@key/${dotted}` — stamped per-run by assignIds
key: string; // dotted path; numeric segments for arrays ("services.web.ports.0")
namespace: string; // env|json|yaml|toml|ini|properties|dockerfile
value?: string | number | boolean; // present by default; absent under --no-artifact-text
span?: TSSpan; // best-effort: exact for yaml (the parser retains node positions);
// line-based for env/ini/dockerfile; ABSENT for json/jsonc — JSON.parse discards
// source positions, and re-deriving one by searching the text for the key token would
// point at the wrong occurrence whenever a key name repeats under different parents
// (routine in tsconfig/compose). Absent is honest; a wrong span is a lie a consumer would act on.
references: string[]; // recognized ${VAR}/$VAR tokens, deduped, in order
}
/** A recognized non-code file (config, manifest, CI, container spec). */
export interface TSArtifact {
id: string; // can://<app>/artifact/<path> — language-NEUTRAL namespace, stamped per-run
kind: "artifact";
path: string; // repo-relative POSIX path (also the map key)
format: string; // json | jsonc | yaml | toml | ini | requirements? | dockerfile | yarnlock | text | env | binary
roles: string[]; // dependency-manifest | tool-config | container-image | service-topology | ci | env | packaging | legal | docs | script | unknown
size_bytes: number;
sha256: string;
source: string; // verbatim, unbounded by decision (spec §3)
extraction: "none" | "partial" | "full";
config_keys: TSConfigKey[]; // contained children; containment mirrors DEFINES_CONFIG
}
/** One third-party dependency (declared or lockfile-only transitive), evidence-tagged via `prov`. */
export interface TSDependency {
name: string; // npm-native, @scope kept
spec: string; // as declared ("^4.17.21"); "" when the section value is not a string
kind: "runtime" | "dev" | "optional" | "peer" | "build"; // `peer` is the spec'd additive npm token
extras: string[]; // npm has none — always [] (shared shape parity)
declared_in: string; // TSArtifact id (a manifest for direct:true, the lock for direct:false)
direct: boolean; // false = lockfile-only transitive (no manifest declares it)
locked_version?: string;
provides_imports: string[]; // import specifiers this distribution provides (npm: the name; @types/x: x)
prov: string[]; // declared | lockfile | installed-metadata | heuristic
}
/** A non-relative import no declared dependency accounts for (the dependency-hygiene signal). */
export interface TSImportBinding {
module: string; // the specifier root ("express", "@scope/pkg")
bound_to?: string; // best-effort distribution name when partially bound
prov: string[];
}
// ----------------------------------------------------------------------------------------------
// config_use literal tier (#101 unit C2/C3): joins a recognized config READ (a `config_access` or
// detector-table CALL body node) to the declared `TSConfigKey`(s) it names. Runs with the L2 stage
// (src/semantic_analysis/configUse.ts) because CALL rules need the resolved call graph. `src`/
// `site` are GLOBAL ordinal ids (`<callable-id>@<local>`); `dst` is a TSConfigKey id.
// ----------------------------------------------------------------------------------------------
/** One resolved config read: a recognized read whose key closed on exactly one literal that
* matches a declared ConfigKey. `src` is the read's GLOBAL ordinal id; `dst` the key's id. */
export interface TSConfigUse {
src: string;
dst: string;
prov: Array<"literal" | "dataflow">;
}
/** A recognized read that resolved to no declared key — first class, so an untraceable read is
* as visible as a traced one. `config_reads` SHRINKS as levels rise (higher tiers resolve some);
* that is deliberate and is the layer's one non-monotonic section. */
export interface TSConfigRead {
site: string; // GLOBAL ordinal id
callee: string; // the read root ("process.env") or the resolved callee id for call rules
key?: string; // set only for reason "undefined-key"
reason: "non-literal" | "undefined-key";
prov: Array<"literal" | "dataflow">;
}
// ----------------------------------------------------------------------------------------------
// Call-graph edge (identity-only, provider output; endpoints are signature strings until the
// call-graph-ids pass rewrites them onto can:// ids at L2)
// ----------------------------------------------------------------------------------------------
export const CALL_DEP = "CALL_DEP" as const;
export interface TSCallEdge {
source: string; // caller TSCallable.signature
target: string; // callee TSCallable.signature
type: typeof CALL_DEP;
weight: number;
provenance: string[]; // e.g. ["tsc"]
tags: Record<string, string>;
}
// ----------------------------------------------------------------------------------------------
// External (phantom) symbol — a synthetic stub for a call target OUTSIDE the project (an imported
// library / Node builtin). Lets the call graph point at external callees (WALA-style phantom
// nodes) without dropping the edge or dangling: an edge `target` byte-matches either a real
// `Callable.signature` or a `TSExternalSymbol.signature`.
// ----------------------------------------------------------------------------------------------
// Slim: the map key IS the signature (e.g. "commander.parse"), and membership in
// `external_symbols` already means external — so neither is repeated in the value.
export interface TSExternalSymbol {
name: string; // the called member, e.g. "readFileSync"
module: string; // the import/require specifier, e.g. "node:fs", "express", "@scope/pkg"
}
// A first-party anonymous callback a call-graph builder resolved as an edge endpoint but could
// not name against the symbol table (a residual-fallback safety net; since #92 the tree names
// anonymous callables positionally, so this map is normally empty). The map key IS the
// synthesized signature, so an edge `source`/`target` byte-matches it like a real signature.
export interface TSSynthesizedCallable {
name: string; // display name — always "<anonymous>"; the signature carries the precise identity
path: string; // owning module key (project-relative POSIX path WITH extension)
start_line: number;
start_column: number;
}
/**
* The analyzer's INTERNAL working set: the live tree plus the signature-keyed provider outputs.
* `finalizeAnalysis` consumes it and assembles the wire (`TSAnalysis`); it is never serialized
* itself. (The program-graph IR travels separately — see AnalysisResult.)
*/
export interface AnalysisInternal {
symbol_table: Record<string, TSModule>;
call_graph: TSCallEdge[];
external_symbols: Record<string, TSExternalSymbol>;
synthesized_callables: Record<string, TSSynthesizedCallable>;
/** Repository-artifact layer (level-free). */
artifacts?: Record<string, TSArtifact>;
dependencies?: TSDependency[];
unresolved_imports?: TSImportBinding[];
/** config_use literal tier (#101 unit C2/C3) — stamped by finalizeAnalysis, not by core.ts. */
config_uses?: TSConfigUse[];
config_reads?: TSConfigRead[];
}
// ----------------------------------------------------------------------------------------------
// The wire: envelope → application root → cross-callable edges (what analysis.json IS, and what
// the Neo4j projection consumes)
// ----------------------------------------------------------------------------------------------
export interface TSAnalysis {
schema_version: string; // "2.0.0"
language: string; // "typescript"
max_level: number; // highest level populated; consumers read this, not key-sniffing
k_limit?: number; // access-path depth bound for the L3/L4 dataflow (present at L3+)
analyzer: TSAnalyzer; // which analyzer produced this artifact, and at what version
application: TSApplication;
}
/** Analyzer identity — lets consumers correlate an `analysis.json` with the tool/version that emitted it. */
export interface TSAnalyzer {
name: string; // "codeanalyzer-typescript"
version: string; // ANALYZER_VERSION (src/utils/version.ts)
}
/** The application ROOT node (python's PyApplication): the containment tree + app-scope overlays. */
export interface TSApplication {
id: string; // can://<app> — the prefix every id below it shares
name: string; // normalized --app-name, or the input directory basename
kind: "application";
symbol_table: Record<string, TSModule>; // keyed by project-relative POSIX path (with extension)
call_graph: TSCallGraphEdge[]; // L2 — callable → callable (empty at L1)
param_in: TSParamEdge[]; // L4 (empty until L4)
param_out: TSParamEdge[]; // L4
/** Repository-artifact layer — identical at every level (#101, python PR #160 parity). */
artifacts: Record<string, TSArtifact>;
dependencies: TSDependency[];
unresolved_imports: TSImportBinding[];
/** config_use literal tier (#101 unit C2/C3) — empty until L2; CALL rules need the call graph. */
config_uses: TSConfigUse[];
config_reads: TSConfigRead[];
/** Entrypoint coverage report (#72) — level-free, identical at every -a. */
entrypoint_report: TSEntrypointReport;
// TS-additive (parity): edge endpoints outside the containment tree need an id home.
external_symbols?: Record<string, import("./homing").TSExternalNode>; // L2 — library call targets, keyed by id
// L2 — #92 compatibility index: the older anonymous-callable id → the tree id that replaced
// it. Entries whose key equals their own `id` are the residual fallback nodes for signatures no
// provider could name.
synthesized_callables?: Record<string, import("./homing").TSSynthesizedNode>;
}
/** A wire call-graph edge: identity-only, can:// endpoints (l2Callees re-identifies onto these). */
export interface TSCallGraphEdge {
src: string;
dst: string;
prov: string[]; // provenance, e.g. ["tsc"], ["defuse"], ["import"]
weight: number;
}
export interface TSParamEdge {
src: string;
dst: string;
// param_in / param_out: the callee-side formal this edge binds — the parameter name, `$ret`
// for the return port, or the global path on a global read/write. Set on every such edge
// (codeanalyzer-python#195). Optional only because `summary` edges share the shape.
var?: string;
}
// ----------------------------------------------------------------------------------------------
// Tree walkers — the one place the containment reach is defined (shared by the id/body/callee
// passes, the call-graph resolver, and the dataflow join).
// ----------------------------------------------------------------------------------------------
/** Depth-first over every callable in a module: free functions, type members (class/interface
* accessors and methods, namespace functions), and everything nested inside callables. */
export function forEachCallable(mod: TSModule, fn: (c: TSCallable) => void): void {
const visitCallable = (c: TSCallable): void => {
fn(c);
for (const nested of Object.values(c.callables ?? {})) visitCallable(nested);
for (const t of Object.values(c.types ?? {})) visitType(t);
};
const visitType = (t: TSType): void => {
for (const m of Object.values(t.callables ?? {})) visitCallable(m);
for (const f of Object.values(t.functions ?? {})) visitCallable(f); // namespace
for (const nt of Object.values(t.types ?? {})) visitType(nt); // namespace
};
for (const f of Object.values(mod.functions ?? {})) visitCallable(f);
for (const t of Object.values(mod.types ?? {})) visitType(t);
}
/** Depth-first over every type node in a module, including types nested inside callables. */
export function forEachType(mod: TSModule, fn: (t: TSType) => void): void {
const visitType = (t: TSType): void => {
fn(t);
for (const nt of Object.values(t.types ?? {})) visitType(nt);
for (const m of Object.values(t.callables ?? {})) visitCallable(m);
for (const f of Object.values(t.functions ?? {})) visitCallable(f);
};
const visitCallable = (c: TSCallable): void => {
for (const nested of Object.values(c.callables ?? {})) visitCallable(nested);
for (const t of Object.values(c.types ?? {})) visitType(t);
};
for (const t of Object.values(mod.types ?? {})) visitType(t);
for (const f of Object.values(mod.functions ?? {})) visitCallable(f);
}
// ==============================================================================================
// signatureOf — THE linchpin. One canonicalizer, used caller- and callee-side, so ids byte-match.
// ==============================================================================================
/**
* Compute the stable symbol-table key (project-relative POSIX path WITH extension) and the
* module/signature prefix (the same path WITHOUT its extension) for an absolute file path.
*/
export function fileKeyOf(absPath: string, projectRoot: string): { fileKey: string; modulePrefix: string } {
const rel = toPosix(relativePath(projectRoot, absPath));
return { fileKey: rel, modulePrefix: modulePrefixOf(rel) };
}
/**
* Build a signature by dot-joining a scope prefix with one or more member names. The prefix is
* the module/signature prefix (rel path without extension) or an already-built parent signature.
* Constructors normalize to `<ClassSignature>.constructor`.
*/
export function signatureOf(prefix: string, ...members: string[]): string {
return [prefix, ...members].join(".");
}
export function constructorSignatureOf(classSignature: string): string {
return `${classSignature}.constructor`;
}
// --- small path helpers (kept dependency-light so schema.ts has no runtime deps beyond ids) ---
function toPosix(p: string): string {
return p.replace(/\\/g, "/");
}
function relativePath(from: string, to: string): string {
const a = toPosix(from).replace(/\/+$/, "").split("/");
const b = toPosix(to).split("/");
let i = 0;
while (i < a.length && i < b.length && a[i] === b[i]) i++;
const up = a.slice(i).map(() => "..");
const down = b.slice(i);
return [...up, ...down].join("/");
}