@holmes-lab/holmes-kit 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/.build-id +1 -1
  3. package/dist/holmes/assoc/assoc-arm.d.ts +94 -0
  4. package/dist/holmes/assoc/assoc-arm.js +187 -0
  5. package/dist/holmes/assoc/explore.d.ts +21 -0
  6. package/dist/holmes/assoc/explore.js +160 -0
  7. package/dist/holmes/assoc/impact-baseline.d.ts +13 -0
  8. package/dist/holmes/assoc/impact-baseline.js +48 -0
  9. package/dist/holmes/assoc/ppr.d.ts +64 -0
  10. package/dist/holmes/assoc/ppr.js +110 -0
  11. package/dist/holmes/cli/agents.d.ts +24 -11
  12. package/dist/holmes/cli/agents.js +93 -21
  13. package/dist/holmes/cli/doctor.d.ts +56 -0
  14. package/dist/holmes/cli/doctor.js +311 -5
  15. package/dist/holmes/cli/index.js +13 -1
  16. package/dist/holmes/cli/semantic-key.d.ts +19 -0
  17. package/dist/holmes/cli/semantic-key.js +93 -0
  18. package/dist/holmes/config/config.d.ts +9 -0
  19. package/dist/holmes/config/config.js +8 -1
  20. package/dist/holmes/cpg/language-capability.d.ts +65 -0
  21. package/dist/holmes/cpg/language-capability.js +145 -0
  22. package/dist/holmes/cpg/language-parser-walk.js +179 -34
  23. package/dist/holmes/cpg/language-parser.d.ts +1 -1
  24. package/dist/holmes/governance/ledger-rechain.d.ts +12 -0
  25. package/dist/holmes/governance/ledger-rechain.js +17 -2
  26. package/dist/holmes/governance/provenance-ledger.js +21 -0
  27. package/dist/holmes/guardrail/blind-spots.js +12 -1
  28. package/dist/holmes/guardrail/impact-gate.d.ts +77 -0
  29. package/dist/holmes/guardrail/impact-gate.js +263 -0
  30. package/dist/holmes/guardrail/write-target.d.ts +38 -1
  31. package/dist/holmes/guardrail/write-target.js +41 -4
  32. package/dist/holmes/hooks/adapters/antigravity.js +12 -1
  33. package/dist/holmes/hooks/corrupt-state-run.d.ts +33 -0
  34. package/dist/holmes/hooks/corrupt-state-run.js +16 -0
  35. package/dist/holmes/hooks/pre-tool-use.d.ts +26 -0
  36. package/dist/holmes/hooks/pre-tool-use.js +203 -11
  37. package/dist/holmes/hooks/stop.d.ts +65 -0
  38. package/dist/holmes/hooks/stop.js +200 -2
  39. package/dist/holmes/mcp/handlers.d.ts +88 -0
  40. package/dist/holmes/mcp/handlers.js +597 -16
  41. package/dist/holmes/mcp/maintenance-analyze.d.ts +435 -0
  42. package/dist/holmes/mcp/maintenance-analyze.js +994 -0
  43. package/dist/holmes/mcp/maintenance-evidence.d.ts +140 -0
  44. package/dist/holmes/mcp/maintenance-evidence.js +253 -0
  45. package/dist/holmes/mcp/tool-schemas.js +71 -0
  46. package/dist/holmes/project/root.js +3 -1
  47. package/dist/holmes/review/baseline-arm.d.ts +37 -0
  48. package/dist/holmes/review/baseline-arm.js +51 -0
  49. package/dist/holmes/review/captured-stdin-guard.d.ts +8 -0
  50. package/dist/holmes/review/captured-stdin-guard.js +48 -0
  51. package/dist/holmes/review/coherence-verify.d.ts +31 -0
  52. package/dist/holmes/review/coherence-verify.js +144 -0
  53. package/dist/holmes/review/commit-text.d.ts +50 -0
  54. package/dist/holmes/review/commit-text.js +76 -0
  55. package/dist/holmes/review/confidence-calibration.d.ts +39 -0
  56. package/dist/holmes/review/confidence-calibration.js +39 -0
  57. package/dist/holmes/review/content-baseline.d.ts +38 -0
  58. package/dist/holmes/review/content-baseline.js +103 -0
  59. package/dist/holmes/review/content-verify.d.ts +20 -0
  60. package/dist/holmes/review/content-verify.js +73 -0
  61. package/dist/holmes/review/dense-retrieval.d.ts +66 -0
  62. package/dist/holmes/review/dense-retrieval.js +97 -0
  63. package/dist/holmes/review/edge-quality.d.ts +44 -0
  64. package/dist/holmes/review/edge-quality.js +117 -0
  65. package/dist/holmes/review/evaluation-metrics.d.ts +138 -0
  66. package/dist/holmes/review/evaluation-metrics.js +175 -0
  67. package/dist/holmes/review/graph-verifier.d.ts +34 -0
  68. package/dist/holmes/review/graph-verifier.js +62 -0
  69. package/dist/holmes/review/hop-ablation.d.ts +100 -0
  70. package/dist/holmes/review/hop-ablation.js +89 -0
  71. package/dist/holmes/review/manual-baseline.d.ts +209 -0
  72. package/dist/holmes/review/manual-baseline.js +2846 -0
  73. package/dist/holmes/review/oracle-gap.d.ts +32 -0
  74. package/dist/holmes/review/oracle-gap.js +102 -0
  75. package/dist/holmes/review/point-in-time-replay.d.ts +41 -0
  76. package/dist/holmes/review/point-in-time-replay.js +161 -0
  77. package/dist/holmes/review/rank-diagnosis.d.ts +43 -0
  78. package/dist/holmes/review/rank-diagnosis.js +163 -0
  79. package/dist/holmes/review/replay-calibration.d.ts +62 -0
  80. package/dist/holmes/review/replay-calibration.js +83 -0
  81. package/dist/holmes/review/replay-corpus.d.ts +135 -0
  82. package/dist/holmes/review/replay-corpus.js +210 -0
  83. package/dist/holmes/review/run-replay.d.ts +260 -0
  84. package/dist/holmes/review/run-replay.js +729 -0
  85. package/dist/holmes/review/semantic-arm.d.ts +271 -0
  86. package/dist/holmes/review/semantic-arm.js +717 -0
  87. package/dist/holmes/review/semantic-retrieval.d.ts +55 -0
  88. package/dist/holmes/review/semantic-retrieval.js +156 -0
  89. package/dist/holmes/review/spec-layer-stats.d.ts +38 -0
  90. package/dist/holmes/review/spec-layer-stats.js +52 -0
  91. package/dist/holmes/review/temporal-prior.d.ts +33 -0
  92. package/dist/holmes/review/temporal-prior.js +53 -0
  93. package/dist/holmes/review/test-runner.d.ts +15 -0
  94. package/dist/holmes/review/test-runner.js +41 -4
  95. package/dist/holmes/review/test-selection-breadth.d.ts +75 -0
  96. package/dist/holmes/review/test-selection-breadth.js +57 -0
  97. package/dist/holmes/review/traceability-benchmark.d.ts +81 -0
  98. package/dist/holmes/review/traceability-benchmark.js +135 -0
  99. package/dist/holmes/review/union-verify.d.ts +12 -0
  100. package/dist/holmes/review/union-verify.js +70 -0
  101. package/dist/holmes/rtm/graph-store.d.ts +51 -0
  102. package/dist/holmes/rtm/graph-store.js +122 -0
  103. package/dist/holmes/rtm/incremental.d.ts +25 -1
  104. package/dist/holmes/rtm/incremental.js +18 -1
  105. package/dist/holmes/rtm/localize.d.ts +28 -0
  106. package/dist/holmes/rtm/localize.js +272 -14
  107. package/dist/holmes/rtm/rtm-builder.d.ts +118 -3
  108. package/dist/holmes/rtm/rtm-builder.js +265 -28
  109. package/dist/holmes/rtm/rtm-graph.d.ts +117 -2
  110. package/dist/holmes/rtm/rtm-graph.js +194 -31
  111. package/dist/holmes/rtm/taint-benchmark.d.ts +97 -0
  112. package/dist/holmes/rtm/taint-benchmark.js +141 -0
  113. package/dist/holmes/rtm/test-scope.js +24 -1
  114. package/dist/holmes/semantic/credentials.d.ts +15 -0
  115. package/dist/holmes/semantic/credentials.js +134 -0
  116. package/dist/holmes/semantic/embedder.d.ts +44 -0
  117. package/dist/holmes/semantic/embedder.js +185 -0
  118. package/dist/holmes/semantic/hit-rerank.d.ts +4 -0
  119. package/dist/holmes/semantic/hit-rerank.js +38 -0
  120. package/dist/holmes/semantic/tier.d.ts +37 -0
  121. package/dist/holmes/semantic/tier.js +54 -0
  122. package/dist/holmes/semantic/vector-cache.d.ts +11 -0
  123. package/dist/holmes/semantic/vector-cache.js +91 -0
  124. package/dist/holmes/spec/acceptance-quality.d.ts +81 -0
  125. package/dist/holmes/spec/acceptance-quality.js +169 -0
  126. package/dist/holmes/spec/validator.js +33 -1
  127. package/dist/holmes/spec/yaml-scalar.d.ts +1 -0
  128. package/dist/holmes/spec/yaml-scalar.js +43 -0
  129. package/package.json +1 -1
@@ -0,0 +1,65 @@
1
+ /**
2
+ * What the extractor actually recovers, per language.
3
+ *
4
+ * Goal Phase 4 requires that per-language coverage and its limits always travel with the result.
5
+ * They did not, and the capability is genuinely uneven — measured with fixtures 2026-08-28, symbols
6
+ * come out of every advertised language, `imports` only out of Python and TypeScript-family files,
7
+ * and `inherits` out of NONE of them, not even where the declaration is completely unambiguous
8
+ * (`class Child(Base)`, `extends Base`).
9
+ *
10
+ * The problem was never the unevenness; it was the silence. Coverage reported only extensions
11
+ * OUTSIDE the scannable set, so an impact answer over a Java file looked exactly as trustworthy as
12
+ * one over Python while resting on strictly less evidence, and every downstream answer inherited
13
+ * that asymmetry without anyone being told.
14
+ *
15
+ * This table is pinned against the real scanner by fixtures in the sibling test. If it drifts from
16
+ * reality in either direction that test fails — a capability table that cannot fail guarantees
17
+ * nothing.
18
+ */
19
+ export declare const EXTRACTABLE_RELATIONS: readonly ["calls", "imports", "inherits"];
20
+ export type ExtractableRelation = typeof EXTRACTABLE_RELATIONS[number];
21
+ export interface LanguageCapability {
22
+ symbols: boolean;
23
+ /** What the SCANNER recovers from the source text. */
24
+ relations: ExtractableRelation[];
25
+ /**
26
+ * @implements A-SPEC-286
27
+ * What actually becomes a GRAPH edge — always a subset of `relations`.
28
+ *
29
+ * These are different claims and conflating them misleads. Measured 2026-08-28: the scanner emits
30
+ * 1,892 `imports` edges of which zero reach the graph, because their target is a module path
31
+ * rather than a symbol and the builder resolves symbol-to-symbol relations only. Everything
32
+ * downstream — impact, localization, the pre-edit gate — queries the graph, so the graph column is
33
+ * the one a coverage claim has to be made in.
34
+ */
35
+ graphResolved: ExtractableRelation[];
36
+ }
37
+ export declare const LANGUAGE_CAPABILITY: Record<string, LanguageCapability>;
38
+ export declare function capabilityFor(ext: string): LanguageCapability | undefined;
39
+ export interface LanguageGap {
40
+ ext: string;
41
+ /** Relation kinds this language's extractor does NOT recover at all. */
42
+ missing: ExtractableRelation[];
43
+ /**
44
+ * @implements A-SPEC-286
45
+ * Recovered by the scanner but never resolved into a graph edge. A distinct fact from `missing`:
46
+ * the information exists and is still unusable to anything that queries the graph.
47
+ */
48
+ extractedButUnresolved: ExtractableRelation[];
49
+ }
50
+ /**
51
+ * The capability gaps for the languages ACTUALLY present in a scan.
52
+ *
53
+ * Only languages that appear are listed: enumerating every language the tool could theoretically
54
+ * meet would bury the ones this answer actually rests on. A language with nothing missing drops out
55
+ * entirely, so the report shrinks to nothing as extraction catches up rather than becoming noise.
56
+ */
57
+ export declare function capabilityGapsFor(sourcePaths: readonly string[]): LanguageGap[];
58
+ /** Every advertised extension must have an entry; a blank would be a silent overclaim. */
59
+ export declare const ADVERTISED_EXTENSIONS: readonly string[];
60
+ /**
61
+ * An unknown extension gets a family of its own rather than joining a shared bucket: an unfamiliar
62
+ * language must not be able to take another language's definitions. Narrowing is the safe direction
63
+ * — it costs recall, which is disclosed, instead of asserting something false.
64
+ */
65
+ export declare function languageFamilyOf(ext: string): string;
@@ -0,0 +1,145 @@
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.ADVERTISED_EXTENSIONS = exports.LANGUAGE_CAPABILITY = exports.EXTRACTABLE_RELATIONS = void 0;
37
+ exports.capabilityFor = capabilityFor;
38
+ exports.capabilityGapsFor = capabilityGapsFor;
39
+ exports.languageFamilyOf = languageFamilyOf;
40
+ // @implements A-SPEC-289
41
+ // @implements A-SPEC-288
42
+ // @implements A-SPEC-287
43
+ // @implements A-SPEC-286
44
+ // @implements A-SPEC-285
45
+ const path = __importStar(require("node:path"));
46
+ const cpg_scanner_1 = require("./cpg-scanner");
47
+ /**
48
+ * What the extractor actually recovers, per language.
49
+ *
50
+ * Goal Phase 4 requires that per-language coverage and its limits always travel with the result.
51
+ * They did not, and the capability is genuinely uneven — measured with fixtures 2026-08-28, symbols
52
+ * come out of every advertised language, `imports` only out of Python and TypeScript-family files,
53
+ * and `inherits` out of NONE of them, not even where the declaration is completely unambiguous
54
+ * (`class Child(Base)`, `extends Base`).
55
+ *
56
+ * The problem was never the unevenness; it was the silence. Coverage reported only extensions
57
+ * OUTSIDE the scannable set, so an impact answer over a Java file looked exactly as trustworthy as
58
+ * one over Python while resting on strictly less evidence, and every downstream answer inherited
59
+ * that asymmetry without anyone being told.
60
+ *
61
+ * This table is pinned against the real scanner by fixtures in the sibling test. If it drifts from
62
+ * reality in either direction that test fails — a capability table that cannot fail guarantees
63
+ * nothing.
64
+ */
65
+ exports.EXTRACTABLE_RELATIONS = ['calls', 'imports', 'inherits'];
66
+ // @implements A-SPEC-288 — `extends` joins Python's inheritance; `implements` stays a stated gap.
67
+ // @implements A-SPEC-289 — imports now resolve to FILE nodes when the specifier is relative and
68
+ // names a scanned file; bare specifiers stay external and unresolved by design.
69
+ const TS_FAMILY = { symbols: true, relations: ['calls', 'imports', 'inherits'], graphResolved: ['calls', 'imports', 'inherits'] };
70
+ /** Symbols and calls only — no import edges, no inheritance. */
71
+ const CALLS_ONLY = { symbols: true, relations: ['calls'], graphResolved: ['calls'] };
72
+ exports.LANGUAGE_CAPABILITY = {
73
+ '.ts': TS_FAMILY, '.mts': TS_FAMILY, '.cts': TS_FAMILY, '.tsx': TS_FAMILY,
74
+ '.js': TS_FAMILY, '.mjs': TS_FAMILY, '.jsx': TS_FAMILY,
75
+ // CommonJS `require()` is not recovered as an import, unlike ESM `import` — measured.
76
+ '.cjs': CALLS_ONLY,
77
+ // @implements A-SPEC-286 — the first language whose inheritance is both extracted AND resolved.
78
+ '.py': { symbols: true, relations: ['calls', 'imports', 'inherits'], graphResolved: ['calls', 'imports', 'inherits'] },
79
+ '.java': CALLS_ONLY, '.cs': CALLS_ONLY, '.go': CALLS_ONLY, '.rs': CALLS_ONLY,
80
+ // @implements A-SPEC-287 — C++ used to extract calls that never became graph edges, because its
81
+ // 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,
84
+ };
85
+ function capabilityFor(ext) {
86
+ return exports.LANGUAGE_CAPABILITY[ext.toLowerCase()];
87
+ }
88
+ /**
89
+ * The capability gaps for the languages ACTUALLY present in a scan.
90
+ *
91
+ * Only languages that appear are listed: enumerating every language the tool could theoretically
92
+ * meet would bury the ones this answer actually rests on. A language with nothing missing drops out
93
+ * entirely, so the report shrinks to nothing as extraction catches up rather than becoming noise.
94
+ */
95
+ function capabilityGapsFor(sourcePaths) {
96
+ const present = new Set();
97
+ for (const p of sourcePaths) {
98
+ const ext = path.extname(p).toLowerCase();
99
+ if (capabilityFor(ext))
100
+ present.add(ext);
101
+ }
102
+ return [...present].sort()
103
+ .map((ext) => {
104
+ const cap = capabilityFor(ext);
105
+ return {
106
+ ext,
107
+ missing: exports.EXTRACTABLE_RELATIONS.filter((r) => !cap.relations.includes(r)),
108
+ extractedButUnresolved: cap.relations.filter((r) => !cap.graphResolved.includes(r)),
109
+ };
110
+ })
111
+ .filter((gap) => gap.missing.length > 0 || gap.extractedButUnresolved.length > 0);
112
+ }
113
+ /** Every advertised extension must have an entry; a blank would be a silent overclaim. */
114
+ exports.ADVERTISED_EXTENSIONS = cpg_scanner_1.SCANNABLE_EXTENSIONS;
115
+ // @implements A-SPEC-300
116
+ /**
117
+ * Extensions that can actually call each other, grouped.
118
+ *
119
+ * Name resolution links a call to a definition when the name is defined in exactly one file. That
120
+ * rule had no notion of language, and measured on a five-language fixture (2026-08-28) it asserted
121
+ * CODE:Child.run@Child.java --calls--> CODE:helper@lib.rs
122
+ * A Java method calling a Rust function. Worse, the correct target sat in the SAME file: it was
123
+ * missed on qualification, leaving the Rust definition as the only candidate, so the precision rule
124
+ * actively confirmed a wrong answer. A missing edge widens scope; a wrong edge narrows it onto the
125
+ * wrong place.
126
+ *
127
+ * The groups follow real interop, not file-name similarity: TypeScript and JavaScript genuinely call
128
+ * each other, and a C++ implementation genuinely calls its header's declarations.
129
+ */
130
+ const LANGUAGE_FAMILIES = {
131
+ '.ts': 'ts-js', '.mts': 'ts-js', '.cts': 'ts-js', '.tsx': 'ts-js',
132
+ '.js': 'ts-js', '.mjs': 'ts-js', '.jsx': 'ts-js', '.cjs': 'ts-js',
133
+ '.py': 'python',
134
+ '.java': 'java', '.cs': 'csharp', '.go': 'go', '.rs': 'rust',
135
+ '.cpp': 'cpp', '.cc': 'cpp', '.cxx': 'cpp', '.hpp': 'cpp', '.hh': 'cpp', '.h': 'cpp',
136
+ };
137
+ /**
138
+ * An unknown extension gets a family of its own rather than joining a shared bucket: an unfamiliar
139
+ * language must not be able to take another language's definitions. Narrowing is the safe direction
140
+ * — it costs recall, which is disclosed, instead of asserting something false.
141
+ */
142
+ function languageFamilyOf(ext) {
143
+ const lower = ext.toLowerCase();
144
+ return LANGUAGE_FAMILIES[lower] ?? `ext:${lower}`;
145
+ }
@@ -43,6 +43,32 @@ const LANG_CONFIG = {
43
43
  funcTypes: ['function_declaration'],
44
44
  methodTypes: ['method_definition'],
45
45
  funcIsMethodInClass: false,
46
+ // @implements A-SPEC-301
47
+ // Contracts are deliberately NOT emitted, and this is a measured decision rather than an
48
+ // oversight. This repository has 192 interfaces and 54 type aliases that the graph does not
49
+ // know about — about a quarter of its top-level declarations — and in TypeScript the interface
50
+ // IS the contract, so Phase 5's contract-level impact has no object to reason about.
51
+ //
52
+ // Emitting them was implemented and measured on the point-in-time replay corpus (2026-08-28):
53
+ //
54
+ // hit rate recall
55
+ // Top-1 0.250 -> 0.333 0.194 -> 0.278 better
56
+ // Top-3 0.500 -> 0.500 0.331 -> 0.331 unchanged
57
+ // Top-5 0.583 -> 0.583 0.470 -> 0.470 unchanged
58
+ // Top-10 0.833 -> 0.750 0.690 -> 0.607 WORSE
59
+ //
60
+ // 246 extra candidates sharpen the top of the ranking and crowd true files out of the tail. The
61
+ // A-SPEC-297 floor on Top-10 recall (0.62) caught it. Operating principle 11 is explicit that a
62
+ // graph extension must show an improvement, and this one shows a trade, so it was reverted.
63
+ // A-SPEC-302 tried the obvious next design — contracts as nodes, excluded from localization
64
+ // scoring — and measured it: every replay metric came back EXACTLY at baseline
65
+ // (Top-1 0.250/0.194, Top-10 0.833/0.690). So S-301's Top-1 gain came entirely from scoring
66
+ // contract names, and removing them from scoring removes the gain with it. What remains is 246
67
+ // nodes and 4 edges with no measured downstream benefit, because nothing consumes contract nodes
68
+ // yet. Operating principle 11 forbids exactly that, so this was declined too.
69
+ //
70
+ // What would justify it: building the CONSUMER and the nodes in one slice, so the benefit is
71
+ // measurable in the same experiment. Nodes first, benefit later, is the shape that keeps failing.
46
72
  },
47
73
  python: {
48
74
  classTypes: ['class_definition'],
@@ -84,9 +110,22 @@ LANG_CONFIG.tsx = LANG_CONFIG.typescript;
84
110
  // only when lexically inside a class, and recursing into ANY def resets
85
111
  // enclosingClass to null for its children, so a def nested inside a def is
86
112
  // never mistaken for a method of the outer class).
113
+ // @implements A-SPEC-291
114
+ // Symbols are qualified by the FULL path of named scopes, exactly as `namedScopeOf` qualifies an
115
+ // edge's caller. The two used to disagree: this walk qualified only by an enclosing CLASS, so a
116
+ // function nested in a function came out bare (`inner`) and an object-literal method was not emitted
117
+ // at all, while the edge walk named the caller `outer.inner` / `makeHandlers.doThing`. The builder
118
+ // requires a caller to be a real node in its file, so every such edge was dropped — measured
119
+ // 2026-08-28, 1,307 relations in this repository (591 in TypeScript, 9.4%), including every call
120
+ // made by the MCP handler surface, which lives in methods of an object a function returns.
121
+ //
122
+ // `scope` carries whether each segment is a class, because `kind` still distinguishes a method of a
123
+ // class from a plain nested function. An unnamed scope (a function EXPRESSION) contributes no
124
+ // segment — the edge walk does not count it either, so the two stay aligned.
87
125
  function walkSymbols(tree, config) {
88
126
  const out = [];
89
- const visit = (node, enclosingClass) => {
127
+ const pathOf = (scope, name) => [...scope.map((s) => s.name), name].join('.');
128
+ const visit = (node, scope) => {
90
129
  const type = node.type;
91
130
  if (config.classTypes.includes(type)) {
92
131
  const nameNode = node.childForFieldName('name');
@@ -95,65 +134,68 @@ function walkSymbols(tree, config) {
95
134
  out.push({
96
135
  name,
97
136
  kind: 'class',
98
- qualifiedName: name,
137
+ qualifiedName: pathOf(scope, name),
99
138
  startLine: node.startPosition.row + 1,
100
139
  endLine: node.endPosition.row + 1,
101
140
  });
102
141
  }
103
- const childClass = name || enclosingClass;
142
+ const childScope = name ? [...scope, { name, isClass: true }] : scope;
104
143
  for (let i = 0; i < node.childCount; i++)
105
- visit(node.child(i), childClass);
144
+ visit(node.child(i), childScope);
106
145
  return;
107
146
  }
108
147
  if (config.funcTypes.includes(type)) {
109
148
  const nameNode = node.childForFieldName('name');
110
149
  const name = nameNode ? nameNode.text : undefined;
150
+ let childScope = scope;
111
151
  if (name) {
112
- const isMethod = config.funcIsMethodInClass && !!enclosingClass;
113
- const kind = isMethod ? 'method' : 'function';
114
- const qualifiedName = isMethod ? `${enclosingClass}.${name}` : name;
152
+ // A method only when the IMMEDIATE enclosing scope is a class — the same question the old
153
+ // `enclosingClass` flag answered, now asked of the path's last segment. Python's nested-def
154
+ // reset rule falls out of this for free: a def inside a def has a function as its immediate
155
+ // scope, so it is a function, and its qualified name states the nesting rather than hiding it.
156
+ const enclosing = scope[scope.length - 1];
157
+ const isMethod = config.funcIsMethodInClass && !!enclosing && enclosing.isClass;
115
158
  out.push({
116
159
  name,
117
- kind,
118
- qualifiedName,
160
+ kind: isMethod ? 'method' : 'function',
161
+ qualifiedName: pathOf(scope, name),
119
162
  startLine: node.startPosition.row + 1,
120
163
  endLine: node.endPosition.row + 1,
121
164
  });
165
+ childScope = [...scope, { name, isClass: false }];
122
166
  }
123
- // Nested-def reset rule (see funcIsMethodInClass doc above): only
124
- // context-sensitive languages (Python) reset enclosingClass to null
125
- // here; languages with a dedicated methodTypes node (TypeScript) carry
126
- // enclosingClass through unchanged, matching original behavior.
127
- const childEnclosing = config.funcIsMethodInClass ? null : enclosingClass;
128
167
  for (let i = 0; i < node.childCount; i++)
129
- visit(node.child(i), childEnclosing);
168
+ visit(node.child(i), childScope);
130
169
  return;
131
170
  }
132
171
  if (config.methodTypes.includes(type)) {
133
172
  const nameNode = node.childForFieldName('name');
134
- if (nameNode) {
173
+ let childScope = scope;
174
+ // An object-literal method is emitted only when the scope path can QUALIFY it. Nested inside
175
+ // a named function it is nameable — `makeHandlers.doThing` — and the edge walk already called
176
+ // it that, so emitting it is what makes the two agree. At the top level there is nothing to
177
+ // qualify it with, and a bare `foo` symbol would collide with any real function of that name
178
+ // and corrupt resolution for both: that is the spurious-unqualified-method case the previous
179
+ // rule existed to prevent, and it still holds.
180
+ if (nameNode && scope.length > 0) {
135
181
  const name = nameNode.text;
136
- // Only emit method symbols for genuine class members
137
- // (enclosingClass !== null). Skip object-literal methods, which have
138
- // no enclosing class, to avoid spurious unqualified method symbols.
139
- if (enclosingClass) {
140
- out.push({
141
- name,
142
- kind: 'method',
143
- qualifiedName: `${enclosingClass}.${name}`,
144
- startLine: node.startPosition.row + 1,
145
- endLine: node.endPosition.row + 1,
146
- });
147
- }
182
+ out.push({
183
+ name,
184
+ kind: 'method',
185
+ qualifiedName: pathOf(scope, name),
186
+ startLine: node.startPosition.row + 1,
187
+ endLine: node.endPosition.row + 1,
188
+ });
189
+ childScope = [...scope, { name, isClass: false }];
148
190
  }
149
191
  for (let i = 0; i < node.childCount; i++)
150
- visit(node.child(i), enclosingClass);
192
+ visit(node.child(i), childScope);
151
193
  return;
152
194
  }
153
195
  for (let i = 0; i < node.childCount; i++)
154
- visit(node.child(i), enclosingClass);
196
+ visit(node.child(i), scope);
155
197
  };
156
- visit(tree.rootNode, null);
198
+ visit(tree.rootNode, []);
157
199
  return out;
158
200
  }
159
201
  // @implements A-SPEC-121.6
@@ -488,11 +530,26 @@ function extractSymbolsFromTree(tree, lang = 'typescript') {
488
530
  // identifier segment); `scopeTypes` are the class/function nodes whose `name` builds the enclosing
489
531
  // qualified path (so a method's edges resolve to `Class.method`, matching the symbol walk).
490
532
  const EDGE_CONFIG = {
533
+ // @implements A-SPEC-300
534
+ // Go had the same defect, found by measuring rather than by assuming it was Rust-only: the symbol
535
+ // walk qualifies a method by its RECEIVER type (`Store.Load`) while the edge walk used the bare
536
+ // `name` field, so Go method calls never became edges either.
491
537
  go: { callTypes: ['call_expression'], calleeField: 'function', scopeTypes: ['function_declaration', 'method_declaration'] },
492
- rust: { callTypes: ['call_expression'], calleeField: 'function', scopeTypes: ['function_item'] },
538
+ // @implements A-SPEC-300
539
+ // `impl_item`/`trait_item` must open a qualifying scope, exactly as the symbol walk does: a method
540
+ // in `impl Greet for En` is the symbol `En.hello`, but its edges came out qualified as bare
541
+ // `hello`, so the caller matched no node and EVERY Rust method call was dropped (measured:
542
+ // 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'] },
493
544
  java: { callTypes: ['method_invocation'], calleeField: 'name', scopeTypes: ['class_declaration', 'method_declaration'] },
494
545
  csharp: { callTypes: ['invocation_expression'], calleeField: 'function', scopeTypes: ['class_declaration', 'method_declaration'] },
495
- cpp: { callTypes: ['call_expression'], calleeField: 'function', scopeTypes: ['function_definition'] },
546
+ // @implements A-SPEC-287
547
+ // C++ listed only `function_definition`, so a member function's edges came out qualified as bare
548
+ // `run` while the symbol walk (which treats class_specifier/struct_specifier as scopes) emitted
549
+ // `Child.run`. The builder requires the caller to be a real node in that file, so EVERY C++ edge
550
+ // was dropped and the language contributed symbols and no edges at all. This is not a new rule —
551
+ // 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'] },
496
553
  };
497
554
  // The callee's bare name: a plain identifier is itself; a member/selector/scoped/field node
498
555
  // (`o.m`, `self.c`, `mod::f`, `this.C`) unwraps to its LAST identifier segment — the method name.
@@ -515,6 +572,23 @@ function calleeNameOf(node) {
515
572
  // The shared table-driven edge walk. `from` is the enclosing definition's QUALIFIED name (so it
516
573
  // matches the symbol node id and can be resolved), `to` is the bare callee name. A call the config
517
574
  // cannot read emits nothing — never a guessed edge.
575
+ // @implements A-SPEC-300
576
+ // The receiver's type name for a Go method, descending through pointer and generic forms —
577
+ // `(s Store)`, `(s *Store)`, `(s *Store[T])` all yield `Store`. Mirrors the symbol walk so edge
578
+ // qualification and symbol qualification cannot drift apart.
579
+ function goReceiverTypeName(methodDeclNode) {
580
+ const receiver = methodDeclNode.childForFieldName('receiver');
581
+ if (!receiver)
582
+ return null;
583
+ const paramDecl = receiver.namedChild(0);
584
+ let t = paramDecl && paramDecl.childForFieldName('type');
585
+ for (let hops = 0; t && hops < 5; hops++) {
586
+ if (t.type === 'type_identifier')
587
+ return t.text;
588
+ t = t.childForFieldName('type') ?? t.namedChild(0);
589
+ }
590
+ return null;
591
+ }
518
592
  function walkEdges(tree, cfg) {
519
593
  const out = [];
520
594
  const scope = new Set(cfg.scopeTypes);
@@ -524,6 +598,21 @@ function walkEdges(tree, cfg) {
524
598
  // `declarator` fields to the first identifier — otherwise a C++ function's edges would qualify to
525
599
  // `<module>` and never resolve to its bare symbol name.
526
600
  const scopeName = (n) => {
601
+ // @implements A-SPEC-300
602
+ // Nodes whose qualifying name is NOT in a `name` field. `impl_item` has no name at all — the
603
+ // type it implements for lives in `type` — and a Go `method_declaration` has a `name`, but the
604
+ // symbol walk qualifies it by the receiver type, so using `name` here would disagree with the
605
+ // node ids that actually exist.
606
+ if (n.type === 'impl_item') {
607
+ const t = n.childForFieldName('type');
608
+ return t ? (t.childForFieldName('name')?.text ?? calleeNameOf(t) ?? t.text) : null;
609
+ }
610
+ if (n.type === 'method_declaration' && n.childForFieldName('receiver')) {
611
+ const recv = goReceiverTypeName(n);
612
+ const nm2 = n.childForFieldName('name');
613
+ if (recv && nm2)
614
+ return `${recv}.${nm2.text}`;
615
+ }
527
616
  const nm = n.childForFieldName('name');
528
617
  if (nm)
529
618
  return nm.text;
@@ -636,6 +725,26 @@ function extractEdgesFromTree(tree, lang = 'typescript') {
636
725
  return nm ? nm.text : null;
637
726
  };
638
727
  const visit = (node, scope) => {
728
+ if (node.type === 'class_declaration' || node.type === 'interface_declaration') {
729
+ // @implements A-SPEC-288
730
+ // `class Child extends Base` and `interface A extends B` are syntactically unambiguous, so
731
+ // this costs no precision. Only a plain identifier base is taken: `extends mixin(Base)` is
732
+ // computed and cannot be read statically, and `implements` is a different relation that this
733
+ // slice deliberately leaves out. Both stay gaps rather than guesses.
734
+ const nm = node.childForFieldName('name');
735
+ const heritage = (node.namedChildren ?? []).find((c) => c.type === 'class_heritage' || c.type === 'extends_type_clause');
736
+ if (nm && heritage) {
737
+ for (const clause of [heritage, ...(heritage.namedChildren ?? [])]) {
738
+ if (clause.type === 'implements_clause')
739
+ continue;
740
+ for (const c of clause.namedChildren ?? []) {
741
+ if (c.type === 'identifier' || c.type === 'type_identifier') {
742
+ out.push({ from: nm.text, to: c.text, rel: 'inherits' });
743
+ }
744
+ }
745
+ }
746
+ }
747
+ }
639
748
  if (node.type === 'import_statement') {
640
749
  const src = node.childForFieldName('source') ?? (node.namedChildren && node.namedChildren.find((c) => c.type === 'string'));
641
750
  if (src)
@@ -673,6 +782,8 @@ function extractEdgesFromTree(tree, lang = 'typescript') {
673
782
  }
674
783
  const named = namedScopeOf(node);
675
784
  const childScope = named ? scope.concat(named) : scope;
785
+ // Type parameters are scoped to their declaration; collect them before descending so a use of
786
+ // `C` inside `function identity<C>(v: C)` is never mistaken for the contract `C`.
676
787
  for (let i = 0; i < node.childCount; i++)
677
788
  visit(node.child(i), childScope);
678
789
  };
@@ -719,7 +830,41 @@ function walkPythonEdges(tree) {
719
830
  return n.text;
720
831
  };
721
832
  const visit = (node) => {
722
- if (node.type === 'import_statement') {
833
+ if (node.type === 'class_definition') {
834
+ // @implements A-SPEC-286
835
+ // `class Child(Base)` is syntactically unambiguous, so this costs no precision. Excluded:
836
+ // keyword arguments (`metaclass=Meta` is not a base) and starred/dynamic forms, which produce
837
+ // NOTHING rather than a guess — a wrong edge silently corrupts every impact answer that walks
838
+ // it, while a missing one shows up as a gap.
839
+ const name = node.childForFieldName('name');
840
+ const supers = node.childForFieldName('superclasses');
841
+ if (name && supers) {
842
+ for (const arg of supers.namedChildren ?? []) {
843
+ if (arg.type === 'keyword_argument')
844
+ continue;
845
+ let base = null;
846
+ if (arg.type === 'identifier')
847
+ base = arg.text;
848
+ // `mod.Base` -> `Base`, and `Generic[T]` -> `Generic`: same convention the call walk uses.
849
+ else if (arg.type === 'attribute') {
850
+ const a = arg.childForFieldName('attribute');
851
+ base = a ? a.text : null;
852
+ }
853
+ else if (arg.type === 'subscript') {
854
+ const v = arg.childForFieldName('value');
855
+ if (v && v.type === 'identifier')
856
+ base = v.text;
857
+ else if (v && v.type === 'attribute') {
858
+ const a = v.childForFieldName('attribute');
859
+ base = a ? a.text : null;
860
+ }
861
+ }
862
+ if (base)
863
+ out.push({ from: enclosing(name), to: base, rel: 'inherits' });
864
+ }
865
+ }
866
+ }
867
+ else if (node.type === 'import_statement') {
723
868
  for (const c of node.namedChildren ?? []) {
724
869
  const m = moduleName(c);
725
870
  if (m)
@@ -26,7 +26,7 @@ export interface CodeSymbol {
26
26
  export interface CodeEdge {
27
27
  from: string;
28
28
  to: string;
29
- rel: 'calls' | 'imports';
29
+ rel: 'calls' | 'imports' | 'inherits';
30
30
  }
31
31
  export type TsLang = 'typescript' | 'tsx' | 'python' | 'csharp' | 'java' | 'go' | 'rust' | 'cpp';
32
32
  /**
@@ -22,4 +22,16 @@ export type RechainResult = {
22
22
  } | {
23
23
  refused: string;
24
24
  };
25
+ /**
26
+ * @implements A-SPEC-455
27
+ * Is the COMMITTED mainline a hash+seq prefix of the working copy?
28
+ *
29
+ * Exported so the turn boundary asks the same question the rechain CLI asks. It was inline here,
30
+ * and inline meant it ran only when an operator invoked a command — the fourth instance in one day
31
+ * of a judgement that is right and enforced in exactly one place.
32
+ *
33
+ * Duplicating it instead would be the mistake A-SPEC-449 measured: the config rule knew the
34
+ * copy/move family and the code rule did not, because the vocabulary lived in two copies.
35
+ */
36
+ export declare function mainlineIsPrefix(mainline: ProvenanceEvent[], working: ProvenanceEvent[]): boolean;
25
37
  export declare function rechainLedger(mainline: ProvenanceEvent[], working: ProvenanceEvent[], key?: string): RechainResult;
@@ -2,6 +2,7 @@
2
2
  // @implements A-SPEC-256.1
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.acceptsAsMainline = acceptsAsMainline;
5
+ exports.mainlineIsPrefix = mainlineIsPrefix;
5
6
  exports.rechainLedger = rechainLedger;
6
7
  /**
7
8
  * Re-chain a forked ledger against an EXPLICIT mainline — never a greedy one.
@@ -36,14 +37,28 @@ const bodyOf = (e) => {
36
37
  const { seq: _s, prevHash: _p, hash: _h, ...body } = e;
37
38
  return body;
38
39
  };
40
+ /**
41
+ * @implements A-SPEC-455
42
+ * Is the COMMITTED mainline a hash+seq prefix of the working copy?
43
+ *
44
+ * Exported so the turn boundary asks the same question the rechain CLI asks. It was inline here,
45
+ * and inline meant it ran only when an operator invoked a command — the fourth instance in one day
46
+ * of a judgement that is right and enforced in exactly one place.
47
+ *
48
+ * Duplicating it instead would be the mistake A-SPEC-449 measured: the config rule knew the
49
+ * copy/move family and the code rule did not, because the vocabulary lived in two copies.
50
+ */
51
+ function mainlineIsPrefix(mainline, working) {
52
+ return mainline.length <= working.length
53
+ && mainline.every((e, i) => working[i] !== undefined && working[i].hash === e.hash && working[i].seq === e.seq);
54
+ }
39
55
  function rechainLedger(mainline, working, key) {
40
56
  // The working file is ALREADY-VERIFIED only when it verifies under the current key AND carries
41
57
  // the mainline as a hash-prefix (round-4): the adoption arm belongs to COMMITTED mainline
42
58
  // candidates only — applied to the working copy it certified a keyless re-signed replacement as
43
59
  // "no change" (reproduced forgery), and a rolled-back verifying prefix as "no change" (leaving
44
60
  // committed history unrestored — the working copy shorter than the committed mainline).
45
- const mainlineIsPrefixOfWorking = mainline.length <= working.length
46
- && mainline.every((e, i) => working[i] !== undefined && working[i].hash === e.hash && working[i].seq === e.seq);
61
+ const mainlineIsPrefixOfWorking = mainlineIsPrefix(mainline, working);
47
62
  if ((0, provenance_chain_1.verifyChain)(working, key).ok && mainlineIsPrefixOfWorking) {
48
63
  return { events: working, orphanCount: 0, alreadyVerified: true };
49
64
  }
@@ -183,6 +183,27 @@ class ProvenanceLedger {
183
183
  if (!r.ok)
184
184
  broken.push({ replicaId: c.replicaId, file: c.file, detail: r.detail, brokenAt: r.brokenAt });
185
185
  }
186
+ // @implements A-SPEC-458
187
+ // Cross-replica double-spend. consumeNonceExclusively's lock is per-file, so two machines can
188
+ // each spend the same single-use nonce offline and the merge is chain-valid — each chain
189
+ // verifies. The comment there promised this is "DETECTED afterwards in the merged ledger";
190
+ // measured 2026-08-30, nothing did. A single-use nonce appearing in two `nonce-consumed`
191
+ // records is always a violation (REQ-141): there is no legitimate re-spend. Counted by the raw
192
+ // input value, the same rule isNonceConsumed matches on — one consume path writes one form, so
193
+ // a real double-spend records the same value twice.
194
+ const counts = new Map();
195
+ for (const e of this.loadAll()) {
196
+ if (e.kind !== 'nonce-consumed')
197
+ continue;
198
+ for (const inp of e.inputs ?? [])
199
+ counts.set(inp, (counts.get(inp) ?? 0) + 1);
200
+ }
201
+ for (const [nonce, n] of counts) {
202
+ if (n > 1) {
203
+ broken.push({ replicaId: null, file: '(cross-replica)',
204
+ detail: `single-use nonce ${nonce.slice(0, 16)}… spent ${n} times across replicas — REQ-141 double-spend` });
205
+ }
206
+ }
186
207
  return { ok: broken.length === 0, broken };
187
208
  }
188
209
  /**
@@ -34,5 +34,16 @@ function blindSpotSummary() {
34
34
  // shell route to either state is denied by this gate; what remains is deletion from outside the
35
35
  // session, and saying so is the only honest option left.
36
36
  + ' `.ax` 전체가 세션 밖에서 삭제되면(에디터·Finder·세션 밖 git) 원장도 함께 사라져'
37
- + ' 거버넌스를 켠 적 없는 프로젝트와 구별되지 않습니다 — `.ax/specs`만 사라진 경우는 탐지됩니다(REQ-175).';
37
+ + ' 거버넌스를 켠 적 없는 프로젝트와 구별되지 않습니다 — `.ax/specs`만 사라진 경우는 탐지됩니다(REQ-175).'
38
+ // @implements A-SPEC-421
39
+ // Measured against the INSTALLED build, which is the only place it shows: `tool_name: "Bash"`
40
+ // with `rm -rf /` is denied and `tool_name: "mcp__shell__run"` with the same command is allowed.
41
+ // The gate dispatches on the tool's NAME, so a name it does not know carries whatever it likes.
42
+ // Every MCP tool is such a name, and this repository itself runs several MCP servers — so this
43
+ // is the shape of the gap, not a hypothetical. Denying unknown names instead was considered and
44
+ // rejected: it would refuse every harness's Read, Grep and search tools and leave the guardrail
45
+ // unusable. What is fixable here is the map, and an undeclared gap reads as coverage.
46
+ + ' 게이트는 **도구 이름**으로 판정하므로 이름을 모르는 도구는 검사하지 않습니다 —'
47
+ + ' MCP 서버가 제공하는 도구(`mcp__<서버>__<도구>`)는 셸을 실행하더라도 이 게이트를 거치지'
48
+ + ' 않습니다. MCP 서버는 각자의 승인 경로로 통제하십시오.';
38
49
  }