@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 +30 -0
- package/README.md +15 -9
- package/dist/.build-id +1 -1
- package/dist/holmes/cpg/foundation/cfg.js +58 -5
- package/dist/holmes/cpg/foundation/ddg.js +74 -2
- package/dist/holmes/cpg/foundation/language-matrix.d.ts +17 -0
- package/dist/holmes/cpg/foundation/language-matrix.js +52 -4
- package/dist/holmes/cpg/language-capability.js +15 -3
- package/dist/holmes/cpg/language-parser-walk.js +63 -5
- package/dist/holmes/mcp/handlers.js +40 -28
- package/dist/holmes/rtm/flow-sensitive-taint.js +18 -2
- package/dist/holmes/rtm/rtm-builder.d.ts +6 -14
- package/dist/holmes/rtm/rtm-builder.js +117 -0
- package/dist/holmes/rtm/taint-vocabulary.d.ts +1 -1
- package/dist/holmes/rtm/taint-vocabulary.js +46 -0
- package/package.json +1 -1
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
|
223
|
-
|
|
|
224
|
-
| **
|
|
225
|
-
| **
|
|
226
|
-
| **
|
|
227
|
-
| **
|
|
228
|
-
| **
|
|
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
|
-
|
|
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 ===
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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':
|
|
101
|
-
'.hpp':
|
|
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
|
-
|
|
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
|
-
|
|
1717
|
-
|
|
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
|
|
1744
|
-
//
|
|
1745
|
-
//
|
|
1746
|
-
const
|
|
1747
|
-
if (
|
|
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
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
src
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
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
|
-
|
|
1768
|
-
|
|
1769
|
+
}
|
|
1770
|
+
const vocab = vocabularyFor(rel);
|
|
1771
|
+
const ast = await parseAst(src, rel);
|
|
1772
|
+
if (!ast || ast.errorCount > 0)
|
|
1769
1773
|
continue;
|
|
1770
|
-
const
|
|
1771
|
-
|
|
1772
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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.
|
|
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",
|