@holmes-lab/holmes-kit 0.3.10 → 0.4.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.
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MATRIX_LANGUAGES = exports.LAYERS = void 0;
4
+ exports.languageMatrix = languageMatrix;
5
+ exports.renderLanguageSupport = renderLanguageSupport;
6
+ // @implements A-SPEC-510.6
7
+ /**
8
+ * The layer × language support matrix — what "officially supported" MEANS, made checkable.
9
+ *
10
+ * Every cell is DERIVED from the module that owns the capability, never restated here: a
11
+ * hand-written table is a second truth, and a second truth drifts (this repository has the
12
+ * scars — `wiring presence is not enforcement`, the codex parity gap, the moving baseline pin).
13
+ * So the matrix reads AST_LANGUAGES, CFG_LANGUAGES, LANGUAGE_CAPABILITY, hasDataFlowWalk and
14
+ * ecosystemOf, and `docs/language-support.md` is a RENDER of this function, pinned byte-identical
15
+ * by a test. Adding a language or a layer changes exactly one place: the owning module.
16
+ *
17
+ * The `relations` row keeps A-SPEC-286's doctrine intact — a capability claim is made in the
18
+ * GRAPH-RESOLVED column, because everything downstream queries the graph, not the scanner's
19
+ * intermediate output.
20
+ */
21
+ const ast_store_1 = require("./ast-store");
22
+ const cfg_1 = require("./cfg");
23
+ const language_capability_1 = require("../language-capability");
24
+ const language_parser_1 = require("../language-parser");
25
+ const test_runner_1 = require("../../review/test-runner");
26
+ exports.LAYERS = ['relations', 'ast', 'cfg', 'ddg', 'cdg', 'runner', 'taint'];
27
+ /** The language families holmes-kit advertises, each with the probe inputs the derivation needs. */
28
+ exports.MATRIX_LANGUAGES = [
29
+ { lang: 'typescript', label: 'TypeScript / JavaScript', ext: '.ts', astLang: 'typescript', sampleFile: 'x.test.ts' },
30
+ { lang: 'python', label: 'Python', ext: '.py', astLang: 'python', sampleFile: 'test_x.py' },
31
+ { lang: 'csharp', label: 'C#', ext: '.cs', astLang: 'csharp', sampleFile: 'XTest.cs' },
32
+ { lang: 'java', label: 'Java', ext: '.java', astLang: 'java', sampleFile: 'XTest.java' },
33
+ { lang: 'go', label: 'Go', ext: '.go', astLang: 'go', sampleFile: 'x_test.go' },
34
+ { lang: 'rust', label: 'Rust', ext: '.rs', astLang: 'rust', sampleFile: 'tests/x.rs' },
35
+ { lang: 'cpp', label: 'C++', ext: '.cpp', astLang: 'cpp', sampleFile: 'x_test.cpp' },
36
+ ];
37
+ const AST_LANG_SET = new Set(ast_store_1.AST_LANGUAGES.map((l) => l.lang));
38
+ function relationsCell(ext) {
39
+ const cap = language_capability_1.LANGUAGE_CAPABILITY[ext];
40
+ if (!cap)
41
+ return { support: 'none', basis: 'not in LANGUAGE_CAPABILITY' };
42
+ const resolved = cap.graphResolved;
43
+ if (resolved.length === 0)
44
+ return { support: 'none', basis: 'nothing reaches the graph' };
45
+ // A-SPEC-286: the claim is made in the graph-resolved column, never the scanner's raw output.
46
+ return resolved.length >= 3
47
+ ? { support: 'full', basis: `graph edges: ${resolved.join(', ')}` }
48
+ : { support: 'partial', basis: `graph edges: ${resolved.join(', ')} (no imports/inherits)` };
49
+ }
50
+ function astCell(astLang) {
51
+ // The TS grammar family covers tsx/js too — AST_LANGUAGES is the authority on which wasm applies.
52
+ const covered = AST_LANG_SET.has(astLang) || (astLang === 'typescript' && AST_LANG_SET.has('tsx'));
53
+ return covered
54
+ ? { support: 'full', basis: 'vendored tree-sitter grammar (L1 AST persisted)' }
55
+ : { support: 'none', basis: 'no vendored grammar for the L1 substrate' };
56
+ }
57
+ function cfgFamilyCell(astLang, layer) {
58
+ const covered = cfg_1.CFG_LANGUAGES.has(astLang) || (astLang === 'typescript' && cfg_1.CFG_LANGUAGES.has('tsx'));
59
+ const what = layer === 'cfg' ? 'control flow + dominators'
60
+ : layer === 'ddg' ? 'reaching definitions (REACHING_DEF)'
61
+ : 'control dependence (CDG)';
62
+ return covered
63
+ ? { support: 'full', basis: `${what}, statement granularity` }
64
+ : { support: 'none', basis: `${what} not lowered for this language yet` };
65
+ }
66
+ function runnerCell(sampleFile) {
67
+ const eco = (0, test_runner_1.ecosystemOf)(sampleFile);
68
+ return eco
69
+ ? { support: 'full', basis: `test runner adapter: ${eco}` }
70
+ : { support: 'none', basis: 'no test-runner adapter — execution evidence unavailable' };
71
+ }
72
+ function taintCell(astLang) {
73
+ const has = (0, language_parser_1.hasDataFlowWalk)(astLang);
74
+ return has
75
+ ? { support: 'full', basis: 'data-flow walk present (taint_scan analyses this language)' }
76
+ : { support: 'none', basis: 'no data-flow walk — taint_scan does not analyse this language' };
77
+ }
78
+ // @implements A-SPEC-510.6
79
+ /** Derive the whole matrix. Pure, cheap, and impossible to drift from its sources. */
80
+ function languageMatrix() {
81
+ const out = {};
82
+ for (const layer of exports.LAYERS)
83
+ out[layer] = {};
84
+ for (const { lang, ext, astLang, sampleFile } of exports.MATRIX_LANGUAGES) {
85
+ out.relations[lang] = relationsCell(ext);
86
+ out.ast[lang] = astCell(astLang);
87
+ out.cfg[lang] = cfgFamilyCell(astLang, 'cfg');
88
+ out.ddg[lang] = cfgFamilyCell(astLang, 'ddg');
89
+ out.cdg[lang] = cfgFamilyCell(astLang, 'cdg');
90
+ out.runner[lang] = runnerCell(sampleFile);
91
+ out.taint[lang] = taintCell(astLang);
92
+ }
93
+ return out;
94
+ }
95
+ const LAYER_DOC = {
96
+ relations: 'L0 — symbols and relations that actually become RTM graph edges',
97
+ ast: 'L1 — lossless-span AST, persisted (tree-sitter wasm substrate)',
98
+ cfg: 'L2 — control-flow graph with dominator / post-dominator trees',
99
+ ddg: 'L3 — data dependence (reaching definitions, REACHING_DEF edges)',
100
+ cdg: 'L3 — control dependence (CDG edges); PDG = DDG ∪ CDG',
101
+ runner: 'test_run — executes this language\'s suites for ART-4 execution evidence',
102
+ taint: 'taint_scan — data-flow walk for source→sink analysis',
103
+ };
104
+ const MARK = { full: '●', partial: '◐', none: '○' };
105
+ // @implements A-SPEC-510.6
106
+ /** Render docs/language-support.md — the table is a PRODUCT of the code, not a claim beside it. */
107
+ function renderLanguageSupport() {
108
+ const m = languageMatrix();
109
+ const head = `| Layer | ${exports.MATRIX_LANGUAGES.map((l) => l.label).join(' | ')} |`;
110
+ const sep = `|---|${exports.MATRIX_LANGUAGES.map(() => '---').join('|')}|`;
111
+ const rows = exports.LAYERS.map((layer) => `| **${layer}** | ${exports.MATRIX_LANGUAGES.map((l) => MARK[m[layer][l.lang].support]).join(' | ')} |`);
112
+ const bases = exports.LAYERS.flatMap((layer) => exports.MATRIX_LANGUAGES.map((l) => `- \`${layer}\` / ${l.label}: **${m[layer][l.lang].support}** — ${m[layer][l.lang].basis}`));
113
+ return [
114
+ '<!-- GENERATED by renderLanguageSupport() (A-SPEC-510.6). Do not edit by hand:',
115
+ ' every cell is derived from the module that owns the capability, and a test pins this',
116
+ ' file byte-identical to the render. Regenerate: `node scripts/render-language-support.js`. -->',
117
+ '# Language support matrix',
118
+ '',
119
+ 'Legend: ● full · ◐ partial · ○ none',
120
+ '',
121
+ head,
122
+ sep,
123
+ ...rows,
124
+ '',
125
+ '## What each layer means',
126
+ '',
127
+ ...exports.LAYERS.map((l) => `- **${l}** — ${LAYER_DOC[l]}`),
128
+ '',
129
+ '## Per-cell basis',
130
+ '',
131
+ ...bases,
132
+ '',
133
+ ].join('\n');
134
+ }
@@ -0,0 +1,36 @@
1
+ /** The construct corpus — file name ↔ construct, the single truth the boundary test pins. */
2
+ export declare const CONSTRUCT_FIXTURES: ReadonlyArray<{
3
+ file: string;
4
+ construct: string;
5
+ }>;
6
+ export declare const FIXTURES_DIR: string;
7
+ export interface CensusRow {
8
+ file: string;
9
+ construct: string;
10
+ /** Symbols the current substrate extracted from this fixture (0 = the construct is invisible). */
11
+ symbols: number;
12
+ /** Call/import/inherit relations recovered. */
13
+ edges: number;
14
+ /** Every extracted symbol carries line-level start/end. Column-level spans do not exist yet. */
15
+ lineSpans: boolean;
16
+ /** AST node count under the new substrate (visibility even where `symbols` froze at 0). */
17
+ astNodes?: number;
18
+ /** ERROR/missing nodes under the new substrate — 0 across the corpus is the L1 bar. */
19
+ astErrors?: number;
20
+ }
21
+ /**
22
+ * Scan EXACTLY the fixture corpus with the current substrate. Pure with respect to everything
23
+ * outside `dir` — files not named by CONSTRUCT_FIXTURES never enter the table, so a decoy planted
24
+ * beside the corpus cannot inflate or pollute the envelope.
25
+ */
26
+ export declare function substrateCensus(dir?: string): CensusRow[];
27
+ /**
28
+ * The census with the new-substrate columns filled in — the decision table's two arms side by
29
+ * side. Loads the wasm substrate lazily (never at import), so consumers that only need the old
30
+ * columns keep paying nothing.
31
+ */
32
+ export declare function fullCensus(dir?: string): Promise<CensusRow[]>;
33
+ /** The failures, by name — the honest half of the envelope (what the substrate cannot see). */
34
+ export declare function invisibleConstructs(rows?: CensusRow[]): string[];
35
+ /** Ensure fs is a declared dependency of this module's contract (fixture existence check). */
36
+ export declare function fixturesPresent(dir?: string): boolean;
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.FIXTURES_DIR = exports.CONSTRUCT_FIXTURES = void 0;
37
+ exports.substrateCensus = substrateCensus;
38
+ exports.fullCensus = fullCensus;
39
+ exports.invisibleConstructs = invisibleConstructs;
40
+ exports.fixturesPresent = fixturesPresent;
41
+ // @implements A-SPEC-510.1
42
+ /**
43
+ * The substrate census — the ONE table both the decision report and the regression tests consume.
44
+ *
45
+ * S-510.1 freezes the CURRENT hand-rolled parser's extraction outcome over a construct corpus
46
+ * (one tricky language construct per fixture file). The table is an honest envelope, not a
47
+ * scoreboard: a construct the parser cannot see is recorded as a failure, because an all-green
48
+ * census would be the envelope lying, not coverage. Every later substrate change (.2+) must move
49
+ * this table VISIBLY — updating it is how progress gets recorded.
50
+ *
51
+ * Line-level spans only: the current CodeSymbol carries startLine/endLine and no columns, so
52
+ * `spans` here asserts line presence — lossless round-trip is exactly what the current substrate
53
+ * cannot do, and that fact belongs in the decision report.
54
+ */
55
+ const fs = __importStar(require("node:fs"));
56
+ const path = __importStar(require("node:path"));
57
+ const cpg_scanner_1 = require("../cpg-scanner");
58
+ /** The construct corpus — file name ↔ construct, the single truth the boundary test pins. */
59
+ exports.CONSTRUCT_FIXTURES = [
60
+ { file: 'class-features.ts', construct: 'class: private field, getter/setter, static block' },
61
+ { file: 'generics.ts', construct: 'generics: constrained type params, generic class/alias' },
62
+ { file: 'async-await.ts', construct: 'async/await function + async arrow' },
63
+ { file: 'generators.ts', construct: 'generator + async generator' },
64
+ { file: 'optional-chaining.ts', construct: 'optional chaining / nullish coalescing / optional call' },
65
+ { file: 'switch-fallthrough.ts', construct: 'switch with fallthrough cases' },
66
+ { file: 'try-catch-finally.ts', construct: 'try/catch/finally with rethrow' },
67
+ { file: 'labeled-break.ts', construct: 'labeled break/continue' },
68
+ { file: 'nested-arrows.ts', construct: 'curried nested arrow functions' },
69
+ { file: 'destructuring.ts', construct: 'nested destructuring with defaults and rest' },
70
+ { file: 'enum-namespace.ts', construct: 'enum + namespace' },
71
+ { file: 'jsx-component.tsx', construct: 'JSX element in a .tsx component' },
72
+ { file: 'dynamic-import.ts', construct: 'dynamic import()' },
73
+ { file: 'async_def.py', construct: 'python: async def / await' },
74
+ { file: 'generators_yield_from.py', construct: 'python: generator + yield from' },
75
+ { file: 'comprehensions.py', construct: 'python: nested list/dict/set comprehensions' },
76
+ { file: 'decorators_args.py', construct: 'python: parameterized decorator + wraps' },
77
+ { file: 'match_case.py', construct: 'python: match/case with or- and mapping-patterns' },
78
+ { file: 'with_multi.py', construct: 'python: multi-item with statement' },
79
+ { file: 'walrus.py', construct: 'python: walrus operator in condition' },
80
+ { file: 'try_except_else_finally.py', construct: 'python: try/except/else/finally' },
81
+ { file: 'lambdas.py', construct: 'python: lambda returning lambda' },
82
+ { file: 'global_nonlocal.py', construct: 'python: global + nonlocal in closure' },
83
+ ];
84
+ // Fixtures are DATA, not code: excluded from tsc (a .tsx parse target must not need the build's
85
+ // JSX config), so `dist` carries no copy — a dist-resolved consumer falls back to the src corpus.
86
+ const srcFallback = path.join(__dirname, 'fixtures').replace(`${path.sep}dist${path.sep}`, `${path.sep}src${path.sep}`);
87
+ exports.FIXTURES_DIR = fs.existsSync(path.join(__dirname, 'fixtures'))
88
+ ? path.join(__dirname, 'fixtures')
89
+ : srcFallback;
90
+ /**
91
+ * Scan EXACTLY the fixture corpus with the current substrate. Pure with respect to everything
92
+ * outside `dir` — files not named by CONSTRUCT_FIXTURES never enter the table, so a decoy planted
93
+ * beside the corpus cannot inflate or pollute the envelope.
94
+ */
95
+ function substrateCensus(dir = exports.FIXTURES_DIR) {
96
+ const scanned = new cpg_scanner_1.CpgScanner().scan(dir, dir);
97
+ const byBase = new Map(scanned.map((f) => [path.basename(f.path), f]));
98
+ return exports.CONSTRUCT_FIXTURES.map(({ file, construct }) => {
99
+ const f = byBase.get(file);
100
+ const symbols = f?.symbols ?? [];
101
+ return {
102
+ file,
103
+ construct,
104
+ symbols: symbols.length,
105
+ edges: f?.edges?.length ?? 0,
106
+ lineSpans: symbols.length > 0 && symbols.every((s) => s.startLine > 0 && s.endLine >= s.startLine),
107
+ };
108
+ });
109
+ }
110
+ // @implements A-SPEC-510.2
111
+ /**
112
+ * The census with the new-substrate columns filled in — the decision table's two arms side by
113
+ * side. Loads the wasm substrate lazily (never at import), so consumers that only need the old
114
+ * columns keep paying nothing.
115
+ */
116
+ async function fullCensus(dir = exports.FIXTURES_DIR) {
117
+ const { parseAst } = await Promise.resolve().then(() => __importStar(require('./ast-store')));
118
+ const rows = substrateCensus(dir);
119
+ for (const row of rows) {
120
+ const ast = await parseAst(fs.readFileSync(path.join(dir, row.file), 'utf8'), row.file);
121
+ if (ast) {
122
+ row.astNodes = ast.nodes.length;
123
+ row.astErrors = ast.errorCount;
124
+ }
125
+ }
126
+ return rows;
127
+ }
128
+ /** The failures, by name — the honest half of the envelope (what the substrate cannot see). */
129
+ function invisibleConstructs(rows = substrateCensus()) {
130
+ return rows.filter((r) => r.symbols === 0).map((r) => r.file);
131
+ }
132
+ /** Ensure fs is a declared dependency of this module's contract (fixture existence check). */
133
+ function fixturesPresent(dir = exports.FIXTURES_DIR) {
134
+ return exports.CONSTRUCT_FIXTURES.every(({ file }) => fs.existsSync(path.join(dir, file)));
135
+ }
@@ -69,6 +69,21 @@ exports.EXTRACTABLE_RELATIONS = ['calls', 'imports', 'inherits'];
69
69
  const TS_FAMILY = { symbols: true, relations: ['calls', 'imports', 'inherits'], graphResolved: ['calls', 'imports', 'inherits'] };
70
70
  /** Symbols and calls only — no import edges, no inheritance. */
71
71
  const CALLS_ONLY = { symbols: true, relations: ['calls'], graphResolved: ['calls'] };
72
+ // @implements A-SPEC-511.1
73
+ /**
74
+ * Calls AND inheritance. The five non-TS/Python languages moved here once their inheritance was
75
+ * actually recovered AND measured arriving in the graph (S-510.6's corpus proved the gap; this is
76
+ * the repair). Imports stay out on purpose: their targets are package specifiers, which the
77
+ * resolver cannot turn into a scanned FILE node, so extracting them would widen `relations`
78
+ * without widening `graphResolved` — the exact conflation A-SPEC-286 forbids.
79
+ *
80
+ * Go's cell carries a stated limit: only STRUCT EMBEDDING is recovered. Interface satisfaction in
81
+ * Go is implicit and structural, so naming it would need a type checker; holmes-kit declares the
82
+ * gap instead of guessing it.
83
+ */
84
+ const CALLS_AND_INHERITS = {
85
+ symbols: true, relations: ['calls', 'inherits'], graphResolved: ['calls', 'inherits'],
86
+ };
72
87
  exports.LANGUAGE_CAPABILITY = {
73
88
  '.ts': TS_FAMILY, '.mts': TS_FAMILY, '.cts': TS_FAMILY, '.tsx': TS_FAMILY,
74
89
  '.js': TS_FAMILY, '.mjs': TS_FAMILY, '.jsx': TS_FAMILY,
@@ -76,11 +91,14 @@ exports.LANGUAGE_CAPABILITY = {
76
91
  '.cjs': CALLS_ONLY,
77
92
  // @implements A-SPEC-286 — the first language whose inheritance is both extracted AND resolved.
78
93
  '.py': { symbols: true, relations: ['calls', 'imports', 'inherits'], graphResolved: ['calls', 'imports', 'inherits'] },
79
- '.java': CALLS_ONLY, '.cs': CALLS_ONLY, '.go': CALLS_ONLY, '.rs': CALLS_ONLY,
94
+ // @implements A-SPEC-511.1 inheritance recovered: Java extends/implements, C# base_list,
95
+ // 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,
80
97
  // @implements A-SPEC-287 — C++ used to extract calls that never became graph edges, because its
81
98
  // edge scope omitted the class node the symbol walk uses. Fixed; it now resolves like the others.
82
- '.cpp': CALLS_ONLY, '.cc': CALLS_ONLY, '.cxx': CALLS_ONLY,
83
- '.hpp': CALLS_ONLY, '.hh': CALLS_ONLY, '.h': CALLS_ONLY,
99
+ // @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,
84
102
  };
85
103
  function capabilityFor(ext) {
86
104
  return exports.LANGUAGE_CAPABILITY[ext.toLowerCase()];
@@ -534,22 +534,41 @@ const EDGE_CONFIG = {
534
534
  // Go had the same defect, found by measuring rather than by assuming it was Rust-only: the symbol
535
535
  // walk qualifies a method by its RECEIVER type (`Store.Load`) while the edge walk used the bare
536
536
  // `name` field, so Go method calls never became edges either.
537
- go: { callTypes: ['call_expression'], calleeField: 'function', scopeTypes: ['function_declaration', 'method_declaration'] },
537
+ // @implements A-SPEC-511.1 `inherits` for Go is STRUCT EMBEDDING only: an embedded field is a
538
+ // syntactic fact. Interface satisfaction is implicit and structural (no `implements` keyword), so
539
+ // recovering it needs a type checker — a DECLARED PERMANENT LIMIT, never a guess.
540
+ go: { callTypes: ['call_expression'], calleeField: 'function', scopeTypes: ['function_declaration', 'method_declaration'],
541
+ inherits: { declTypes: ['type_spec'], mode: 'go-embedding' } },
538
542
  // @implements A-SPEC-300
539
543
  // `impl_item`/`trait_item` must open a qualifying scope, exactly as the symbol walk does: a method
540
544
  // in `impl Greet for En` is the symbol `En.hello`, but its edges came out qualified as bare
541
545
  // `hello`, so the caller matched no node and EVERY Rust method call was dropped (measured:
542
546
  // callerNotNamed 1 of 1). Same defect C++ had under A-SPEC-287.
543
- rust: { callTypes: ['call_expression'], calleeField: 'function', scopeTypes: ['function_item', 'impl_item', 'trait_item'] },
544
- java: { callTypes: ['method_invocation'], calleeField: 'name', scopeTypes: ['class_declaration', 'method_declaration'] },
545
- csharp: { callTypes: ['invocation_expression'], calleeField: 'function', scopeTypes: ['class_declaration', 'method_declaration'] },
547
+ // @implements A-SPEC-511.1 `impl Trait for Type` Type inherits Trait (implementer → abstract).
548
+ // An inherent `impl Type { }` carries no trait field and is NOT inheritance: no edge.
549
+ rust: { callTypes: ['call_expression'], calleeField: 'function', scopeTypes: ['function_item', 'impl_item', 'trait_item'],
550
+ inherits: { declTypes: ['impl_item'], mode: 'rust-impl' } },
551
+ // @implements A-SPEC-511.1 — Java separates the two syntactically, and BOTH are inheritance edges:
552
+ // `superclass` (extends) and `super_interfaces` (implements), plus an interface's own extends.
553
+ java: { callTypes: ['method_invocation'], calleeField: 'name', scopeTypes: ['class_declaration', 'method_declaration'],
554
+ inherits: { declTypes: ['class_declaration', 'interface_declaration', 'record_declaration', 'enum_declaration'],
555
+ fields: ['superclass', 'super_interfaces', 'interfaces', 'extends_interfaces'] } },
556
+ // @implements A-SPEC-511.1 — C# puts the base class AND the interfaces in ONE `base_list`, with no
557
+ // syntactic marker telling them apart. Splitting them would be a guess, so BOTH become `inherits`
558
+ // (a deliberate asymmetry with the TS walk, which emits extends only — H-SPEC-511 decision 1).
559
+ csharp: { callTypes: ['invocation_expression'], calleeField: 'function', scopeTypes: ['class_declaration', 'method_declaration'],
560
+ inherits: { declTypes: ['class_declaration', 'interface_declaration', 'struct_declaration', 'record_declaration'],
561
+ childTypes: ['base_list'] } },
546
562
  // @implements A-SPEC-287
547
563
  // C++ listed only `function_definition`, so a member function's edges came out qualified as bare
548
564
  // `run` while the symbol walk (which treats class_specifier/struct_specifier as scopes) emitted
549
565
  // `Child.run`. The builder requires the caller to be a real node in that file, so EVERY C++ edge
550
566
  // was dropped and the language contributed symbols and no edges at all. This is not a new rule —
551
567
  // it is the rule this table's own comment states, which java and csharp already follow.
552
- cpp: { callTypes: ['call_expression'], calleeField: 'function', scopeTypes: ['class_specifier', 'struct_specifier', 'function_definition'] },
568
+ // @implements A-SPEC-511.1 C++ bases live in `base_class_clause`; access specifiers
569
+ // (public/private/protected/virtual) are skipped and only the type name is taken.
570
+ cpp: { callTypes: ['call_expression'], calleeField: 'function', scopeTypes: ['class_specifier', 'struct_specifier', 'function_definition'],
571
+ inherits: { declTypes: ['class_specifier', 'struct_specifier'], childTypes: ['base_class_clause'] } },
553
572
  };
554
573
  // The callee's bare name: a plain identifier is itself; a member/selector/scoped/field node
555
574
  // (`o.m`, `self.c`, `mod::f`, `this.C`) unwraps to its LAST identifier segment — the method name.
@@ -589,6 +608,33 @@ function goReceiverTypeName(methodDeclNode) {
589
608
  }
590
609
  return null;
591
610
  }
611
+ // @implements A-SPEC-511.1
612
+ // A base type's NAME, normalized the way every other consumer expects: generic/template arguments
613
+ // dropped (`Base<T>` → `Base`), path qualifiers reduced to the last segment (`ns::Base`,
614
+ // `pkg.Base` → `Base`) — the same convention the Python attribute/subscript walk already uses.
615
+ // A computed or dependent base yields null: an unreadable base stays a gap, never a guess.
616
+ function baseTypeNameOf(node) {
617
+ if (!node)
618
+ return null;
619
+ const t = node.type;
620
+ if (t === 'identifier' || t === 'type_identifier' || t === 'field_identifier')
621
+ return node.text;
622
+ if (t === 'generic_type' || t === 'scoped_type_identifier' || t === 'qualified_identifier'
623
+ || t === 'scoped_identifier' || t === 'template_type' || t === 'qualified_type'
624
+ || t === 'type_arguments' || t === 'generic_name' || t === 'qualified_name') {
625
+ // Take the LAST identifier-ish child that is not inside the argument list.
626
+ for (let i = node.namedChildCount - 1; i >= 0; i--) {
627
+ const c = node.namedChild(i);
628
+ if (c.type === 'type_arguments' || c.type === 'template_argument_list')
629
+ continue;
630
+ const n = baseTypeNameOf(c);
631
+ if (n)
632
+ return n;
633
+ }
634
+ return null;
635
+ }
636
+ return null;
637
+ }
592
638
  function walkEdges(tree, cfg) {
593
639
  const out = [];
594
640
  const scope = new Set(cfg.scopeTypes);
@@ -639,12 +685,93 @@ function walkEdges(tree, cfg) {
639
685
  }
640
686
  return parts.length ? parts.join('.') : '<module>';
641
687
  };
688
+ // @implements A-SPEC-511.1
689
+ // Inheritance, table-driven per language. `from` is the declaring type's qualified name (the same
690
+ // id the symbol walk emits, so the builder can resolve it); `to` is the normalized base name.
691
+ const inh = cfg.inherits;
692
+ const declTypes = new Set(inh ? inh.declTypes : []);
693
+ const emitInherits = (node) => {
694
+ const selfName = scopeName(node);
695
+ // A clause reachable BOTH as a field and as a named child (Java's interfaces/super_interfaces)
696
+ // would otherwise emit each base twice; dedupe per declaration, not globally, so two classes
697
+ // inheriting the same base still produce two edges.
698
+ const seenHere = new Set();
699
+ const push = (target) => {
700
+ const to = baseTypeNameOf(target);
701
+ if (!to || !selfName || to === selfName || seenHere.has(to))
702
+ return;
703
+ seenHere.add(to);
704
+ out.push({ from: selfName, to, rel: 'inherits' });
705
+ };
706
+ if (inh.mode === 'rust-impl') {
707
+ // `impl Trait for Type`: the trait field only exists on a trait impl; inherent impls have none.
708
+ const trait = node.childForFieldName('trait');
709
+ if (trait)
710
+ push(trait);
711
+ return;
712
+ }
713
+ if (inh.mode === 'go-embedding') {
714
+ // `type A struct { B }` — an embedded field is a field_declaration with a type and NO name.
715
+ for (let i = 0; i < node.namedChildCount; i++) {
716
+ const st = node.namedChild(i);
717
+ if (st.type !== 'struct_type')
718
+ continue;
719
+ for (let j = 0; j < st.namedChildCount; j++) {
720
+ const fl = st.namedChild(j);
721
+ if (fl.type !== 'field_declaration_list')
722
+ continue;
723
+ for (let k = 0; k < fl.namedChildCount; k++) {
724
+ const fd = fl.namedChild(k);
725
+ if (fd.type !== 'field_declaration' || fd.childForFieldName('name'))
726
+ continue;
727
+ push(fd.childForFieldName('type'));
728
+ }
729
+ }
730
+ }
731
+ return;
732
+ }
733
+ // A clause wraps its targets one or two levels deep depending on the grammar: Java's
734
+ // `interfaces` field yields a `super_interfaces` node holding a `type_list` (measured), while
735
+ // `superclass` holds the type directly. Descend until a name is readable — but only through
736
+ // wrapper nodes, never into type arguments (baseTypeNameOf owns that rule).
737
+ const pushDeep = (n, depth = 0) => {
738
+ if (!n)
739
+ return;
740
+ if (baseTypeNameOf(n)) {
741
+ push(n);
742
+ return;
743
+ }
744
+ if (depth >= 3)
745
+ return;
746
+ for (let i = 0; i < n.namedChildCount; i++)
747
+ pushDeep(n.namedChild(i), depth + 1);
748
+ };
749
+ for (const field of inh.fields ?? [])
750
+ pushDeep(node.childForFieldName(field));
751
+ for (const childType of inh.childTypes ?? []) {
752
+ for (let i = 0; i < node.namedChildCount; i++) {
753
+ const c = node.namedChild(i);
754
+ if (c.type === childType)
755
+ pushDeep(c);
756
+ }
757
+ }
758
+ // Some grammars expose the clause only as a NAMED CHILD (Java's `super_interfaces`,
759
+ // `extends_interfaces`) rather than through a field — cover both spellings.
760
+ for (const field of inh.fields ?? []) {
761
+ for (let i = 0; i < node.namedChildCount; i++) {
762
+ if (node.namedChild(i).type === field)
763
+ pushDeep(node.namedChild(i));
764
+ }
765
+ }
766
+ };
642
767
  const visit = (node) => {
643
768
  if (callTypes.has(node.type)) {
644
769
  const callee = calleeNameOf(node.childForFieldName(cfg.calleeField));
645
770
  if (callee)
646
771
  out.push({ from: enclosing(node), to: callee, rel: 'calls' });
647
772
  }
773
+ if (inh && declTypes.has(node.type))
774
+ emitInherits(node);
648
775
  for (let i = 0; i < node.childCount; i++)
649
776
  visit(node.child(i));
650
777
  };
@@ -32,6 +32,9 @@ export declare function judgePush(input: PushJudgeInput): PushVerdict;
32
32
  /**
33
33
  * The pre-push stdin format: `<local ref> <local sha> <remote ref> <remote sha>` per line.
34
34
  * A deletion push (all-zero local sha) deletes a remote ref — there is nothing to vouch for.
35
+ * Only BRANCH refs (refs/heads/) are judged: the gate guards the moment a code line becomes
36
+ * someone's baseline, and a tag merely NAMES an existing commit — dogfooded 2026-09-01, pushing
37
+ * v0.3.9 (an ancestor of the evidenced tip) was refused by the exact-head rule until this line.
35
38
  */
36
39
  export declare function parsePushLines(stdinText: string): Array<{
37
40
  localSha: string;
@@ -50,6 +50,9 @@ function flatSubject(s) {
50
50
  /**
51
51
  * The pre-push stdin format: `<local ref> <local sha> <remote ref> <remote sha>` per line.
52
52
  * A deletion push (all-zero local sha) deletes a remote ref — there is nothing to vouch for.
53
+ * Only BRANCH refs (refs/heads/) are judged: the gate guards the moment a code line becomes
54
+ * someone's baseline, and a tag merely NAMES an existing commit — dogfooded 2026-09-01, pushing
55
+ * v0.3.9 (an ancestor of the evidenced tip) was refused by the exact-head rule until this line.
53
56
  */
54
57
  function parsePushLines(stdinText) {
55
58
  const out = [];
@@ -57,7 +60,9 @@ function parsePushLines(stdinText) {
57
60
  const parts = line.trim().split(/\s+/);
58
61
  if (parts.length !== 4)
59
62
  continue;
60
- const [, localSha, , remoteSha] = parts;
63
+ const [localRef, localSha, , remoteSha] = parts;
64
+ if (!localRef.startsWith('refs/heads/'))
65
+ continue; // tags/notes: names, not baselines
61
66
  if (!/^[0-9a-f]{40}$/.test(localSha))
62
67
  continue;
63
68
  if (/^0{40}$/.test(localSha))
@@ -0,0 +1,23 @@
1
+ {
2
+ "webTreeSitter": "0.27.0",
3
+ "grammars": [
4
+ {
5
+ "file": "tree-sitter-typescript.wasm",
6
+ "package": "tree-sitter-typescript",
7
+ "packageVersion": "0.23.2",
8
+ "sha256": "778025db5a8be0e70f8ccc3671e486dfeddd048c25d9e8a70c26de2e1bf6f97d"
9
+ },
10
+ {
11
+ "file": "tree-sitter-tsx.wasm",
12
+ "package": "tree-sitter-typescript",
13
+ "packageVersion": "0.23.2",
14
+ "sha256": "79e5da75ea62855a0cd67177685f0164eac87d5f630b3cbe1e0a099751ad30f8"
15
+ },
16
+ {
17
+ "file": "tree-sitter-python.wasm",
18
+ "package": "tree-sitter-python",
19
+ "packageVersion": "0.25.0",
20
+ "sha256": "16108b50df4ee9a30168794252ab55e7c93bfc5765d7fa0aa3e335752c515f47"
21
+ }
22
+ ]
23
+ }
Binary file
Binary file
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.3.10",
4
+ "version": "0.4.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",
@@ -12,6 +12,7 @@
12
12
  "files": [
13
13
  "bin/",
14
14
  "dist/",
15
+ "grammars/",
15
16
  "playbooks/",
16
17
  "scripts/install.ps1",
17
18
  "docs/install-guide.md",
@@ -68,7 +69,8 @@
68
69
  "tree-sitter-python": "^0.21.0",
69
70
  "tree-sitter-rust": "^0.21.0",
70
71
  "tree-sitter-typescript": "^0.21.2",
71
- "typescript": "^5.0.0"
72
+ "typescript": "^5.0.0",
73
+ "web-tree-sitter": "0.27.0"
72
74
  },
73
75
  "repository": {
74
76
  "type": "git",