@holmes-lab/holmes-kit 0.6.0 → 0.7.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,36 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+ <!-- @implements A-SPEC-209 -->
8
+ ## [0.7.0] - 2026-09-02
9
+
10
+ GOAL-7langs completes: all six analysis axes (relations, ast, cfg, ddg, cdg, taint) are now
11
+ corpus-validated for all seven official languages — 42 cells, every one a measurement. The
12
+ README's language table becomes a byte-pinned render of the same derivation, so it can no
13
+ longer drift from what the code actually does.
14
+
15
+ ### Added
16
+
17
+ - **Taint for all seven languages** (REQ-524): five vocabularies join the table (selected by
18
+ extension, never unioned), the flow-sensitive lane generalizes from Python-only to every
19
+ language with a CFG, and two measured engine holes close (Java/C# call nodes including
20
+ constructors; Java callee vs receiver). Validated with positive AND negative controls per
21
+ language, then measured over ten corpora: Go/Rust/Java/C# report zero (library code — same
22
+ structural decomposition as before), C++ reports two REAL argv→execv flows in leveldb's test
23
+ harness (verified; benign by intent, exactly what a taint tool should surface).
24
+ - **Imports reach the graph in all seven languages** (REQ-525): extraction is a table row per
25
+ language; resolution is per-language semantics (Go strips the go.mod module prefix and fans
26
+ out to the package directory — with go.mod discovered by walking up from the file; Rust maps
27
+ crate::/super::/self:: and retries once without a trailing item segment; Java/C# resolve by
28
+ UNIQUE path suffix; C++ resolves quoted includes with extension preserved); ambiguity and
29
+ externals resolve to nothing, never to a guess. **Capability moved on measured arrival:
30
+ go 389, rust 64, java 2,688, csharp 316 (measurement overturned the ~0 expectation), cpp 482
31
+ resolved edges.**
32
+ - **The README language table is a render** (REQ-526): a marker block generated from the live
33
+ matrix plus a corpus-evidence column (functions, zero violations, stated limits — C++'s 32.1%
34
+ L1 refusal rate is in the table), byte-pinned by the suite like docs/language-support.md.
35
+ The old hand-written table claimed "Full Support" for every language before it was true.
36
+
7
37
  <!-- @implements A-SPEC-209 -->
8
38
  ## [0.6.0] - 2026-09-02
9
39
 
package/README.md CHANGED
@@ -217,15 +217,21 @@ flowchart LR
217
217
  Holmes-Kit embeds native AST & Code Property Graph (D-CPG) analyzers to track causal relationships and anchor implementations across diverse technology stacks.
218
218
 
219
219
  ### 💻 Supported Programming Languages
220
- | Language | CPG Parser | Anchor Syntax | Dataflow Taint & RTM |
221
- | :--- | :--- | :--- | :---: |
222
- | **TypeScript / JavaScript** | Tree-sitter TypeScript/JS | `// @implements A-SPEC-XXX` | Full Support |
223
- | **Python** | Tree-sitter Python | `# @implements A-SPEC-XXX` | Full Support |
224
- | **Go** | Tree-sitter Go | `// @implements A-SPEC-XXX` | Full Support |
225
- | **Rust** | Tree-sitter Rust | `// @implements A-SPEC-XXX` | Full Support |
226
- | **Java** | Tree-sitter Java | `// @implements A-SPEC-XXX` | Full Support |
227
- | **C / C++** | Tree-sitter C/C++ | `// @implements A-SPEC-XXX` | Full Support |
228
- | **C# (.NET)** | Tree-sitter C# | `// @implements A-SPEC-XXX` | Full Support |
220
+
221
+ <!-- language-matrix:begin -->
222
+ | Language | relations | ast | cfg | ddg | cdg | taint | Corpus evidence |
223
+ | :--- | :---: | :---: | :---: | :---: | :---: | :---: | :--- |
224
+ | **TypeScript / JavaScript** | | | | | | | 10,929 functions (this repository), zero invariant violations |
225
+ | **Python** | | | | | | | 1,209 functions (jarvis), zero invariant violations |
226
+ | **C#** | | | | | | | 9,184 functions (Newtonsoft.Json + RestSharp), zero invariant violations — 4.4% of files refuse at L1 (preprocessor across syntax) |
227
+ | **Java** | | | | | | | 8,376 functions (gson + junit4), zero invariant violations |
228
+ | **Go** | | | | | | | 2,637 functions (gin + cobra), zero invariant violations |
229
+ | **Rust** | ● | ● | ● | ● | ● | ● | 5,247 functions (ripgrep + serde), zero invariant violations |
230
+ | **C++** | ● | ● | ● | ● | ● | ● | 1,762 functions (nlohmann + fmt + leveldb), zero invariant violations — 32.1% of template/macro-heavy files refuse at L1 — layers above speak only for what parses |
231
+
232
+ Every grade is a measurement, not a goal: a cell moves only when a real corpus proves it.
233
+ Full per-cell bases (and every stated limit) live in [docs/language-support.md](docs/language-support.md), which is generated from the same derivation and byte-pinned by the suite — as is this table.
234
+ <!-- language-matrix:end -->
229
235
 
230
236
  ### 🖥️ Supported Operating Systems & Runtimes
231
237
  | OS / Platform | Architecture | Status | Notes |
package/dist/.build-id CHANGED
@@ -1 +1 @@
1
- 814d021-mtjewubg
1
+ 7cb32c4-mtjifuwa
@@ -151,9 +151,28 @@ const RUST_RULES = {
151
151
  ]),
152
152
  block: 'block',
153
153
  };
154
+ // @implements A-SPEC-523.1 — C++, classified from three corpora. Its first fact is the PARSER:
155
+ // 25-34% of template/macro-heavy real files refuse at L1 (measured on nlohmann/fmt/leveldb) —
156
+ // the matrix's ast basis discloses this. What parses rides the TS chassis (else_clause wrapper,
157
+ // C fallthrough switch, same try-clause names). Labels and goto refuse (zero corpus occurrences).
158
+ const CPP_SIMPLE = new Set([
159
+ 'expression_statement', 'declaration', 'alias_declaration', 'static_assert_declaration',
160
+ 'enum_specifier', 'using_declaration', 'type_definition', 'struct_specifier',
161
+ 'preproc_call', 'preproc_def', 'preproc_function_def', 'preproc_include',
162
+ ]);
163
+ const CPP_RULES = {
164
+ simple: CPP_SIMPLE,
165
+ handled: new Set([
166
+ ...CPP_SIMPLE,
167
+ 'compound_statement', 'if_statement', 'while_statement', 'do_statement', 'for_statement',
168
+ 'for_range_loop', 'switch_statement', 'try_statement', 'throw_statement',
169
+ 'break_statement', 'continue_statement', 'return_statement', 'preproc_if', 'preproc_ifdef',
170
+ ]),
171
+ block: 'compound_statement',
172
+ };
154
173
  const LANG_RULES = {
155
174
  typescript: TS_RULES, tsx: TS_RULES, python: PY_RULES, go: GO_RULES, java: JAVA_RULES,
156
- csharp: CSHARP_RULES, rust: RUST_RULES,
175
+ csharp: CSHARP_RULES, rust: RUST_RULES, cpp: CPP_RULES,
157
176
  };
158
177
  // @implements A-SPEC-510.6
159
178
  /**
@@ -301,6 +320,7 @@ function cfgOf(ast, fn, source) {
301
320
  case 'statement_block':
302
321
  case 'block':
303
322
  case 'statement_list':
323
+ case 'compound_statement':
304
324
  // Go's block carries one extra statement_list layer (probed shape) — both are containers.
305
325
  return lowerSeq(stmtChildren(i), ctx);
306
326
  case 'labeled_statement': {
@@ -531,9 +551,10 @@ function cfgOf(ast, fn, source) {
531
551
  // @implements A-SPEC-520.1 — Java's enhanced for: the header owns the binding; body
532
552
  // loops back; exhaustion exits false — same skeleton as Go's range.
533
553
  case 'enhanced_for_statement':
534
- case 'foreach_statement': {
554
+ case 'foreach_statement':
555
+ case 'for_range_loop': {
535
556
  const header = newPoint(i);
536
- const bodyIdx = kids.of(i).find((k) => ast.nodes[k].type === 'block')
557
+ const bodyIdx = kids.of(i).find((k) => ast.nodes[k].type === rules.block)
537
558
  ?? stmtChildren(i).at(-1);
538
559
  const myBreak = { label, target: -1 };
539
560
  ctx.breakT.push({ label, owner: myBreak }, { label: null, owner: myBreak });
@@ -565,7 +586,8 @@ function cfgOf(ast, fn, source) {
565
586
  // @implements A-SPEC-521.1 — the preprocessor puts ALTERNATIVES in statement position.
566
587
  // Sequencing the arms would fabricate flow no build configuration ever runs; each arm is
567
588
  // a branch and both rejoin after #endif (a may-analysis over every configuration).
568
- case 'preproc_if': {
589
+ case 'preproc_if':
590
+ case 'preproc_ifdef': {
569
591
  const p = newPoint(i);
570
592
  const named = stmtChildren(i);
571
593
  const elseArm = named.find((k) => ['preproc_else', 'preproc_elif'].includes(ast.nodes[k].type));
@@ -840,6 +862,37 @@ function cfgOf(ast, fn, source) {
840
862
  exits.push({ from: b.point, kind: 'break' });
841
863
  return { entry: p, exits };
842
864
  }
865
+ // @implements A-SPEC-523.1 — C++: flat case_statement children inside a
866
+ // compound_statement, with C's implicit fallthrough (a value-less case is default).
867
+ if (ast.lang === 'cpp') {
868
+ const p = newPoint(i);
869
+ const myBreak = { label, target: -1 };
870
+ ctx.breakT.push({ label, owner: myBreak }, { label: null, owner: myBreak });
871
+ const swBody = kids.of(i).find((k) => ast.nodes[k].type === 'compound_statement');
872
+ let sawDefault = false;
873
+ let prevFall = [];
874
+ const exits = [];
875
+ for (const c of swBody === undefined ? [] : kids.of(swBody)) {
876
+ if (ast.nodes[c].type !== 'case_statement')
877
+ continue;
878
+ const children = stmtChildren(c);
879
+ const stmts = children.filter((k) => rules.handled.has(ast.nodes[k].type));
880
+ if (stmts.length === children.length)
881
+ sawDefault = true; // no label value → default
882
+ const cf = lowerSeq(stmts, ctx);
883
+ link(p, cf.entry, stmts.length === children.length ? 'default' : 'case');
884
+ for (const e of prevFall)
885
+ link(e.from, cf.entry, 'seq'); // C fallthrough
886
+ prevFall = cf.exits;
887
+ }
888
+ exits.push(...prevFall);
889
+ if (!sawDefault)
890
+ exits.push({ from: p, kind: 'default' });
891
+ ctx.breakT.splice(-2);
892
+ for (const b of claimBreaks(myBreak))
893
+ exits.push({ from: b.point, kind: 'break' });
894
+ return { entry: p, exits };
895
+ }
843
896
  const p = newPoint(i);
844
897
  const body = stmtChildren(i).at(-1); // switch_body
845
898
  const myBreak = { label, target: -1 };
@@ -908,7 +961,7 @@ function cfgOf(ast, fn, source) {
908
961
  // @implements A-SPEC-520.1 — Java spells the try body `block`; TS `statement_block`.
909
962
  const body = isPython
910
963
  ? kids.of(i).find((k) => ast.nodes[k].type === 'block')
911
- : named.find((k) => ['statement_block', 'block'].includes(ast.nodes[k].type));
964
+ : named.find((k) => ['statement_block', 'block', 'compound_statement'].includes(ast.nodes[k].type));
912
965
  const handler = isPython
913
966
  ? kids.of(i).find((k) => ast.nodes[k].type === 'except_clause')
914
967
  : named.find((k) => ast.nodes[k].type === 'catch_clause');
@@ -97,6 +97,65 @@ function defUseOf(ast, cfg, fn, source) {
97
97
  if (ty === 'field_identifier' || ty === 'package_identifier' || ty === 'label_name')
98
98
  return;
99
99
  switch (ty) {
100
+ // @implements A-SPEC-523.1 — C++'s binding shapes.
101
+ case 'init_declarator': {
102
+ // `int y = g()` / `T& r = s` — the declarator's identifier (through reference/pointer
103
+ // wrappers) is the def; the initializer is a use.
104
+ const named = kids.of(n).filter((c) => ast.nodes[c].named);
105
+ const firstId = (m) => {
106
+ if (ast.nodes[m].type === 'identifier')
107
+ return m;
108
+ for (const c of kids.of(m)) {
109
+ const r = firstId(c);
110
+ if (r !== undefined)
111
+ return r;
112
+ }
113
+ return undefined;
114
+ };
115
+ if (named[0] !== undefined) {
116
+ const id = firstId(named[0]);
117
+ if (id !== undefined)
118
+ add(defs, stmt, text(id));
119
+ }
120
+ for (const r of named.slice(1))
121
+ walk(r, false);
122
+ return;
123
+ }
124
+ case 'for_range_loop': {
125
+ // `for (auto& s : xs)` — the declarator's identifier binds; the range is a use.
126
+ const named = kids.of(n).filter((c) => ast.nodes[c].named);
127
+ const body = named.find((c) => ast.nodes[c].type === 'compound_statement');
128
+ for (const c of named) {
129
+ if (c === body)
130
+ continue;
131
+ const ty = ast.nodes[c].type;
132
+ if (ty.endsWith('_declarator') || ty === 'identifier') {
133
+ const firstId = (m) => {
134
+ if (ast.nodes[m].type === 'identifier')
135
+ return m;
136
+ for (const k of kids.of(m)) {
137
+ const r = firstId(k);
138
+ if (r !== undefined)
139
+ return r;
140
+ }
141
+ return undefined;
142
+ };
143
+ // the LAST such child before the body is the RANGE when it is a bare identifier —
144
+ // walk order below fixes that: declarator-shaped nodes def, the final expr uses.
145
+ if (ty === 'identifier' && c === named.filter((x) => x !== body).at(-1)) {
146
+ walk(c, false);
147
+ continue;
148
+ }
149
+ const id = firstId(c);
150
+ if (id !== undefined)
151
+ add(defs, stmt, text(id));
152
+ }
153
+ else if (!ty.includes('type') && ty !== 'placeholder_type_specifier') {
154
+ walk(c, false);
155
+ }
156
+ }
157
+ return;
158
+ }
100
159
  // @implements A-SPEC-522.1 — Rust's binding shapes. Pattern identifiers bind by the
101
160
  // LOWERCASE-INITIAL convention (rustc warns on violations): `Some(x)` binds x, skips the
102
161
  // constructor Some — a documented heuristic, not a guess about arbitrary code.
@@ -461,11 +520,24 @@ function defUseOf(ast, cfg, fn, source) {
461
520
  // @implements A-SPEC-520.1 — Java wraps the parameter in catch_formal_parameter; TS puts the
462
521
  // identifier directly under the clause. One lookup serves both.
463
522
  const formal = named.find((k) => ast.nodes[k].type === 'catch_formal_parameter');
523
+ // @implements A-SPEC-523.1 — C++ wraps it deeper still: parameter_list > parameter_declaration.
524
+ const plist = named.find((k) => ast.nodes[k].type === 'parameter_list');
525
+ const firstIdIn = (m) => {
526
+ if (ast.nodes[m].type === 'identifier')
527
+ return m;
528
+ for (const c of kids.of(m)) {
529
+ const r = firstIdIn(c);
530
+ if (r !== undefined)
531
+ return r;
532
+ }
533
+ return undefined;
534
+ };
464
535
  const param = named.find((k) => ast.nodes[k].type === 'identifier')
465
536
  ?? (formal !== undefined
466
537
  ? kids.of(formal).find((k) => ast.nodes[k].type === 'identifier')
467
- : undefined);
468
- const block = named.find((k) => ['statement_block', 'block'].includes(ast.nodes[k].type));
538
+ : undefined)
539
+ ?? (plist !== undefined ? firstIdIn(plist) : undefined);
540
+ const block = named.find((k) => ['statement_block', 'block', 'compound_statement'].includes(ast.nodes[k].type));
469
541
  if (param === undefined || block === undefined)
470
542
  return;
471
543
  const first = kids.of(block).find((k) => stmtSet.has(k));
@@ -55,3 +55,20 @@ export type LanguageMatrix = Record<Layer, Record<string, Cell>>;
55
55
  export declare function languageMatrix(): LanguageMatrix;
56
56
  /** Render docs/language-support.md — the table is a PRODUCT of the code, not a claim beside it. */
57
57
  export declare function renderLanguageSupport(): string;
58
+ /**
59
+ * The corpus evidence behind each language's grades — TRANSCRIBED from the activation log's
60
+ * measurements (sources: S-510 for ts/py, S-519.1..S-523.1 for the five), so the README can cite
61
+ * numbers without re-running corpora at test time. If a re-run moves a number, this table moves
62
+ * with it in the same commit — the pin in the test makes forgetting that a red suite.
63
+ */
64
+ export declare const CORPUS_EVIDENCE: Record<MatrixLanguage, {
65
+ fns: string;
66
+ corpus: string;
67
+ note?: string;
68
+ }>;
69
+ /**
70
+ * The README's language table — a RENDER, like docs/language-support.md, byte-pinned by a test.
71
+ * Glyphs are derived from the live matrix; the evidence column is CORPUS_EVIDENCE. No slogans:
72
+ * a grade appears next to the measurement that earned it.
73
+ */
74
+ export declare function renderReadmeLanguageTable(): string;
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MATRIX_LANGUAGES = exports.LAYERS = void 0;
3
+ exports.CORPUS_EVIDENCE = exports.MATRIX_LANGUAGES = exports.LAYERS = void 0;
4
4
  exports.languageMatrix = languageMatrix;
5
5
  exports.renderLanguageSupport = renderLanguageSupport;
6
+ exports.renderReadmeLanguageTable = renderReadmeLanguageTable;
6
7
  // @implements A-SPEC-510.6
7
8
  /**
8
9
  * The layer × language support matrix — what "officially supported" MEANS, made checkable.
@@ -57,9 +58,15 @@ function relationsCell(ext) {
57
58
  function astCell(astLang) {
58
59
  // The TS grammar family covers tsx/js too — AST_LANGUAGES is the authority on which wasm applies.
59
60
  const covered = AST_LANG_SET.has(astLang) || (astLang === 'typescript' && AST_LANG_SET.has('tsx'));
60
- return covered
61
- ? { support: 'full', basis: 'vendored tree-sitter grammar (L1 AST persisted)' }
62
- : { support: 'none', basis: 'no vendored grammar for the L1 substrate' };
61
+ if (!covered)
62
+ return { support: 'none', basis: 'no vendored grammar for the L1 substrate' };
63
+ // @implements A-SPEC-523.1 the C++ cell carries its measured limit IN the table: 25-34% of
64
+ // template/macro-heavy real files refuse at L1 (nlohmann/fmt/leveldb), and every layer above
65
+ // speaks only about what L1 accepts. Same glyph, weaker evidence — the reader sees it here.
66
+ if (astLang === 'cpp') {
67
+ return { support: 'full', basis: 'vendored tree-sitter grammar (L1 AST persisted) — limit: template/macro-heavy real files can refuse at L1 (measured 25-34% on three corpora); layers above speak only for what parses' };
68
+ }
69
+ return { support: 'full', basis: 'vendored tree-sitter grammar (L1 AST persisted)' };
63
70
  }
64
71
  function cfgFamilyCell(astLang, layer) {
65
72
  const covered = cfg_1.CFG_LANGUAGES.has(astLang) || (astLang === 'typescript' && cfg_1.CFG_LANGUAGES.has('tsx'));
@@ -147,3 +154,44 @@ function renderLanguageSupport() {
147
154
  '',
148
155
  ].join('\n');
149
156
  }
157
+ // @implements A-SPEC-526.1
158
+ /**
159
+ * The corpus evidence behind each language's grades — TRANSCRIBED from the activation log's
160
+ * measurements (sources: S-510 for ts/py, S-519.1..S-523.1 for the five), so the README can cite
161
+ * numbers without re-running corpora at test time. If a re-run moves a number, this table moves
162
+ * with it in the same commit — the pin in the test makes forgetting that a red suite.
163
+ */
164
+ exports.CORPUS_EVIDENCE = {
165
+ typescript: { fns: '10,929', corpus: 'this repository' },
166
+ python: { fns: '1,209', corpus: 'jarvis' },
167
+ csharp: { fns: '9,184', corpus: 'Newtonsoft.Json + RestSharp', note: '4.4% of files refuse at L1 (preprocessor across syntax)' },
168
+ java: { fns: '8,376', corpus: 'gson + junit4' },
169
+ go: { fns: '2,637', corpus: 'gin + cobra' },
170
+ rust: { fns: '5,247', corpus: 'ripgrep + serde' },
171
+ cpp: { fns: '1,762', corpus: 'nlohmann + fmt + leveldb', note: '32.1% of template/macro-heavy files refuse at L1 — layers above speak only for what parses' },
172
+ };
173
+ // @implements A-SPEC-526.1
174
+ /**
175
+ * The README's language table — a RENDER, like docs/language-support.md, byte-pinned by a test.
176
+ * Glyphs are derived from the live matrix; the evidence column is CORPUS_EVIDENCE. No slogans:
177
+ * a grade appears next to the measurement that earned it.
178
+ */
179
+ function renderReadmeLanguageTable() {
180
+ const m = languageMatrix();
181
+ const glyph = (s) => (s === 'full' ? '●' : s === 'partial' ? '◐' : '○');
182
+ const lines = [];
183
+ lines.push('| Language | relations | ast | cfg | ddg | cdg | taint | Corpus evidence |');
184
+ lines.push('| :--- | :---: | :---: | :---: | :---: | :---: | :---: | :--- |');
185
+ for (const { lang, label } of exports.MATRIX_LANGUAGES) {
186
+ const ev = exports.CORPUS_EVIDENCE[lang];
187
+ const cells = ['relations', 'ast', 'cfg', 'ddg', 'cdg', 'taint']
188
+ .map((layer) => glyph(m[layer][lang].support)).join(' | ');
189
+ const note = ev.note ? ` — ${ev.note}` : '';
190
+ lines.push(`| **${label}** | ${cells} | ${ev.fns} functions (${ev.corpus}), zero invariant violations${note} |`);
191
+ }
192
+ lines.push('');
193
+ lines.push('Every grade is a measurement, not a goal: a cell moves only when a real corpus proves it.');
194
+ lines.push('Full per-cell bases (and every stated limit) live in [docs/language-support.md](docs/language-support.md), which is generated from the same derivation and byte-pinned by the suite — as is this table.');
195
+ lines.push('');
196
+ return lines.join('\n');
197
+ }
@@ -84,6 +84,17 @@ const CALLS_ONLY = { symbols: true, relations: ['calls'], graphResolved: ['calls
84
84
  const CALLS_AND_INHERITS = {
85
85
  symbols: true, relations: ['calls', 'inherits'], graphResolved: ['calls', 'inherits'],
86
86
  };
87
+ // @implements A-SPEC-525.1
88
+ /**
89
+ * All three relations, extracted AND graph-resolved. The five non-TS/Python languages moved here
90
+ * when their imports were measured ARRIVING (A-SPEC-286's column): go 389, rust 64, java 2,688,
91
+ * csharp 316, cpp 482 resolved edges over the ten corpora. Rates differ by language semantics —
92
+ * C# resolves only where namespace happens to mirror the folder layout, Rust skips use-trees as
93
+ * ambiguous — and the unresolved remainder is external/ambiguous by design, never guessed.
94
+ */
95
+ const FULL_RELATIONS = {
96
+ symbols: true, relations: ['calls', 'imports', 'inherits'], graphResolved: ['calls', 'imports', 'inherits'],
97
+ };
87
98
  exports.LANGUAGE_CAPABILITY = {
88
99
  '.ts': TS_FAMILY, '.mts': TS_FAMILY, '.cts': TS_FAMILY, '.tsx': TS_FAMILY,
89
100
  '.js': TS_FAMILY, '.mjs': TS_FAMILY, '.jsx': TS_FAMILY,
@@ -93,12 +104,13 @@ exports.LANGUAGE_CAPABILITY = {
93
104
  '.py': { symbols: true, relations: ['calls', 'imports', 'inherits'], graphResolved: ['calls', 'imports', 'inherits'] },
94
105
  // @implements A-SPEC-511.1 — inheritance recovered: Java extends/implements, C# base_list,
95
106
  // Go struct embedding (interface satisfaction is a declared permanent gap), Rust `impl T for S`.
96
- '.java': CALLS_AND_INHERITS, '.cs': CALLS_AND_INHERITS, '.go': CALLS_AND_INHERITS, '.rs': CALLS_AND_INHERITS,
107
+ // @implements A-SPEC-525.1 imports measured arriving for all five (see FULL_RELATIONS).
108
+ '.java': FULL_RELATIONS, '.cs': FULL_RELATIONS, '.go': FULL_RELATIONS, '.rs': FULL_RELATIONS,
97
109
  // @implements A-SPEC-287 — C++ used to extract calls that never became graph edges, because its
98
110
  // edge scope omitted the class node the symbol walk uses. Fixed; it now resolves like the others.
99
111
  // @implements A-SPEC-511.1 — base_class_clause recovered (access specifiers skipped).
100
- '.cpp': CALLS_AND_INHERITS, '.cc': CALLS_AND_INHERITS, '.cxx': CALLS_AND_INHERITS,
101
- '.hpp': CALLS_AND_INHERITS, '.hh': CALLS_AND_INHERITS, '.h': CALLS_AND_INHERITS,
112
+ '.cpp': FULL_RELATIONS, '.cc': FULL_RELATIONS, '.cxx': FULL_RELATIONS,
113
+ '.hpp': FULL_RELATIONS, '.hh': FULL_RELATIONS, '.h': FULL_RELATIONS,
102
114
  };
103
115
  function capabilityFor(ext) {
104
116
  return exports.LANGUAGE_CAPABILITY[ext.toLowerCase()];
@@ -538,7 +538,9 @@ const EDGE_CONFIG = {
538
538
  // syntactic fact. Interface satisfaction is implicit and structural (no `implements` keyword), so
539
539
  // recovering it needs a type checker — a DECLARED PERMANENT LIMIT, never a guess.
540
540
  go: { callTypes: ['call_expression'], calleeField: 'function', scopeTypes: ['function_declaration', 'method_declaration'],
541
- inherits: { declTypes: ['type_spec'], mode: 'go-embedding' } },
541
+ inherits: { declTypes: ['type_spec'], mode: 'go-embedding' },
542
+ // @implements A-SPEC-525.1 — import_spec's interpreted_string_literal, quotes stripped.
543
+ imports: { nodeType: 'import_spec', mode: 'go-string' } },
542
544
  // @implements A-SPEC-300
543
545
  // `impl_item`/`trait_item` must open a qualifying scope, exactly as the symbol walk does: a method
544
546
  // in `impl Greet for En` is the symbol `En.hello`, but its edges came out qualified as bare
@@ -547,18 +549,27 @@ const EDGE_CONFIG = {
547
549
  // @implements A-SPEC-511.1 — `impl Trait for Type` ⇒ Type inherits Trait (implementer → abstract).
548
550
  // An inherent `impl Type { … }` carries no trait field and is NOT inheritance: no edge.
549
551
  rust: { callTypes: ['call_expression'], calleeField: 'function', scopeTypes: ['function_item', 'impl_item', 'trait_item'],
550
- inherits: { declTypes: ['impl_item'], mode: 'rust-impl' } },
552
+ inherits: { declTypes: ['impl_item'], mode: 'rust-impl' },
553
+ // @implements A-SPEC-525.1 — use_declaration's scoped path; use-trees (braces) are
554
+ // ambiguous fan-outs and are skipped rather than half-read.
555
+ imports: { nodeType: 'use_declaration', mode: 'rust-use' } },
551
556
  // @implements A-SPEC-511.1 — Java separates the two syntactically, and BOTH are inheritance edges:
552
557
  // `superclass` (extends) and `super_interfaces` (implements), plus an interface's own extends.
553
558
  java: { callTypes: ['method_invocation'], calleeField: 'name', scopeTypes: ['class_declaration', 'method_declaration'],
554
559
  inherits: { declTypes: ['class_declaration', 'interface_declaration', 'record_declaration', 'enum_declaration'],
555
- fields: ['superclass', 'super_interfaces', 'interfaces', 'extends_interfaces'] } },
560
+ fields: ['superclass', 'super_interfaces', 'interfaces', 'extends_interfaces'] },
561
+ // @implements A-SPEC-525.1 — import_declaration's scoped_identifier; a wildcard
562
+ // (asterisk sibling) is ambiguous and skipped.
563
+ imports: { nodeType: 'import_declaration', mode: 'java-scoped' } },
556
564
  // @implements A-SPEC-511.1 — C# puts the base class AND the interfaces in ONE `base_list`, with no
557
565
  // syntactic marker telling them apart. Splitting them would be a guess, so BOTH become `inherits`
558
566
  // (a deliberate asymmetry with the TS walk, which emits extends only — H-SPEC-511 decision 1).
559
567
  csharp: { callTypes: ['invocation_expression'], calleeField: 'function', scopeTypes: ['class_declaration', 'method_declaration'],
560
568
  inherits: { declTypes: ['class_declaration', 'interface_declaration', 'struct_declaration', 'record_declaration'],
561
- childTypes: ['base_list'] } },
569
+ childTypes: ['base_list'] },
570
+ // @implements A-SPEC-525.1 — using_directive's qualified_name (a NAMESPACE, which the
571
+ // resolver may fail to map to a file — that failure is measured, not hidden).
572
+ imports: { nodeType: 'using_directive', mode: 'csharp-using' } },
562
573
  // @implements A-SPEC-287
563
574
  // C++ listed only `function_definition`, so a member function's edges came out qualified as bare
564
575
  // `run` while the symbol walk (which treats class_specifier/struct_specifier as scopes) emitted
@@ -568,7 +579,9 @@ const EDGE_CONFIG = {
568
579
  // @implements A-SPEC-511.1 — C++ bases live in `base_class_clause`; access specifiers
569
580
  // (public/private/protected/virtual) are skipped and only the type name is taken.
570
581
  cpp: { callTypes: ['call_expression'], calleeField: 'function', scopeTypes: ['class_specifier', 'struct_specifier', 'function_definition'],
571
- inherits: { declTypes: ['class_specifier', 'struct_specifier'], childTypes: ['base_class_clause'] } },
582
+ inherits: { declTypes: ['class_specifier', 'struct_specifier'], childTypes: ['base_class_clause'] },
583
+ // @implements A-SPEC-525.1 — quoted includes only; <system> headers are external.
584
+ imports: { nodeType: 'preproc_include', mode: 'cpp-include' } },
572
585
  };
573
586
  // The callee's bare name: a plain identifier is itself; a member/selector/scoped/field node
574
587
  // (`o.m`, `self.c`, `mod::f`, `this.C`) unwraps to its LAST identifier segment — the method name.
@@ -764,6 +777,49 @@ function walkEdges(tree, cfg) {
764
777
  }
765
778
  }
766
779
  };
780
+ // @implements A-SPEC-525.1 — imports, table-driven like everything else in this walk. Each
781
+ // mode reads ONE probed shape; anything ambiguous (use-trees, wildcards, system headers) emits
782
+ // nothing rather than half of something.
783
+ const imp = cfg.imports;
784
+ const emitImport = (node) => {
785
+ if (imp.mode === 'go-string') {
786
+ const lit = node.namedChildren.find((c) => c.type === 'interpreted_string_literal');
787
+ if (lit)
788
+ out.push({ from: '<module>', to: lit.text.replace(/["']/g, ''), rel: 'imports' });
789
+ return;
790
+ }
791
+ if (imp.mode === 'rust-use') {
792
+ const arg = node.namedChildren.find((c) => ['scoped_identifier', 'identifier', 'use_as_clause', 'scoped_use_list', 'use_wildcard'].includes(c.type));
793
+ if (!arg)
794
+ return;
795
+ if (arg.type === 'scoped_use_list' || arg.type === 'use_wildcard')
796
+ return; // ambiguous fan-out
797
+ const pathNode = arg.type === 'use_as_clause' ? arg.namedChildren[0] : arg;
798
+ if (pathNode)
799
+ out.push({ from: '<module>', to: pathNode.text, rel: 'imports' });
800
+ return;
801
+ }
802
+ if (imp.mode === 'java-scoped') {
803
+ if (node.children.some((c) => c.type === 'asterisk'))
804
+ return; // wildcard
805
+ const scoped = node.namedChildren.find((c) => ['scoped_identifier', 'identifier'].includes(c.type));
806
+ if (scoped)
807
+ out.push({ from: '<module>', to: scoped.text, rel: 'imports' });
808
+ return;
809
+ }
810
+ if (imp.mode === 'csharp-using') {
811
+ const q = node.namedChildren.find((c) => ['qualified_name', 'identifier'].includes(c.type));
812
+ if (q)
813
+ out.push({ from: '<module>', to: q.text, rel: 'imports' });
814
+ return;
815
+ }
816
+ if (imp.mode === 'cpp-include') {
817
+ const lit = node.namedChildren.find((c) => c.type === 'string_literal');
818
+ if (lit)
819
+ out.push({ from: '<module>', to: lit.text.replace(/["']/g, ''), rel: 'imports' });
820
+ return; // <system>: external
821
+ }
822
+ };
767
823
  const visit = (node) => {
768
824
  if (callTypes.has(node.type)) {
769
825
  const callee = calleeNameOf(node.childForFieldName(cfg.calleeField));
@@ -772,6 +828,8 @@ function walkEdges(tree, cfg) {
772
828
  }
773
829
  if (inh && declTypes.has(node.type))
774
830
  emitInherits(node);
831
+ if (imp && node.type === imp.nodeType)
832
+ emitImport(node);
775
833
  for (let i = 0; i < node.childCount; i++)
776
834
  visit(node.child(i));
777
835
  };
@@ -1708,13 +1708,21 @@ function makeRawHandlers(store, opts) {
1708
1708
  const { vocabularyFor } = require('../rtm/taint-vocabulary');
1709
1709
  const parser = new TreeSitterTsParser();
1710
1710
  const factSet = [];
1711
- const pythonFiles = [];
1711
+ // @implements A-SPEC-524.1 — every non-TS language with a CFG takes the flow-sensitive
1712
+ // lane, each judged with ITS OWN vocabulary. The TS family keeps the facts+reaching-defs
1713
+ // lane (its interprocedural propagation lives there).
1714
+ const FLOW_EXT = [
1715
+ [/\.py$/i, 'python'], [/\.go$/i, 'go'], [/\.rs$/i, 'rust'],
1716
+ [/\.java$/i, 'java'], [/\.cs$/i, 'csharp'], [/\.(cpp|cc|cxx|hpp|h)$/i, 'cpp'],
1717
+ ];
1718
+ const flowLangFiles = new Map();
1712
1719
  for (const f of scanned) {
1713
1720
  const vocab = vocabularyFor(f.sourcePath);
1714
1721
  if (!vocab)
1715
1722
  continue;
1716
- if (/\.py$/i.test(f.sourcePath)) {
1717
- pythonFiles.push(f.sourcePath);
1723
+ const flowLang = FLOW_EXT.find(([re]) => re.test(f.sourcePath))?.[1];
1724
+ if (flowLang !== undefined) {
1725
+ flowLangFiles.set(flowLang, [...(flowLangFiles.get(flowLang) ?? []), f.sourcePath]);
1718
1726
  continue;
1719
1727
  }
1720
1728
  let src;
@@ -1740,36 +1748,40 @@ function makeRawHandlers(store, opts) {
1740
1748
  kept.push(...out.kept.map((f) => ({ ...f, lane: 'facts+reaching-defs' })));
1741
1749
  refuted += out.removed.length;
1742
1750
  }
1743
- // @implements A-SPEC-513.1 Python takes the flow-sensitive lane directly: its facts
1744
- // extractor has no Python walk, but its CFG/DDG do (S-510.7), and the judgement above
1745
- // them is language-agnostic.
1746
- const pyVocab = vocabularyFor('x.py');
1747
- if (pythonFiles.length > 0) {
1751
+ // @implements A-SPEC-513.1 / A-SPEC-524.1 the flow-sensitive lane, generalized from
1752
+ // Python-only to every language whose CFG/DDG landed in P2. The judgement engine is
1753
+ // language-agnostic; only the vocabulary is selected per language.
1754
+ const flowLangsAnalysed = [];
1755
+ if (flowLangFiles.size > 0) {
1748
1756
  const { parseAst } = require('../cpg/foundation/ast-store');
1749
1757
  const { cfgOf, functionsIn } = require('../cpg/foundation/cfg');
1750
1758
  const { ddgOf } = require('../cpg/foundation/ddg');
1751
1759
  const { flowSensitiveTaint } = require('../rtm/flow-sensitive-taint');
1752
- for (const rel of pythonFiles) {
1753
- let src;
1754
- try {
1755
- src = fs.readFileSync(path.join(root, rel), 'utf8');
1756
- }
1757
- catch {
1758
- continue;
1759
- }
1760
- const ast = await parseAst(src, rel);
1761
- if (!ast || ast.errorCount > 0)
1762
- continue;
1763
- for (const fn of functionsIn(ast)) {
1764
- const fcfg = cfgOf(ast, fn, src);
1765
- if ('unsupported' in fcfg)
1760
+ for (const [flowLang, files] of flowLangFiles) {
1761
+ flowLangsAnalysed.push(flowLang);
1762
+ for (const rel of files) {
1763
+ let src;
1764
+ try {
1765
+ src = fs.readFileSync(path.join(root, rel), 'utf8');
1766
+ }
1767
+ catch {
1766
1768
  continue;
1767
- const fddg = ddgOf(ast, fcfg, fn, src);
1768
- if ('unsupported' in fddg)
1769
+ }
1770
+ const vocab = vocabularyFor(rel);
1771
+ const ast = await parseAst(src, rel);
1772
+ if (!ast || ast.errorCount > 0)
1769
1773
  continue;
1770
- const r = flowSensitiveTaint({ ast, cfg: fcfg, ddg: fddg, source: src, fnName: rel, config: pyVocab });
1771
- for (const f of r.findings)
1772
- kept.push({ ...f, file: rel, lane: 'flow-sensitive' });
1774
+ for (const fn of functionsIn(ast)) {
1775
+ const fcfg = cfgOf(ast, fn, src);
1776
+ if ('unsupported' in fcfg)
1777
+ continue;
1778
+ const fddg = ddgOf(ast, fcfg, fn, src);
1779
+ if ('unsupported' in fddg)
1780
+ continue;
1781
+ const r = flowSensitiveTaint({ ast, cfg: fcfg, ddg: fddg, source: src, fnName: rel, config: vocab });
1782
+ for (const f of r.findings)
1783
+ kept.push({ ...f, file: rel, lane: 'flow-sensitive' });
1784
+ }
1773
1785
  }
1774
1786
  }
1775
1787
  }
@@ -1783,7 +1795,7 @@ function makeRawHandlers(store, opts) {
1783
1795
  converged: raw.converged,
1784
1796
  truncated: raw.truncated,
1785
1797
  /** Which languages this run could actually judge, so a zero is readable. */
1786
- languagesAnalysed: [...new Set([factSet.length > 0 ? 'typescript' : null, pythonFiles.length > 0 ? 'python' : null].filter(Boolean))],
1798
+ languagesAnalysed: [...new Set([factSet.length > 0 ? 'typescript' : null, ...flowLangsAnalysed].filter(Boolean))],
1787
1799
  },
1788
1800
  };
1789
1801
  }
@@ -6,7 +6,19 @@ const sink_matching_1 = require("./sink-matching");
6
6
  const lineOf = (source, offset) => source.slice(0, offset).split('\n').length;
7
7
  /** The callee's bare last-segment name for a call node, or null when it is not a plain call. */
8
8
  function calleeNameOf(ast, kids, call, source) {
9
- const fnNode = kids.of(call).find((k) => ast.nodes[k].named);
9
+ const named = kids.of(call).filter((k) => ast.nodes[k].named);
10
+ // @implements A-SPEC-524.1 — Java's method_invocation is [receiver?, NAME, argument_list]:
11
+ // the first named child is the RECEIVER when present, so taking it named `runtime` where the
12
+ // sink is `exec` — measured before sealing. The name is the identifier just before the
13
+ // argument list.
14
+ let fnNode;
15
+ if (ast.nodes[call].type === 'method_invocation') {
16
+ const argsAt = named.findIndex((k) => ast.nodes[k].type === 'argument_list');
17
+ fnNode = argsAt > 0 ? named[argsAt - 1] : named[0];
18
+ }
19
+ else {
20
+ fnNode = named[0];
21
+ }
10
22
  if (fnNode === undefined)
11
23
  return null;
12
24
  const text = source.slice(ast.nodes[fnNode].start, ast.nodes[fnNode].end);
@@ -31,7 +43,11 @@ function flowSensitiveTaint(input) {
31
43
  const ty = ast.nodes[n].type;
32
44
  if (n !== stmt && stmtOfBlock.has(n))
33
45
  return;
34
- if (ty === 'call_expression' || ty === 'call')
46
+ // @implements A-SPEC-524.1 the call-node set, measured per grammar: Java spells calls
47
+ // method_invocation and constructors object_creation_expression (where ProcessBuilder
48
+ // lives), C# spells them invocation_expression.
49
+ if (ty === 'call_expression' || ty === 'call' || ty === 'method_invocation'
50
+ || ty === 'invocation_expression' || ty === 'object_creation_expression')
35
51
  out.push(n);
36
52
  for (const c of kids.of(n))
37
53
  walk(c);
@@ -29,6 +29,12 @@ export interface ResolutionReport {
29
29
  export interface BuildRtmOptions {
30
30
  /** Resolve a spec id to its source file path (e.g. its .md file) for provenance tagging. */
31
31
  specSourcePath?: (id: string) => string | undefined;
32
+ /**
33
+ * @implements A-SPEC-525.1 — the Go module path (go.mod's `module` line). Supplied by the
34
+ * caller that can read go.mod; without it every Go import is external and resolves to nothing,
35
+ * which is the honest default (no prefix to strip, no guess to make).
36
+ */
37
+ goModule?: string;
32
38
  /**
33
39
  * @implements A-SPEC-281
34
40
  * The observation context every fact in this build shares: which commit it was read at, which
@@ -170,20 +176,6 @@ export interface CommitRecord {
170
176
  * scanned are still recorded as nodes — they happened.
171
177
  */
172
178
  export declare function addCommitHistory(history: readonly CommitRecord[], scanned: readonly ScannedFile[], graph: RtmGraph, opts?: BuildRtmOptions): void;
173
- /**
174
- * @implements A-SPEC-289
175
- * FILE nodes and resolved `imports` edges.
176
- *
177
- * Import relations were extracted and then dropped, because their endpoints are a file and a module
178
- * path while the graph knew only symbols and specs — measured on this repository, 1,892 extracted
179
- * and 0 in the graph. `File` is a node kind Goal Phase 3 names, so it becomes one.
180
- *
181
- * ONLY relative specifiers are resolved, and only to a file the scan actually contains. A bare
182
- * specifier (`node:fs`, `js-yaml`) is an external package with no node to point at, and inventing
183
- * one would be a guess — the same precision-over-recall rule call resolution follows. Measured: 441
184
- * of 1,892 specifiers (23%) resolve inside the repository, and the rest failing to resolve is a
185
- * fact about the imports, not a defect in the resolver.
186
- */
187
179
  export declare function addImportEdges(scanned: readonly ScannedFile[], graph: RtmGraph, opts?: BuildRtmOptions): void;
188
180
  /** @implements A-SPEC-293 — one architecture decision as its record states it. */
189
181
  export interface DecisionRecord {
@@ -43,6 +43,7 @@ exports.addCommitHistory = addCommitHistory;
43
43
  exports.addImportEdges = addImportEdges;
44
44
  exports.addDecisionEdges = addDecisionEdges;
45
45
  const path = __importStar(require("node:path"));
46
+ const fs = __importStar(require("node:fs"));
46
47
  const language_capability_1 = require("../cpg/language-capability");
47
48
  /**
48
49
  * @implements A-SPEC-281
@@ -341,12 +342,117 @@ function addCommitHistory(history, scanned, graph, opts) {
341
342
  * of 1,892 specifiers (23%) resolve inside the repository, and the rest failing to resolve is a
342
343
  * fact about the imports, not a defect in the resolver.
343
344
  */
345
+ function fsReadGoMod(p) {
346
+ try {
347
+ return fs.readFileSync(p, 'utf8').match(/^module\s+(\S+)/m)?.[1] ?? null;
348
+ }
349
+ catch {
350
+ return null;
351
+ }
352
+ }
344
353
  function addImportEdges(scanned, graph, opts) {
345
354
  const known = new Set(scanned.map((f) => f.sourcePath));
346
355
  const EXTENSIONS = ['', '.ts', '.tsx', '.mts', '.cts', '.js', '.mjs', '.cjs', '.jsx', '.py',
347
356
  '/index.ts', '/index.js', '/__init__.py'];
348
357
  const firstKnown = (base) => EXTENSIONS.map((ext) => `${base}${ext}`).find((candidate) => known.has(candidate)) ?? null;
358
+ // @implements A-SPEC-525.1 — the five languages' resolution rules, selected by the IMPORTING
359
+ // file's extension because resolution IS per-language semantics. The founding rule is unchanged
360
+ // everywhere: resolve only to files the scan actually contains; the ambiguous resolve to none.
361
+ const knownList = [...known];
362
+ const uniqueSuffix = (suffix) => {
363
+ const hits = knownList.filter((k) => k === suffix || k.endsWith(`/${suffix}`));
364
+ return hits.length === 1 ? hits[0] : null; // two files may not become a guess
365
+ };
366
+ // go.mod discovery: injected via opts when the caller knows it; otherwise walked up from the
367
+ // first Go file's ABSOLUTE path (bounded), so production builds resolve without extra wiring.
368
+ let goModCache;
369
+ const goModuleOf = () => {
370
+ if (opts?.goModule)
371
+ return opts.goModule;
372
+ if (goModCache !== undefined)
373
+ return goModCache ?? undefined;
374
+ goModCache = null;
375
+ const anyGo = scanned.find((f) => /\.go$/.test(f.sourcePath) && f.path);
376
+ if (anyGo) {
377
+ let dir = path.dirname(anyGo.path);
378
+ for (let hops = 0; hops < 12; hops++) {
379
+ try {
380
+ const txt = fsReadGoMod(path.join(dir, 'go.mod'));
381
+ if (txt !== null) {
382
+ goModCache = txt;
383
+ break;
384
+ }
385
+ }
386
+ catch { /* keep walking */ }
387
+ const up = path.dirname(dir);
388
+ if (up === dir)
389
+ break;
390
+ dir = up;
391
+ }
392
+ }
393
+ return goModCache ?? undefined;
394
+ };
395
+ const goPackage = (spec) => {
396
+ const mod = goModuleOf();
397
+ if (!mod || !(spec === mod || spec.startsWith(`${mod}/`)))
398
+ return []; // external, honestly
399
+ const dir = spec === mod ? '' : spec.slice(mod.length + 1);
400
+ const prefix = dir === '' ? '' : `${dir}/`;
401
+ return knownList.filter((k) => k.startsWith(prefix) && k.endsWith('.go')
402
+ && !k.endsWith('_test.go') && !k.slice(prefix.length).includes('/'));
403
+ };
404
+ const rustResolve = (fromFile, spec) => {
405
+ const segs = spec.split('::');
406
+ const head = segs.shift();
407
+ let baseDir;
408
+ if (head === 'crate') {
409
+ // the crate root is the src/ directory nearest above the importing file
410
+ const m = fromFile.match(/^(.*?src)\//);
411
+ baseDir = m ? m[1] : 'src';
412
+ }
413
+ else if (head === 'super') {
414
+ baseDir = path.posix.dirname(path.posix.dirname(fromFile));
415
+ while (segs[0] === 'super') {
416
+ segs.shift();
417
+ baseDir = path.posix.dirname(baseDir);
418
+ }
419
+ }
420
+ else if (head === 'self') {
421
+ baseDir = path.posix.dirname(fromFile);
422
+ }
423
+ else {
424
+ return null; // external crate
425
+ }
426
+ const tryPath = (parts) => {
427
+ if (parts.length === 0)
428
+ return null;
429
+ const base = path.posix.normalize(path.posix.join(baseDir, ...parts));
430
+ if (known.has(`${base}.rs`))
431
+ return `${base}.rs`;
432
+ if (known.has(`${base}/mod.rs`))
433
+ return `${base}/mod.rs`;
434
+ return null;
435
+ };
436
+ // the last segment may be an ITEM, not a module — drop it once and retry
437
+ return tryPath(segs) ?? tryPath(segs.slice(0, -1));
438
+ };
349
439
  const resolve = (fromFile, spec) => {
440
+ // @implements A-SPEC-525.1 — language branches BEFORE the JS/Python-shaped fallthrough.
441
+ if (/\.go$/.test(fromFile))
442
+ return null; // Go fans out separately (a package is its files)
443
+ if (/\.rs$/.test(fromFile))
444
+ return rustResolve(fromFile, spec);
445
+ if (/\.java$/.test(fromFile))
446
+ return uniqueSuffix(`${spec.split('.').join('/')}.java`);
447
+ if (/\.cs$/.test(fromFile))
448
+ return uniqueSuffix(`${spec.split('.').join('/')}.cs`);
449
+ if (/\.(cpp|cc|cxx|hpp|h)$/.test(fromFile)) {
450
+ // extension preserved: the dotted split below would butcher `util/env.h`
451
+ const relative = path.posix.normalize(path.posix.join(path.posix.dirname(fromFile), spec));
452
+ if (known.has(relative))
453
+ return relative;
454
+ return known.has(spec) ? spec : null; // repo-root-relative include
455
+ }
350
456
  // @implements A-SPEC-406
351
457
  // A specifier without a leading dot used to be refused outright, on the ground that it names an
352
458
  // external package. That holds for `node:fs` and `js-yaml`; it does NOT hold for Python, which
@@ -381,6 +487,17 @@ function addImportEdges(scanned, graph, opts) {
381
487
  for (const e of f.edges ?? []) {
382
488
  if (e.rel !== 'imports')
383
489
  continue;
490
+ // @implements A-SPEC-525.1 — Go: importing a package imports ALL its files (that is the
491
+ // language's semantics, not an invention), so one specifier fans out to each scanned
492
+ // file of the package directory.
493
+ if (/\.go$/.test(f.sourcePath)) {
494
+ for (const target of goPackage(e.to)) {
495
+ graph.addNode(`FILE:${f.sourcePath}`, 'FILE', f.sourcePath, fact(opts, f.sourcePath, 'ast-scan', 1));
496
+ graph.addNode(`FILE:${target}`, 'FILE', target, fact(opts, target, 'ast-scan', 1));
497
+ graph.addEdge(`FILE:${f.sourcePath}`, `FILE:${target}`, 'imports', f.sourcePath, fact(opts, `${f.sourcePath} -> ${e.to}`, 'import-resolution', 1));
498
+ }
499
+ continue;
500
+ }
384
501
  const target = resolve(f.sourcePath, e.to);
385
502
  if (!target)
386
503
  continue;
@@ -13,7 +13,7 @@
13
13
  * positives.
14
14
  */
15
15
  import type { DataFlowTaintConfig } from './dataflow-taint';
16
- export declare const TAINT_VOCABULARY: Record<'typescript' | 'python', DataFlowTaintConfig>;
16
+ export declare const TAINT_VOCABULARY: Record<'typescript' | 'python' | 'go' | 'java' | 'csharp' | 'rust' | 'cpp', DataFlowTaintConfig>;
17
17
  /**
18
18
  * The vocabulary this file should be judged with, or null when the file is outside the analysable
19
19
  * set. Null is deliberate: degrading an unknown language to the TypeScript list would produce
@@ -35,9 +35,44 @@ const PYTHON_VOCABULARY = {
35
35
  ],
36
36
  sanitizers: ['quote', 'escape', 'shlex.quote', 're.escape'],
37
37
  };
38
+ // @implements A-SPEC-524.1 — five more rows. Same table discipline: sinks are matched on the
39
+ // callee's LAST segment, sources against expression TEXT, and the vocabularies are SELECTED by
40
+ // file extension, never unioned. Rust's `Command::new` is deliberately absent — its bare last
41
+ // segment is 'new', far too general to be a sink; the EXECUTING methods (spawn/output/status)
42
+ // are the sinks instead (sealed in REQ-524).
43
+ const GO_VOCABULARY = {
44
+ sources: ['os.Getenv', 'os.Environ', 'os.Args', '.FormValue', 'URL.Query', 'PostForm'],
45
+ sinks: ['Command', 'CommandContext', 'StartProcess', 'Exec', 'Query'],
46
+ sanitizers: ['Quote', 'QuoteToASCII', 'EscapeString', 'QueryEscape'],
47
+ };
48
+ const JAVA_VOCABULARY = {
49
+ sources: ['System.getenv', 'System.getProperty', 'getParameter', 'getHeader', 'readLine'],
50
+ sinks: ['exec', 'ProcessBuilder', 'executeQuery', 'executeUpdate', 'eval', 'loadLibrary'],
51
+ sanitizers: ['escapeHtml', 'quoteReplacement', 'encode'],
52
+ };
53
+ const CSHARP_VOCABULARY = {
54
+ sources: ['GetEnvironmentVariable', 'ReadLine', 'QueryString', 'Form'],
55
+ sinks: ['Start', 'ExecuteReader', 'ExecuteNonQuery', 'ExecuteScalar', 'Deserialize'],
56
+ sanitizers: ['HtmlEncode', 'UrlEncode', 'EscapeDataString'],
57
+ };
58
+ const RUST_VOCABULARY = {
59
+ sources: ['env::var', 'env::args', 'read_to_string', 'stdin'],
60
+ sinks: ['spawn', 'output', 'status', 'exec', 'query'],
61
+ sanitizers: ['escape', 'quote'],
62
+ };
63
+ const CPP_VOCABULARY = {
64
+ sources: ['getenv', 'argv', 'cin', 'fgets'],
65
+ sinks: ['system', 'popen', 'exec', 'execl', 'execlp', 'execle', 'execv', 'execvp', 'ShellExecute'],
66
+ sanitizers: ['escape', 'quote'],
67
+ };
38
68
  exports.TAINT_VOCABULARY = {
39
69
  typescript: TYPESCRIPT_VOCABULARY,
40
70
  python: PYTHON_VOCABULARY,
71
+ go: GO_VOCABULARY,
72
+ java: JAVA_VOCABULARY,
73
+ csharp: CSHARP_VOCABULARY,
74
+ rust: RUST_VOCABULARY,
75
+ cpp: CPP_VOCABULARY,
41
76
  };
42
77
  const TS_EXT = /\.(ts|mts|cts|tsx|js|mjs|cjs|jsx)$/i;
43
78
  const PY_EXT = /\.py$/i;
@@ -52,6 +87,17 @@ function vocabularyFor(relPath) {
52
87
  return exports.TAINT_VOCABULARY.typescript;
53
88
  if (PY_EXT.test(relPath))
54
89
  return exports.TAINT_VOCABULARY.python;
90
+ // @implements A-SPEC-524.1 — the five languages whose CFG/DDG landed in P2.
91
+ if (/\.go$/i.test(relPath))
92
+ return exports.TAINT_VOCABULARY.go;
93
+ if (/\.rs$/i.test(relPath))
94
+ return exports.TAINT_VOCABULARY.rust;
95
+ if (/\.java$/i.test(relPath))
96
+ return exports.TAINT_VOCABULARY.java;
97
+ if (/\.cs$/i.test(relPath))
98
+ return exports.TAINT_VOCABULARY.csharp;
99
+ if (/\.(cpp|cc|cxx|hpp|h)$/i.test(relPath))
100
+ return exports.TAINT_VOCABULARY.cpp;
55
101
  return null;
56
102
  }
57
103
  /** Languages this vocabulary table covers — the matrix derives its taint row from this. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "//": "@implements A-SPEC-209",
3
3
  "name": "@holmes-lab/holmes-kit",
4
- "version": "0.6.0",
4
+ "version": "0.7.0",
5
5
  "description": "Holmes-Kit — deterministic Agentic Software Engineering (ASE) harness with causal traceability (spec chain + D-CPG + RTM + phase guardrail)",
6
6
  "main": "dist/holmes/mcp/server.js",
7
7
  "types": "dist/holmes/mcp/server.d.ts",