@holmes-lab/holmes-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +102 -0
  4. package/bin/holmes-hook-antigravity.js +31 -0
  5. package/bin/holmes-kit.js +23 -0
  6. package/bin/holmes-mcp.js +34 -0
  7. package/bin/holmes-stop-antigravity.js +29 -0
  8. package/dist/.build-id +1 -0
  9. package/dist/holmes/cli/agents.js +168 -0
  10. package/dist/holmes/cli/doctor.js +625 -0
  11. package/dist/holmes/cli/gitignore-merge.js +84 -0
  12. package/dist/holmes/cli/governed-precondition.js +157 -0
  13. package/dist/holmes/cli/index.js +384 -0
  14. package/dist/holmes/cli/init.js +462 -0
  15. package/dist/holmes/cli/playbook-skills.js +711 -0
  16. package/dist/holmes/cli/roles-readme.js +134 -0
  17. package/dist/holmes/cli/settings-merge.js +122 -0
  18. package/dist/holmes/config/config.js +70 -0
  19. package/dist/holmes/context/bundler.js +114 -0
  20. package/dist/holmes/context/render.js +29 -0
  21. package/dist/holmes/context/tiers.js +110 -0
  22. package/dist/holmes/context/tokens.js +8 -0
  23. package/dist/holmes/cpg/cpg-scanner.js +213 -0
  24. package/dist/holmes/cpg/hash-cache.js +86 -0
  25. package/dist/holmes/cpg/language-parser-walk.js +917 -0
  26. package/dist/holmes/cpg/language-parser-worker.js +81 -0
  27. package/dist/holmes/cpg/language-parser.js +234 -0
  28. package/dist/holmes/cpg/scan-cache.js +108 -0
  29. package/dist/holmes/cpg/source-path.js +44 -0
  30. package/dist/holmes/cpg/test-files.js +84 -0
  31. package/dist/holmes/governance/constitution-debt.js +73 -0
  32. package/dist/holmes/governance/constitution-report.js +25 -0
  33. package/dist/holmes/governance/constitution.js +129 -0
  34. package/dist/holmes/governance/identity.js +30 -0
  35. package/dist/holmes/governance/ledger-lock.js +165 -0
  36. package/dist/holmes/governance/ledger-store.conformance.js +90 -0
  37. package/dist/holmes/governance/ledger-store.js +106 -0
  38. package/dist/holmes/governance/progress-ledger.js +83 -0
  39. package/dist/holmes/governance/provenance-chain.js +365 -0
  40. package/dist/holmes/governance/provenance-ledger.js +0 -0
  41. package/dist/holmes/governance/provenance-schema.js +47 -0
  42. package/dist/holmes/governance/replica-id.js +106 -0
  43. package/dist/holmes/governance/role-policy.js +137 -0
  44. package/dist/holmes/governance/trust-score.js +43 -0
  45. package/dist/holmes/guardrail/anchors.js +31 -0
  46. package/dist/holmes/guardrail/blind-spots.js +38 -0
  47. package/dist/holmes/guardrail/decision-ledger.js +107 -0
  48. package/dist/holmes/guardrail/executable-artifact.js +129 -0
  49. package/dist/holmes/guardrail/governance-history.js +101 -0
  50. package/dist/holmes/guardrail/phase.js +169 -0
  51. package/dist/holmes/guardrail/risk-classifier.js +450 -0
  52. package/dist/holmes/guardrail/risk-gate.js +160 -0
  53. package/dist/holmes/guardrail/risk-types.js +6 -0
  54. package/dist/holmes/guardrail/tspec-state.js +392 -0
  55. package/dist/holmes/guardrail/write-target.js +224 -0
  56. package/dist/holmes/hooks/adapters/antigravity.js +194 -0
  57. package/dist/holmes/hooks/pre-tool-use.js +1262 -0
  58. package/dist/holmes/hooks/stop.js +416 -0
  59. package/dist/holmes/mcp/basis.js +162 -0
  60. package/dist/holmes/mcp/handlers.js +1831 -0
  61. package/dist/holmes/mcp/server.js +71 -0
  62. package/dist/holmes/mcp/stdio-client.js +165 -0
  63. package/dist/holmes/mcp/supervisor.js +178 -0
  64. package/dist/holmes/mcp/tool-schemas.js +394 -0
  65. package/dist/holmes/mcp/validate-args.js +281 -0
  66. package/dist/holmes/messages/registry.js +50 -0
  67. package/dist/holmes/project/baseline.js +210 -0
  68. package/dist/holmes/project/change-source.js +233 -0
  69. package/dist/holmes/project/ignore.js +145 -0
  70. package/dist/holmes/project/root.js +113 -0
  71. package/dist/holmes/reverse/anchor.js +162 -0
  72. package/dist/holmes/reverse/cluster.js +187 -0
  73. package/dist/holmes/reverse/draft.js +151 -0
  74. package/dist/holmes/reverse/dynamic-wiring.js +47 -0
  75. package/dist/holmes/reverse/scan.js +194 -0
  76. package/dist/holmes/reverse/surface.js +154 -0
  77. package/dist/holmes/reverse/test-map.js +263 -0
  78. package/dist/holmes/review/coverage.js +33 -0
  79. package/dist/holmes/review/findings.js +123 -0
  80. package/dist/holmes/review/package.js +40 -0
  81. package/dist/holmes/review/review-targets.js +92 -0
  82. package/dist/holmes/review/scope.js +57 -0
  83. package/dist/holmes/review/test-evidence.js +77 -0
  84. package/dist/holmes/review/test-runner.js +572 -0
  85. package/dist/holmes/rtm/dataflow-taint.js +262 -0
  86. package/dist/holmes/rtm/gap-analyzer.js +27 -0
  87. package/dist/holmes/rtm/git-changes.js +72 -0
  88. package/dist/holmes/rtm/incremental.js +45 -0
  89. package/dist/holmes/rtm/localize.js +100 -0
  90. package/dist/holmes/rtm/rtm-builder.js +191 -0
  91. package/dist/holmes/rtm/rtm-check.js +89 -0
  92. package/dist/holmes/rtm/rtm-graph.js +232 -0
  93. package/dist/holmes/rtm/taint.js +92 -0
  94. package/dist/holmes/rtm/test-scope.js +336 -0
  95. package/dist/holmes/spec/approval-blockers.js +204 -0
  96. package/dist/holmes/spec/breaking-change.js +89 -0
  97. package/dist/holmes/spec/legacy-format.js +87 -0
  98. package/dist/holmes/spec/spec-digest.js +71 -0
  99. package/dist/holmes/spec/spec-parser.js +106 -0
  100. package/dist/holmes/spec/spec-store.conformance.js +118 -0
  101. package/dist/holmes/spec/spec-store.js +331 -0
  102. package/dist/holmes/spec/spec-types.js +177 -0
  103. package/dist/holmes/spec/validator.js +280 -0
  104. package/package.json +76 -0
  105. package/playbooks/adopt/PLAYBOOK.md +125 -0
  106. package/playbooks/author-slice/PLAYBOOK.md +119 -0
  107. package/playbooks/promote-slice/PLAYBOOK.md +134 -0
@@ -0,0 +1,917 @@
1
+ 'use strict';
2
+ // Single source of truth for the per-language symbol/edge tree-walking
3
+ // logic. Called from BOTH the inline (production) parse path and the
4
+ // jest-only worker_thread path in language-parser.ts / language-parser-worker.js,
5
+ // so the two paths are guaranteed to produce byte-identical results — there
6
+ // is no second copy of this logic anywhere to drift out of sync.
7
+ // @implements A-SPEC-121.6
8
+ // Table-driven per-language node-type config that drives the single shared
9
+ // lexical symbol walk below (walkSymbols). Adding a new language is meant to
10
+ // be exactly one entry here — the walk itself should not need to change.
11
+ //
12
+ // Fields:
13
+ // classTypes node types that open a class scope; the class's name
14
+ // is read via childForFieldName('name'); entering one
15
+ // sets `enclosingClass` to that name for its children.
16
+ // funcTypes function/method node types whose "is this a method"
17
+ // status depends on lexical context (see
18
+ // funcIsMethodInClass) rather than the node type
19
+ // itself.
20
+ // methodTypes node types that are ALWAYS a method when lexically
21
+ // inside a class (e.g. TypeScript's method_definition,
22
+ // which is also used for object-literal methods —
23
+ // those have no enclosing class and are skipped so we
24
+ // don't emit spurious unqualified method symbols).
25
+ // funcIsMethodInClass when true, a funcTypes node lexically inside a class
26
+ // becomes a method (qualified `Class.name`); when
27
+ // false (as for TypeScript, which has a dedicated
28
+ // methodTypes node type instead), a funcTypes node is
29
+ // always a plain function regardless of context.
30
+ // This flag also controls the nested-def reset rule:
31
+ // recursing into a funcTypes node resets
32
+ // enclosingClass to null for its children ONLY when
33
+ // funcIsMethodInClass is true — i.e. only for
34
+ // languages (like Python) where a nested def's
35
+ // method-ness is context-sensitive, so a def nested
36
+ // inside another def must not be mistaken for a method
37
+ // of the outer class. Languages with a dedicated
38
+ // methodTypes node (TypeScript) don't reset here,
39
+ // matching their original (pre-refactor) behavior.
40
+ const LANG_CONFIG = {
41
+ typescript: {
42
+ classTypes: ['class_declaration'],
43
+ funcTypes: ['function_declaration'],
44
+ methodTypes: ['method_definition'],
45
+ funcIsMethodInClass: false,
46
+ },
47
+ python: {
48
+ classTypes: ['class_definition'],
49
+ funcTypes: ['function_definition'],
50
+ methodTypes: [],
51
+ funcIsMethodInClass: true,
52
+ },
53
+ // @implements A-SPEC-121.6
54
+ // C# is lexical, like TypeScript: class/struct/interface/record all open a
55
+ // class-like scope (all have a `name` field), and method_declaration is
56
+ // ALWAYS a method node type — never a plain function, never context-
57
+ // sensitive — so funcTypes stays empty and funcIsMethodInClass is false,
58
+ // mirroring the typescript shape rather than python's.
59
+ csharp: {
60
+ classTypes: ['class_declaration', 'struct_declaration', 'interface_declaration', 'record_declaration'],
61
+ funcTypes: [],
62
+ methodTypes: ['method_declaration'],
63
+ funcIsMethodInClass: false,
64
+ },
65
+ // @implements A-SPEC-121.6
66
+ // Java is lexical, like C#: class/interface/enum/record all open a
67
+ // class-like scope (all have a `name` field), and method_declaration is
68
+ // ALWAYS a method node type — never a plain function, never context-
69
+ // sensitive — so funcTypes stays empty and funcIsMethodInClass is false,
70
+ // mirroring the csharp shape.
71
+ java: {
72
+ classTypes: ['class_declaration', 'interface_declaration', 'enum_declaration', 'record_declaration'],
73
+ funcTypes: [],
74
+ methodTypes: ['method_declaration'],
75
+ funcIsMethodInClass: false,
76
+ },
77
+ };
78
+ // tsx shares the exact same node-type vocabulary as typescript.
79
+ LANG_CONFIG.tsx = LANG_CONFIG.typescript;
80
+ // One shared lexical walk driven entirely by LANG_CONFIG. Reproduces, node
81
+ // for node, both the original TypeScript walk (method_definition is always a
82
+ // method node, skipped when it has no enclosing class — the object-literal-
83
+ // method case) and the original Python walk (function_definition is a method
84
+ // only when lexically inside a class, and recursing into ANY def resets
85
+ // enclosingClass to null for its children, so a def nested inside a def is
86
+ // never mistaken for a method of the outer class).
87
+ function walkSymbols(tree, config) {
88
+ const out = [];
89
+ const visit = (node, enclosingClass) => {
90
+ const type = node.type;
91
+ if (config.classTypes.includes(type)) {
92
+ const nameNode = node.childForFieldName('name');
93
+ const name = nameNode ? nameNode.text : undefined;
94
+ if (name) {
95
+ out.push({
96
+ name,
97
+ kind: 'class',
98
+ qualifiedName: name,
99
+ startLine: node.startPosition.row + 1,
100
+ endLine: node.endPosition.row + 1,
101
+ });
102
+ }
103
+ const childClass = name || enclosingClass;
104
+ for (let i = 0; i < node.childCount; i++)
105
+ visit(node.child(i), childClass);
106
+ return;
107
+ }
108
+ if (config.funcTypes.includes(type)) {
109
+ const nameNode = node.childForFieldName('name');
110
+ const name = nameNode ? nameNode.text : undefined;
111
+ if (name) {
112
+ const isMethod = config.funcIsMethodInClass && !!enclosingClass;
113
+ const kind = isMethod ? 'method' : 'function';
114
+ const qualifiedName = isMethod ? `${enclosingClass}.${name}` : name;
115
+ out.push({
116
+ name,
117
+ kind,
118
+ qualifiedName,
119
+ startLine: node.startPosition.row + 1,
120
+ endLine: node.endPosition.row + 1,
121
+ });
122
+ }
123
+ // Nested-def reset rule (see funcIsMethodInClass doc above): only
124
+ // context-sensitive languages (Python) reset enclosingClass to null
125
+ // here; languages with a dedicated methodTypes node (TypeScript) carry
126
+ // enclosingClass through unchanged, matching original behavior.
127
+ const childEnclosing = config.funcIsMethodInClass ? null : enclosingClass;
128
+ for (let i = 0; i < node.childCount; i++)
129
+ visit(node.child(i), childEnclosing);
130
+ return;
131
+ }
132
+ if (config.methodTypes.includes(type)) {
133
+ const nameNode = node.childForFieldName('name');
134
+ if (nameNode) {
135
+ const name = nameNode.text;
136
+ // Only emit method symbols for genuine class members
137
+ // (enclosingClass !== null). Skip object-literal methods, which have
138
+ // no enclosing class, to avoid spurious unqualified method symbols.
139
+ if (enclosingClass) {
140
+ out.push({
141
+ name,
142
+ kind: 'method',
143
+ qualifiedName: `${enclosingClass}.${name}`,
144
+ startLine: node.startPosition.row + 1,
145
+ endLine: node.endPosition.row + 1,
146
+ });
147
+ }
148
+ }
149
+ for (let i = 0; i < node.childCount; i++)
150
+ visit(node.child(i), enclosingClass);
151
+ return;
152
+ }
153
+ for (let i = 0; i < node.childCount; i++)
154
+ visit(node.child(i), enclosingClass);
155
+ };
156
+ visit(tree.rootNode, null);
157
+ return out;
158
+ }
159
+ // @implements A-SPEC-121.6
160
+ // Go is NOT lexical the way TypeScript/Python/C#/Java are: Go has no class
161
+ // construct at all, and a method's qualifying "owner" is its RECEIVER TYPE
162
+ // (`func (t T) M(){}` -> `T.M`), not lexical nesting inside a class-like
163
+ // scope. Reusing walkSymbols'/LANG_CONFIG's enclosingClass model would be
164
+ // actively wrong for Go (there is nothing to lexically nest into), so Go
165
+ // gets its own dedicated walker instead of a LANG_CONFIG table entry.
166
+ function walkGoSymbols(tree) {
167
+ const out = [];
168
+ // Descend through a receiver's (possibly pointer, possibly generic) type to
169
+ // the underlying type name, e.g. `(t T)` -> 'T', `(p *T)` -> 'T',
170
+ // `(t T[V])` -> 'T', `(p *T[V])` -> 'T' (Go 1.18+ generic receivers). The
171
+ // qualifying name is always the bare type name — never '*T', never 'T[V]' —
172
+ // so an RTM `T.Method` lookup resolves regardless of pointer/generic form.
173
+ const receiverTypeName = (methodDeclNode) => {
174
+ const receiver = methodDeclNode.childForFieldName('receiver');
175
+ if (!receiver)
176
+ return undefined;
177
+ const paramDecl = receiver.namedChild(0);
178
+ if (!paramDecl)
179
+ return undefined;
180
+ let typeNode = paramDecl.childForFieldName('type');
181
+ if (!typeNode)
182
+ return undefined;
183
+ if (typeNode.type === 'pointer_type')
184
+ typeNode = typeNode.namedChild(0);
185
+ // generic_type is `BaseType[TypeArgs]`; its first named child is the base
186
+ // type_identifier. Unwrap it so `T[V]` qualifies as `T`.
187
+ if (typeNode && typeNode.type === 'generic_type')
188
+ typeNode = typeNode.namedChild(0);
189
+ return typeNode ? typeNode.text : undefined;
190
+ };
191
+ const visit = (node) => {
192
+ const type = node.type;
193
+ if (type === 'function_declaration') {
194
+ const nameNode = node.childForFieldName('name');
195
+ if (nameNode) {
196
+ out.push({
197
+ name: nameNode.text,
198
+ kind: 'function',
199
+ qualifiedName: nameNode.text,
200
+ startLine: node.startPosition.row + 1,
201
+ endLine: node.endPosition.row + 1,
202
+ });
203
+ }
204
+ }
205
+ else if (type === 'type_declaration') {
206
+ // A type_declaration can group multiple type_spec nodes (`type ( A
207
+ // struct{}; B struct{} )`); each spec is emitted with its OWN line
208
+ // range, not the enclosing type_declaration's.
209
+ for (let i = 0; i < node.namedChildCount; i++) {
210
+ const spec = node.namedChild(i);
211
+ // `type_spec` = a normal named type (`type T struct{}`, `type C float64`);
212
+ // `type_alias` = the `=` alias form (`type Alias = int`). Both name a
213
+ // type and are emitted as a class-kind symbol; only these two node
214
+ // types carry a type name here.
215
+ if (spec.type !== 'type_spec' && spec.type !== 'type_alias')
216
+ continue;
217
+ const nameNode = spec.childForFieldName('name');
218
+ if (nameNode) {
219
+ out.push({
220
+ name: nameNode.text,
221
+ kind: 'class',
222
+ qualifiedName: nameNode.text,
223
+ startLine: spec.startPosition.row + 1,
224
+ endLine: spec.endPosition.row + 1,
225
+ });
226
+ }
227
+ }
228
+ }
229
+ else if (type === 'method_declaration') {
230
+ const nameNode = node.childForFieldName('name');
231
+ const recvType = receiverTypeName(node);
232
+ if (nameNode && recvType) {
233
+ out.push({
234
+ name: nameNode.text,
235
+ kind: 'method',
236
+ qualifiedName: `${recvType}.${nameNode.text}`,
237
+ startLine: node.startPosition.row + 1,
238
+ endLine: node.endPosition.row + 1,
239
+ });
240
+ }
241
+ }
242
+ for (let i = 0; i < node.childCount; i++)
243
+ visit(node.child(i));
244
+ };
245
+ visit(tree.rootNode);
246
+ return out;
247
+ }
248
+ // @implements A-SPEC-121.6
249
+ // Rust, like Go, is NOT lexical the way TypeScript/Python/C#/Java are — but
250
+ // unlike Go, it uses the IMPL-BLOCK method model: a method is a plain
251
+ // `function_item` (the SAME node type as a top-level function) that happens
252
+ // to be lexically nested inside an `impl_item`, and it is qualified by the
253
+ // impl's TARGET TYPE (`impl S { fn m(&self){} }` -> `S.m`), not by a receiver
254
+ // parameter the way Go methods are. `impl_item` itself never emits a class
255
+ // symbol — it only opens a qualifying scope for the function_items nested
256
+ // inside it — so this needs its own dedicated walker rather than a
257
+ // LANG_CONFIG entry (walkSymbols' enclosingClass model assumes the "class"
258
+ // node itself is the thing being named, which is exactly backwards for
259
+ // impl_item: the impl names a type it is NOT a definition of).
260
+ function walkRustSymbols(tree) {
261
+ const out = [];
262
+ // The `type` field of an impl_item names the type being implemented for
263
+ // (present on both `impl S {...}` and `impl T for S {...}` — the `trait`
264
+ // field, `T`, is deliberately ignored for qualification purposes: methods
265
+ // in a trait impl are qualified by the IMPLEMENTING type, not the trait).
266
+ // A generic impl's type field is a `generic_type` (`G<X>`) whose first
267
+ // named child is the base `type_identifier`; unwrap it so `G<X>` qualifies
268
+ // as bare `G`, mirroring walkGoSymbols' receiver generic_type unwrap.
269
+ const implTargetTypeName = (implNode) => {
270
+ let typeNode = implNode.childForFieldName('type');
271
+ if (!typeNode)
272
+ return undefined;
273
+ while (typeNode && typeNode.type !== 'type_identifier' && typeNode.namedChildCount > 0) {
274
+ // Unwrap to the bare type_identifier. For a module-path-qualified target
275
+ // (`scoped_type_identifier`, e.g. `a::S` / `crate::Foo`) the actual type
276
+ // is the LAST named child (`S`/`Foo`), not the first (which is the path
277
+ // segment `a`/`crate`). For `generic_type` (`G<X>` -> `G`),
278
+ // `reference_type` (`&S` -> `S`) and other wrappers the base is the first
279
+ // named child. Taking the first child on a scoped_type_identifier would
280
+ // drop qualification entirely (return undefined -> method mis-emitted as
281
+ // a bare unqualified function).
282
+ typeNode =
283
+ typeNode.type === 'scoped_type_identifier'
284
+ ? typeNode.namedChild(typeNode.namedChildCount - 1)
285
+ : typeNode.namedChild(0);
286
+ }
287
+ return typeNode && typeNode.type === 'type_identifier' ? typeNode.text : undefined;
288
+ };
289
+ const visit = (node, enclosingImplType) => {
290
+ const type = node.type;
291
+ if (type === 'struct_item' || type === 'enum_item' || type === 'trait_item') {
292
+ const nameNode = node.childForFieldName('name');
293
+ const name = nameNode ? nameNode.text : undefined;
294
+ if (name) {
295
+ out.push({
296
+ name,
297
+ kind: 'class',
298
+ qualifiedName: name,
299
+ startLine: node.startPosition.row + 1,
300
+ endLine: node.endPosition.row + 1,
301
+ });
302
+ }
303
+ // A trait's DEFAULT-body methods (`trait T{ fn d(&self){} }`) are
304
+ // function_item nodes lexically nested directly inside the trait, with
305
+ // no enclosing impl_item — so trait_item must ALSO open a qualifying
306
+ // scope for its children, exactly like impl_item, or a default method
307
+ // would wrongly surface as an unqualified top-level function. Harmless
308
+ // for struct_item/enum_item, which never have function_item children
309
+ // directly (Rust has no inline-method syntax on structs/enums — only
310
+ // via a separate impl block, which sets its own qualifier regardless).
311
+ const childImplType = name || enclosingImplType;
312
+ for (let i = 0; i < node.childCount; i++)
313
+ visit(node.child(i), childImplType);
314
+ return;
315
+ }
316
+ if (type === 'impl_item') {
317
+ // impl_item does NOT emit a class symbol of its own — it only opens a
318
+ // qualifying scope (the target type name) for its function_item
319
+ // children.
320
+ const implType = implTargetTypeName(node);
321
+ for (let i = 0; i < node.childCount; i++)
322
+ visit(node.child(i), implType);
323
+ return;
324
+ }
325
+ if (type === 'function_item') {
326
+ const nameNode = node.childForFieldName('name');
327
+ const name = nameNode ? nameNode.text : undefined;
328
+ if (name) {
329
+ const isMethod = !!enclosingImplType;
330
+ out.push({
331
+ name,
332
+ kind: isMethod ? 'method' : 'function',
333
+ qualifiedName: isMethod ? `${enclosingImplType}.${name}` : name,
334
+ startLine: node.startPosition.row + 1,
335
+ endLine: node.endPosition.row + 1,
336
+ });
337
+ }
338
+ // A function nested inside this function_item's body (a local fn) is
339
+ // never itself a method, regardless of whether THIS function_item was
340
+ // one — reset enclosingImplType to undefined for its children.
341
+ for (let i = 0; i < node.childCount; i++)
342
+ visit(node.child(i), undefined);
343
+ return;
344
+ }
345
+ for (let i = 0; i < node.childCount; i++)
346
+ visit(node.child(i), enclosingImplType);
347
+ };
348
+ visit(tree.rootNode, undefined);
349
+ return out;
350
+ }
351
+ // @implements A-SPEC-121.6
352
+ // C++ IS lexical (member functions live inside class_specifier/struct_specifier,
353
+ // like TypeScript/Python/C#/Java) — but unlike those, the function's NAME is
354
+ // never a direct `name` field on the definition node itself. A
355
+ // `function_definition`'s name lives inside a `declarator` chain
356
+ // (function_declarator -> identifier/field_identifier/qualified_identifier),
357
+ // so walkSymbols' `childForFieldName('name')` model does not apply and C++
358
+ // needs its own dedicated walker rather than a LANG_CONFIG entry.
359
+ function walkCppSymbols(tree) {
360
+ const out = [];
361
+ // Descend through a function_declarator's `declarator` field to the
362
+ // underlying name node, unwrapping pointer/reference-returning-function
363
+ // wrappers (`int* foo()` / `int& foo()` parse the declarator as
364
+ // pointer_declarator/reference_declarator wrapping the function_declarator,
365
+ // or vice versa depending on nesting) until an identifier/field_identifier/
366
+ // qualified_identifier is reached, or giving up (undefined) if none is found.
367
+ const resolveDeclaratorName = (declNode) => {
368
+ let n = declNode;
369
+ while (n) {
370
+ if (n.type === 'identifier' || n.type === 'field_identifier' || n.type === 'qualified_identifier') {
371
+ return n;
372
+ }
373
+ if (n.type === 'function_declarator' || n.type === 'pointer_declarator' || n.type === 'reference_declarator') {
374
+ // `reference_declarator` (a reference-return function/method like
375
+ // `int& bar()` or `C& operator=(...)`) has NO named `declarator` field
376
+ // in tree-sitter-cpp — its wrapped declarator is its first named child.
377
+ // Falling back to namedChild(0) recovers the name instead of silently
378
+ // dropping the whole symbol. function_declarator/pointer_declarator do
379
+ // expose the `declarator` field, so this fallback only fires when the
380
+ // field is absent.
381
+ const next = n.childForFieldName('declarator') || n.namedChild(0);
382
+ if (!next || next === n)
383
+ return undefined;
384
+ n = next;
385
+ continue;
386
+ }
387
+ return undefined;
388
+ }
389
+ return undefined;
390
+ };
391
+ const visit = (node, enclosingClass) => {
392
+ const type = node.type;
393
+ if (type === 'class_specifier' || type === 'struct_specifier') {
394
+ const nameNode = node.childForFieldName('name');
395
+ const name = nameNode ? nameNode.text : undefined;
396
+ if (name) {
397
+ out.push({
398
+ name,
399
+ kind: 'class',
400
+ qualifiedName: name,
401
+ startLine: node.startPosition.row + 1,
402
+ endLine: node.endPosition.row + 1,
403
+ });
404
+ }
405
+ const childClass = name || enclosingClass;
406
+ for (let i = 0; i < node.childCount; i++)
407
+ visit(node.child(i), childClass);
408
+ return;
409
+ }
410
+ if (type === 'function_definition') {
411
+ // The declarator field of a function_definition is a function_declarator
412
+ // (possibly wrapped in pointer_declarator/reference_declarator for a
413
+ // pointer/reference return type, e.g. `int* foo(){}` / `int& foo(){}`);
414
+ // resolveDeclaratorName unwraps those wrappers uniformly, drilling down
415
+ // to the innermost identifier/field_identifier/qualified_identifier.
416
+ const declarator = node.childForFieldName('declarator');
417
+ const nameNode = declarator ? resolveDeclaratorName(declarator) : undefined;
418
+ if (nameNode) {
419
+ const startLine = node.startPosition.row + 1;
420
+ const endLine = node.endPosition.row + 1;
421
+ if (nameNode.type === 'qualified_identifier') {
422
+ // Out-of-line member definition, e.g. `void C::outOfLine(){}` or a
423
+ // multi-segment `A::B::m` — the qualifying CLASS is always the
424
+ // segment immediately before the LAST '::'; the method name is the
425
+ // final segment.
426
+ const segments = nameNode.text.split('::');
427
+ const name = segments[segments.length - 1];
428
+ const cls = segments.length > 1 ? segments[segments.length - 2] : undefined;
429
+ if (name && cls) {
430
+ out.push({
431
+ name,
432
+ kind: 'method',
433
+ qualifiedName: `${cls}.${name}`,
434
+ startLine,
435
+ endLine,
436
+ });
437
+ }
438
+ }
439
+ else {
440
+ // identifier -> plain function, unless lexically inside a class
441
+ // (which shouldn't normally happen for a bare identifier declarator,
442
+ // but is handled defensively); field_identifier -> always a member
443
+ // function (only appears inside a class/struct body).
444
+ const name = nameNode.text;
445
+ const isMethod = nameNode.type === 'field_identifier' || !!enclosingClass;
446
+ const kind = isMethod ? 'method' : 'function';
447
+ const qualifiedName = isMethod && enclosingClass ? `${enclosingClass}.${name}` : name;
448
+ out.push({ name, kind, qualifiedName, startLine, endLine });
449
+ }
450
+ }
451
+ // A function nested inside this function's body is never itself a
452
+ // method of the outer enclosing class merely by lexical accident, but
453
+ // C++ has no local function/class definitions inside a function body
454
+ // in the way Python does — recurse with enclosingClass unchanged is
455
+ // harmless here since function bodies can't contain further
456
+ // function_definition/class_specifier nodes at the top level we care
457
+ // about. Recurse into children (e.g. lambda bodies) preserving context.
458
+ for (let i = 0; i < node.childCount; i++)
459
+ visit(node.child(i), enclosingClass);
460
+ return;
461
+ }
462
+ // namespace_definition is deliberately NOT a class-like scope: a
463
+ // function_definition lexically inside a namespace is still a plain
464
+ // top-level FUNCTION (not qualified by the namespace) — so no special
465
+ // handling is needed here; it just falls through to the generic recurse
466
+ // below with enclosingClass unchanged (namespaces don't open a class scope).
467
+ for (let i = 0; i < node.childCount; i++)
468
+ visit(node.child(i), enclosingClass);
469
+ };
470
+ visit(tree.rootNode, undefined);
471
+ return out;
472
+ }
473
+ function extractSymbolsFromTree(tree, lang = 'typescript') {
474
+ if (lang === 'go')
475
+ return walkGoSymbols(tree);
476
+ if (lang === 'rust')
477
+ return walkRustSymbols(tree);
478
+ if (lang === 'cpp')
479
+ return walkCppSymbols(tree);
480
+ const config = LANG_CONFIG[lang] || LANG_CONFIG.typescript;
481
+ return walkSymbols(tree, config);
482
+ }
483
+ // @implements A-SPEC-136
484
+ // Per-language call-edge extraction table, measured against the installed grammars. Adding a language
485
+ // is one row here — the same table-driven shape symbol extraction already uses (LANG_CONFIG). The
486
+ // shared walkEdges below reads it. `callTypes` is the call-node type; `calleeField` is the field
487
+ // holding the callee (an identifier, or a member/selector node that walkEdges unwraps to its last
488
+ // identifier segment); `scopeTypes` are the class/function nodes whose `name` builds the enclosing
489
+ // qualified path (so a method's edges resolve to `Class.method`, matching the symbol walk).
490
+ const EDGE_CONFIG = {
491
+ go: { callTypes: ['call_expression'], calleeField: 'function', scopeTypes: ['function_declaration', 'method_declaration'] },
492
+ rust: { callTypes: ['call_expression'], calleeField: 'function', scopeTypes: ['function_item'] },
493
+ java: { callTypes: ['method_invocation'], calleeField: 'name', scopeTypes: ['class_declaration', 'method_declaration'] },
494
+ csharp: { callTypes: ['invocation_expression'], calleeField: 'function', scopeTypes: ['class_declaration', 'method_declaration'] },
495
+ cpp: { callTypes: ['call_expression'], calleeField: 'function', scopeTypes: ['function_definition'] },
496
+ };
497
+ // The callee's bare name: a plain identifier is itself; a member/selector/scoped/field node
498
+ // (`o.m`, `self.c`, `mod::f`, `this.C`) unwraps to its LAST identifier segment — the method name.
499
+ // The receiver's type is never inferred (precision over recall); addCallEdges decides ambiguity.
500
+ function calleeNameOf(node) {
501
+ if (!node)
502
+ return null;
503
+ if (node.type === 'identifier' || node.type === 'field_identifier')
504
+ return node.text;
505
+ // member/selector/scoped access: the last named child that is an identifier is the method name.
506
+ let last = null;
507
+ for (let i = 0; i < node.namedChildCount; i++) {
508
+ const c = node.namedChild(i);
509
+ if (c.type === 'identifier' || c.type === 'field_identifier')
510
+ last = c.text;
511
+ }
512
+ return last;
513
+ }
514
+ // @implements A-SPEC-136
515
+ // The shared table-driven edge walk. `from` is the enclosing definition's QUALIFIED name (so it
516
+ // matches the symbol node id and can be resolved), `to` is the bare callee name. A call the config
517
+ // cannot read emits nothing — never a guessed edge.
518
+ function walkEdges(tree, cfg) {
519
+ const out = [];
520
+ const scope = new Set(cfg.scopeTypes);
521
+ const callTypes = new Set(cfg.callTypes);
522
+ // A scope node's name. Most languages expose a `name` field; C++ nests it inside the declarator
523
+ // chain (function_definition → function_declarator → identifier), so fall back to descending
524
+ // `declarator` fields to the first identifier — otherwise a C++ function's edges would qualify to
525
+ // `<module>` and never resolve to its bare symbol name.
526
+ const scopeName = (n) => {
527
+ const nm = n.childForFieldName('name');
528
+ if (nm)
529
+ return nm.text;
530
+ let d = n.childForFieldName('declarator');
531
+ for (let hops = 0; d && hops < 5; hops++) {
532
+ if (d.type === 'identifier' || d.type === 'field_identifier')
533
+ return d.text;
534
+ if (d.type === 'qualified_identifier' || d.type === 'scoped_identifier')
535
+ return calleeNameOf(d); // last segment
536
+ d = d.childForFieldName('declarator');
537
+ }
538
+ return null;
539
+ };
540
+ const enclosing = (node) => {
541
+ const parts = [];
542
+ let n = node.parent;
543
+ while (n) {
544
+ if (scope.has(n.type)) {
545
+ const nm = scopeName(n);
546
+ if (nm)
547
+ parts.unshift(nm);
548
+ }
549
+ n = n.parent;
550
+ }
551
+ return parts.length ? parts.join('.') : '<module>';
552
+ };
553
+ const visit = (node) => {
554
+ if (callTypes.has(node.type)) {
555
+ const callee = calleeNameOf(node.childForFieldName(cfg.calleeField));
556
+ if (callee)
557
+ out.push({ from: enclosing(node), to: callee, rel: 'calls' });
558
+ }
559
+ for (let i = 0; i < node.childCount; i++)
560
+ visit(node.child(i));
561
+ };
562
+ visit(tree.rootNode);
563
+ return out;
564
+ }
565
+ function extractEdgesFromTree(tree, lang = 'typescript') {
566
+ // @implements A-SPEC-136
567
+ // Call edges ARE wired end-to-end: CpgScanner.scan populates ScannedFile.edges from here,
568
+ // addCallEdges resolves bare callee names to graph edges (precision-over-recall), and impactedBy
569
+ // traverses `callersOf`. TS/JS/Python have bespoke walks (kept for their import handling); the
570
+ // other five are table-driven (EDGE_CONFIG + walkEdges). See the CodeEdge comment in
571
+ // language-parser.ts.
572
+ if (lang === 'python')
573
+ return walkPythonEdges(tree);
574
+ if (EDGE_CONFIG[lang])
575
+ return walkEdges(tree, EDGE_CONFIG[lang]);
576
+ // TS/JS family: the original walk below (its import_statement handling is TS-grammar-specific).
577
+ if (lang !== 'typescript' && lang !== 'tsx')
578
+ return [];
579
+ const out = [];
580
+ // QUALIFIED, to match extractSymbolsFromTree: a method inside `class Foo` is `Foo.method`, which is
581
+ // the name the symbol walk emitted and therefore the only name a graph node can be resolved from.
582
+ // Returning the bare method name here would make every method's outgoing edges unresolvable.
583
+ const enclosingFn = (node) => {
584
+ const parts = [];
585
+ let n = node.parent;
586
+ while (n) {
587
+ if (n.type === 'function_declaration' || n.type === 'method_definition' || n.type === 'class_declaration') {
588
+ const nm = n.childForFieldName('name');
589
+ if (nm)
590
+ parts.unshift(nm.text);
591
+ }
592
+ n = n.parent;
593
+ }
594
+ return parts.length ? parts.join('.') : '<module>';
595
+ };
596
+ const visit = (node) => {
597
+ if (node.type === 'import_statement') {
598
+ const src = node.childForFieldName('source') ?? (node.namedChildren && node.namedChildren.find((c) => c.type === 'string'));
599
+ if (src)
600
+ out.push({ from: '<module>', to: src.text.replace(/['"]/g, ''), rel: 'imports' });
601
+ }
602
+ else if (node.type === 'call_expression') {
603
+ const fn = node.childForFieldName('function');
604
+ if (fn && (fn.type === 'identifier' || fn.type === 'member_expression')) {
605
+ const property = fn.childForFieldName && fn.childForFieldName('property');
606
+ const callee = fn.type === 'identifier' ? fn.text : (property ? property.text : fn.text);
607
+ out.push({ from: enclosingFn(node), to: callee, rel: 'calls' });
608
+ }
609
+ }
610
+ for (let i = 0; i < node.childCount; i++)
611
+ visit(node.child(i));
612
+ };
613
+ visit(tree.rootNode);
614
+ return out;
615
+ }
616
+ /**
617
+ * Python call/import edges.
618
+ *
619
+ * Mirrors the TS walk's contract exactly: `from` is the enclosing definition's QUALIFIED name (so it
620
+ * matches what extractSymbolsFromTree emitted for the same node and can be resolved to a graph node
621
+ * id), `to` is the bare callee name for calls and the module path for imports.
622
+ *
623
+ * PRECISION OVER RECALL, per the target architecture's D-CPG principle. Only forms whose target is
624
+ * syntactically evident are emitted: a direct `f()` or `obj.m()` call, and an `import x` /
625
+ * `from x import y`. Dynamic dispatch (`getattr(o, name)()`), `importlib.import_module(var)` and
626
+ * string-keyed registries produce NOTHING rather than a guessed edge — a wrong edge is worse than a
627
+ * missing one, because the missing one shows up as an unanchored gap while the wrong one silently
628
+ * corrupts every impact answer that traverses it.
629
+ */
630
+ function walkPythonEdges(tree) {
631
+ const out = [];
632
+ // Qualified enclosing name: `Class.method` for a def inside a class, matching the symbol walk.
633
+ const enclosing = (node) => {
634
+ const parts = [];
635
+ let n = node.parent;
636
+ while (n) {
637
+ if (n.type === 'function_definition' || n.type === 'class_definition') {
638
+ const nm = n.childForFieldName('name');
639
+ if (nm)
640
+ parts.unshift(nm.text);
641
+ }
642
+ n = n.parent;
643
+ }
644
+ return parts.length ? parts.join('.') : '<module>';
645
+ };
646
+ const moduleName = (n) => {
647
+ if (!n)
648
+ return '';
649
+ if (n.type === 'aliased_import') {
650
+ const nm = n.childForFieldName('name');
651
+ return nm ? nm.text : '';
652
+ }
653
+ return n.text;
654
+ };
655
+ const visit = (node) => {
656
+ if (node.type === 'import_statement') {
657
+ for (const c of node.namedChildren ?? []) {
658
+ const m = moduleName(c);
659
+ if (m)
660
+ out.push({ from: '<module>', to: m, rel: 'imports' });
661
+ }
662
+ }
663
+ else if (node.type === 'import_from_statement') {
664
+ const m = moduleName(node.childForFieldName('module_name'));
665
+ if (m)
666
+ out.push({ from: '<module>', to: m, rel: 'imports' });
667
+ }
668
+ else if (node.type === 'call') {
669
+ const fn = node.childForFieldName('function');
670
+ if (fn && (fn.type === 'identifier' || fn.type === 'attribute')) {
671
+ // `obj.method()` -> `method`, matching the TS walk which takes the member expression's
672
+ // property. The receiver is not resolvable without type inference, which is out of scope.
673
+ const attr = fn.childForFieldName && fn.childForFieldName('attribute');
674
+ const callee = fn.type === 'identifier' ? fn.text : (attr ? attr.text : fn.text);
675
+ if (callee)
676
+ out.push({ from: enclosing(node), to: callee, rel: 'calls' });
677
+ }
678
+ }
679
+ for (let i = 0; i < node.childCount; i++)
680
+ visit(node.child(i));
681
+ };
682
+ visit(tree.rootNode);
683
+ return out;
684
+ }
685
+ // @implements A-SPEC-131
686
+ // The ONE parse form both the inline and worker paths use. node-tree-sitter's parse(string) throws
687
+ // 'Invalid argument' above 32,767 chars (measured: handlers.ts at 37,289 chars — silently dropped
688
+ // from the graph for the project's entire history). An explicit bufferSize removes the cliff.
689
+ //
690
+ // MEASURED CONTRACT (adversarial-review sweep, 2026-08-06): the requirement is
691
+ // bufferSize >= the input's UTF-16 LENGTH — not its UTF-8 byte length. handlers.ts (37,289 units)
692
+ // fails at 32,768 and parses at 65,536; a 200K-unit Korean-heavy source fails at 65,536 regardless
693
+ // of its ~2.9x UTF-8 expansion; a 600K-unit source parses with bufferSize below its byte length.
694
+ // So `length * 2` over-provisions by design and NO input can re-introduce the cliff. Verified
695
+ // tree-identical to string parsing on small input, and error-free on the previously-throwing file.
696
+ function parseWithBuffer(parser, code) {
697
+ return parser.parse(code, undefined, { bufferSize: Math.max(1024 * 1024, code.length * 2 + 16) });
698
+ }
699
+ // @implements A-SPEC-140.1
700
+ // Data-flow FACTS — the four kinds a def-use fixpoint needs, and nothing more. This layer performs
701
+ // no analysis: it reports what the tree literally says, tagged with the enclosing function's
702
+ // qualified name so the facts join the call graph produced by extractEdgesFromTree.
703
+ //
704
+ // The identity scheme is deliberately the SAME as the TS/JS edge walk's `enclosingFn` — a fact whose
705
+ // `fn` did not match an edge's `from` would make every inter-procedural hop silently unresolvable,
706
+ // which reads as "clean" rather than "not analysed". A test pins the two against each other.
707
+ const DF_SCOPE_TYPES = new Set(['function_declaration', 'method_definition', 'class_declaration']);
708
+ const DF_FUNC_TYPES = new Set([
709
+ 'function_declaration', 'method_definition', 'function_expression', 'arrow_function', 'function',
710
+ ]);
711
+ const EXPR_MAX = 200;
712
+ // An expression as STRUCTURE, not text: `{ text, refs, callees }`.
713
+ //
714
+ // Text alone forces the propagation pass to ask "does this mention a tainted variable?" lexically,
715
+ // and two ordinary lines break that: `const note = "raw user input";` falsely taints `note` because
716
+ // the literal contains the word, and `exec(sanitize(raw))` looks tainted because the argument text
717
+ // contains `raw` even though it was sanitized inline. Both are precisely the false positives REQ-140
718
+ // exists to remove, so the structure is recovered HERE, from the tree, once.
719
+ //
720
+ // `refs` are the identifiers READ: a property name after a dot is not a read (`req.body.cmd` reads
721
+ // only `req`), and a callee is not a read. `callees` are the invoked names, by the same last-segment
722
+ // rule the edge walk uses. Both are deduped and in first-appearance order, which makes them
723
+ // deterministic and diffable.
724
+ function exprOf(node) {
725
+ const refs = [], callees = [];
726
+ const seenRef = new Set(), seenCallee = new Set();
727
+ if (!node)
728
+ return { text: '', refs, callees };
729
+ const addRef = (name) => { if (name && !seenRef.has(name)) {
730
+ seenRef.add(name);
731
+ refs.push(name);
732
+ } };
733
+ const addCallee = (name) => { if (name && !seenCallee.has(name)) {
734
+ seenCallee.add(name);
735
+ callees.push(name);
736
+ } };
737
+ const visit = (n) => {
738
+ if (n.type === 'call_expression') {
739
+ const fnNode = n.childForFieldName('function');
740
+ if (fnNode) {
741
+ if (fnNode.type === 'identifier') {
742
+ addCallee(fnNode.text);
743
+ }
744
+ else if (fnNode.type === 'member_expression') {
745
+ const prop = fnNode.childForFieldName('property');
746
+ addCallee(prop ? prop.text : null);
747
+ // The RECEIVER is genuinely read (`o.m(x)` reads `o`), so descend into the object side
748
+ // only — descending into the whole member_expression would re-add the method name.
749
+ const obj = fnNode.childForFieldName('object');
750
+ if (obj)
751
+ visit(obj);
752
+ }
753
+ else {
754
+ visit(fnNode); // computed/complex callee: no name to record, but its parts are read
755
+ }
756
+ }
757
+ const args = n.childForFieldName('arguments');
758
+ if (args)
759
+ for (let i = 0; i < args.childCount; i++)
760
+ visit(args.child(i));
761
+ return; // children handled explicitly above
762
+ }
763
+ if (n.type === 'member_expression') {
764
+ const obj = n.childForFieldName('object');
765
+ if (obj)
766
+ visit(obj);
767
+ // The `property` side is a name in the object, not a variable in scope — never a read.
768
+ // A COMPUTED index (`o[k]`) is a real read of `k`, and the grammar gives it as a
769
+ // subscript_expression, which falls through to the generic recursion below instead.
770
+ return;
771
+ }
772
+ if (n.type === 'identifier' || n.type === 'shorthand_property_identifier')
773
+ addRef(n.text);
774
+ for (let i = 0; i < n.childCount; i++)
775
+ visit(n.child(i));
776
+ };
777
+ visit(node);
778
+ return { text: node.text.trim().slice(0, EXPR_MAX), refs, callees };
779
+ }
780
+ // Which languages have a data-flow extractor. A language absent here yields `undefined` — NOT an
781
+ // empty fact set, which a caller would be entitled to read as "analysed and found nothing".
782
+ const DATAFLOW_LANGS = new Set(['typescript', 'tsx']);
783
+ function extractDataFlowFromTree(tree, lang = 'typescript') {
784
+ if (!DATAFLOW_LANGS.has(lang))
785
+ return undefined;
786
+ const defs = [], calls = [], params = [], returns = [];
787
+ // Qualified enclosing name, identical in construction to the TS edge walk's enclosingFn.
788
+ const enclosing = (node) => {
789
+ const parts = [];
790
+ let n = node.parent;
791
+ while (n) {
792
+ if (DF_SCOPE_TYPES.has(n.type)) {
793
+ const nm = n.childForFieldName('name');
794
+ if (nm)
795
+ parts.unshift(nm.text);
796
+ }
797
+ n = n.parent;
798
+ }
799
+ return parts.length ? parts.join('.') : '<module>';
800
+ };
801
+ // A function node's own qualified name, or null when it has none — `enclosing` walks from the
802
+ // PARENT, so a function's parameters (which live inside the function node) must be attributed to
803
+ // the function itself.
804
+ //
805
+ // An ANONYMOUS function (arrow, function expression) yields null and therefore emits NO parameter
806
+ // facts. Measured why: attributing them to the enclosing scope collapsed three distinct top-level
807
+ // arrows onto `{fn:'<module>', index:0}` with three different names — not a missing fact but a
808
+ // meaningless one, and `<module>` has no parameters. Losing the hop is a disclosed false negative;
809
+ // inventing an owner is the kind of wrong fact this walk exists to avoid.
810
+ //
811
+ // NAMED LIMIT, MEASURED. Counting function nodes: 88.6% of this repo's own src (2452/2767) and
812
+ // 83.1% of `reference/`'s OSS TS (4093/4927) are anonymous. Most of that is inline callbacks,
813
+ // which need no parameter binding — the subset that actually costs coverage is a function BOUND
814
+ // TO A NAME (`const f = (a) => {}`), and that is 248 in src and 14 in reference. So in this
815
+ // repository roughly 44% of name-callable functions (248 of 563) contribute no parameter facts.
816
+ //
817
+ // Naming such a function from its declarator would need the CALL walk to agree — a call inside
818
+ // that arrow is attributed to `<module>` today, so taint entering `f` still could not reach
819
+ // anything `f` calls. A half-fix would raise the fact count while leaving the hop broken, which
820
+ // reads as coverage that does not exist. The real fix is one identity-scheme change shared by
821
+ // both walks (A-SPEC-136 territory, and it would improve impact analysis too, which suffers the
822
+ // same collapse). The two schemes staying identical is the invariant that lets facts join the
823
+ // graph at all, so they move together or not at all.
824
+ const selfName = (fnNode) => {
825
+ const nm = fnNode.childForFieldName('name');
826
+ if (!nm)
827
+ return null;
828
+ const outer = enclosing(fnNode);
829
+ return outer === '<module>' ? nm.text : `${outer}.${nm.text}`;
830
+ };
831
+ const visit = (node) => {
832
+ if (node.type === 'variable_declarator') {
833
+ const nm = node.childForFieldName('name');
834
+ const val = node.childForFieldName('value');
835
+ // Only a bare identifier target: a destructuring pattern binds several names through a
836
+ // structure this layer does not model, so it yields no def rather than a guessed one.
837
+ if (nm && nm.type === 'identifier' && val) {
838
+ defs.push({ fn: enclosing(node), name: nm.text, expr: exprOf(val), line: node.startPosition.row + 1 });
839
+ }
840
+ }
841
+ else if (node.type === 'assignment_expression') {
842
+ const left = node.childForFieldName('left');
843
+ const right = node.childForFieldName('right');
844
+ if (left && left.type === 'identifier' && right) {
845
+ defs.push({ fn: enclosing(node), name: left.text, expr: exprOf(right), line: node.startPosition.row + 1 });
846
+ }
847
+ }
848
+ else if (node.type === 'call_expression') {
849
+ const fn = node.childForFieldName('function');
850
+ if (fn && (fn.type === 'identifier' || fn.type === 'member_expression')) {
851
+ const property = fn.childForFieldName && fn.childForFieldName('property');
852
+ const callee = fn.type === 'identifier' ? fn.text : (property ? property.text : null);
853
+ if (callee) {
854
+ // ORDERED argument texts. Position is what binds an argument to a parameter, so this list
855
+ // is never sorted, deduped, or filtered — a dropped element would shift every later
856
+ // binding onto the wrong slot.
857
+ const argsNode = node.childForFieldName('arguments');
858
+ const args = [];
859
+ if (argsNode) {
860
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
861
+ const a = argsNode.namedChild(i);
862
+ if (a.type === 'comment')
863
+ continue;
864
+ args.push(exprOf(a));
865
+ }
866
+ }
867
+ calls.push({ fn: enclosing(node), callee, args, line: node.startPosition.row + 1 });
868
+ }
869
+ }
870
+ }
871
+ else if (DF_FUNC_TYPES.has(node.type)) {
872
+ const ps = node.childForFieldName('parameters');
873
+ const owner = selfName(node);
874
+ if (ps && owner) {
875
+ for (let i = 0; i < ps.namedChildCount; i++) {
876
+ const p = ps.namedChild(i);
877
+ if (p.type === 'comment')
878
+ continue;
879
+ // The parameter's bare identifier. `x`, `x: T`, `x = d` and `x: T = d` all expose one;
880
+ // a destructuring or rest pattern does not, and is SKIPPED — but its POSITION still
881
+ // consumes an index, or every later parameter would bind one slot too early.
882
+ const name = paramIdentifier(p);
883
+ if (name)
884
+ params.push({ fn: owner, index: i, name });
885
+ }
886
+ }
887
+ }
888
+ else if (node.type === 'return_statement') {
889
+ // A bare `return;` has no named child and carries no value — no fact.
890
+ const val = node.namedChildCount > 0 ? node.namedChild(0) : null;
891
+ if (val && val.type !== 'comment') {
892
+ returns.push({ fn: enclosing(node), expr: exprOf(val), line: node.startPosition.row + 1 });
893
+ }
894
+ }
895
+ for (let i = 0; i < node.childCount; i++)
896
+ visit(node.child(i));
897
+ };
898
+ visit(tree.rootNode);
899
+ return { defs, calls, params, returns };
900
+ }
901
+ // The bare identifier a parameter binds, or null when the parameter binds through a pattern this
902
+ // layer does not model. Never returns the pattern's text: naming `{a, b}` as `a` would bind taint to
903
+ // a variable that does not exist at that position.
904
+ function paramIdentifier(p) {
905
+ if (p.type === 'identifier')
906
+ return p.text;
907
+ // required_parameter / optional_parameter (TS) wrap the binding in a `pattern` field.
908
+ const pat = p.childForFieldName && p.childForFieldName('pattern');
909
+ if (pat && pat.type === 'identifier')
910
+ return pat.text;
911
+ // `x = default` in plain JS: assignment_pattern with an identifier on the left.
912
+ const left = p.childForFieldName && p.childForFieldName('left');
913
+ if (left && left.type === 'identifier')
914
+ return left.text;
915
+ return null;
916
+ }
917
+ module.exports = { LANG_CONFIG, DATAFLOW_LANGS, extractSymbolsFromTree, extractEdgesFromTree, extractDataFlowFromTree, walkGoSymbols, walkRustSymbols, walkPythonEdges, parseWithBuffer };