@holmes-lab/holmes-kit 0.2.1 → 0.3.1

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 (132) hide show
  1. package/CHANGELOG.md +55 -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/elicit-approval.d.ts +32 -2
  40. package/dist/holmes/mcp/elicit-approval.js +30 -2
  41. package/dist/holmes/mcp/handlers.d.ts +88 -0
  42. package/dist/holmes/mcp/handlers.js +636 -35
  43. package/dist/holmes/mcp/maintenance-analyze.d.ts +435 -0
  44. package/dist/holmes/mcp/maintenance-analyze.js +994 -0
  45. package/dist/holmes/mcp/maintenance-evidence.d.ts +140 -0
  46. package/dist/holmes/mcp/maintenance-evidence.js +253 -0
  47. package/dist/holmes/mcp/server.js +13 -10
  48. package/dist/holmes/mcp/tool-schemas.js +71 -0
  49. package/dist/holmes/project/root.js +3 -1
  50. package/dist/holmes/review/baseline-arm.d.ts +37 -0
  51. package/dist/holmes/review/baseline-arm.js +51 -0
  52. package/dist/holmes/review/captured-stdin-guard.d.ts +8 -0
  53. package/dist/holmes/review/captured-stdin-guard.js +48 -0
  54. package/dist/holmes/review/coherence-verify.d.ts +31 -0
  55. package/dist/holmes/review/coherence-verify.js +144 -0
  56. package/dist/holmes/review/commit-text.d.ts +50 -0
  57. package/dist/holmes/review/commit-text.js +76 -0
  58. package/dist/holmes/review/confidence-calibration.d.ts +39 -0
  59. package/dist/holmes/review/confidence-calibration.js +39 -0
  60. package/dist/holmes/review/content-baseline.d.ts +38 -0
  61. package/dist/holmes/review/content-baseline.js +103 -0
  62. package/dist/holmes/review/content-verify.d.ts +20 -0
  63. package/dist/holmes/review/content-verify.js +73 -0
  64. package/dist/holmes/review/dense-retrieval.d.ts +66 -0
  65. package/dist/holmes/review/dense-retrieval.js +97 -0
  66. package/dist/holmes/review/edge-quality.d.ts +44 -0
  67. package/dist/holmes/review/edge-quality.js +117 -0
  68. package/dist/holmes/review/evaluation-metrics.d.ts +138 -0
  69. package/dist/holmes/review/evaluation-metrics.js +175 -0
  70. package/dist/holmes/review/graph-verifier.d.ts +34 -0
  71. package/dist/holmes/review/graph-verifier.js +62 -0
  72. package/dist/holmes/review/hop-ablation.d.ts +100 -0
  73. package/dist/holmes/review/hop-ablation.js +89 -0
  74. package/dist/holmes/review/manual-baseline.d.ts +209 -0
  75. package/dist/holmes/review/manual-baseline.js +2846 -0
  76. package/dist/holmes/review/oracle-gap.d.ts +32 -0
  77. package/dist/holmes/review/oracle-gap.js +102 -0
  78. package/dist/holmes/review/point-in-time-replay.d.ts +41 -0
  79. package/dist/holmes/review/point-in-time-replay.js +161 -0
  80. package/dist/holmes/review/rank-diagnosis.d.ts +43 -0
  81. package/dist/holmes/review/rank-diagnosis.js +163 -0
  82. package/dist/holmes/review/replay-calibration.d.ts +62 -0
  83. package/dist/holmes/review/replay-calibration.js +83 -0
  84. package/dist/holmes/review/replay-corpus.d.ts +135 -0
  85. package/dist/holmes/review/replay-corpus.js +210 -0
  86. package/dist/holmes/review/run-replay.d.ts +260 -0
  87. package/dist/holmes/review/run-replay.js +729 -0
  88. package/dist/holmes/review/semantic-arm.d.ts +271 -0
  89. package/dist/holmes/review/semantic-arm.js +717 -0
  90. package/dist/holmes/review/semantic-retrieval.d.ts +55 -0
  91. package/dist/holmes/review/semantic-retrieval.js +156 -0
  92. package/dist/holmes/review/spec-layer-stats.d.ts +38 -0
  93. package/dist/holmes/review/spec-layer-stats.js +52 -0
  94. package/dist/holmes/review/temporal-prior.d.ts +33 -0
  95. package/dist/holmes/review/temporal-prior.js +53 -0
  96. package/dist/holmes/review/test-runner.d.ts +15 -0
  97. package/dist/holmes/review/test-runner.js +41 -4
  98. package/dist/holmes/review/test-selection-breadth.d.ts +75 -0
  99. package/dist/holmes/review/test-selection-breadth.js +57 -0
  100. package/dist/holmes/review/traceability-benchmark.d.ts +81 -0
  101. package/dist/holmes/review/traceability-benchmark.js +135 -0
  102. package/dist/holmes/review/union-verify.d.ts +12 -0
  103. package/dist/holmes/review/union-verify.js +70 -0
  104. package/dist/holmes/rtm/graph-store.d.ts +51 -0
  105. package/dist/holmes/rtm/graph-store.js +122 -0
  106. package/dist/holmes/rtm/incremental.d.ts +25 -1
  107. package/dist/holmes/rtm/incremental.js +18 -1
  108. package/dist/holmes/rtm/localize.d.ts +28 -0
  109. package/dist/holmes/rtm/localize.js +272 -14
  110. package/dist/holmes/rtm/rtm-builder.d.ts +118 -3
  111. package/dist/holmes/rtm/rtm-builder.js +265 -28
  112. package/dist/holmes/rtm/rtm-graph.d.ts +117 -2
  113. package/dist/holmes/rtm/rtm-graph.js +194 -31
  114. package/dist/holmes/rtm/taint-benchmark.d.ts +97 -0
  115. package/dist/holmes/rtm/taint-benchmark.js +141 -0
  116. package/dist/holmes/rtm/test-scope.js +24 -1
  117. package/dist/holmes/semantic/credentials.d.ts +15 -0
  118. package/dist/holmes/semantic/credentials.js +134 -0
  119. package/dist/holmes/semantic/embedder.d.ts +44 -0
  120. package/dist/holmes/semantic/embedder.js +185 -0
  121. package/dist/holmes/semantic/hit-rerank.d.ts +4 -0
  122. package/dist/holmes/semantic/hit-rerank.js +38 -0
  123. package/dist/holmes/semantic/tier.d.ts +37 -0
  124. package/dist/holmes/semantic/tier.js +54 -0
  125. package/dist/holmes/semantic/vector-cache.d.ts +11 -0
  126. package/dist/holmes/semantic/vector-cache.js +91 -0
  127. package/dist/holmes/spec/acceptance-quality.d.ts +81 -0
  128. package/dist/holmes/spec/acceptance-quality.js +169 -0
  129. package/dist/holmes/spec/validator.js +33 -1
  130. package/dist/holmes/spec/yaml-scalar.d.ts +1 -0
  131. package/dist/holmes/spec/yaml-scalar.js +43 -0
  132. package/package.json +1 -1
@@ -1,4 +1,37 @@
1
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
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.DEFAULT_IMPACT = void 0;
4
37
  exports.buildFileSubgraph = buildFileSubgraph;
@@ -6,6 +39,28 @@ exports.buildRtm = buildRtm;
6
39
  exports.addCallEdges = addCallEdges;
7
40
  exports.impactedBy = impactedBy;
8
41
  exports.explainImpact = explainImpact;
42
+ exports.addCommitHistory = addCommitHistory;
43
+ exports.addImportEdges = addImportEdges;
44
+ exports.addDecisionEdges = addDecisionEdges;
45
+ const path = __importStar(require("node:path"));
46
+ const language_capability_1 = require("../cpg/language-capability");
47
+ /**
48
+ * @implements A-SPEC-281
49
+ * Build one fact's provenance from the shared observation context plus what this call site knows.
50
+ * `confidence: null` for an inferred fact is deliberate — see `Provenance` in rtm-graph.ts.
51
+ */
52
+ function fact(opts, sourceLocation, derivation, confidence, evidenceDigest = null) {
53
+ return {
54
+ sourceCommit: opts?.provenance?.sourceCommit ?? null,
55
+ extractorVersion: opts?.provenance?.extractorVersion ?? null,
56
+ sourceLocation,
57
+ confidence,
58
+ evidenceDigest,
59
+ observedAt: opts?.provenance?.observedAt ?? null,
60
+ validUntil: opts?.provenance?.validUntil ?? null,
61
+ derivation,
62
+ };
63
+ }
9
64
  /**
10
65
  * Adds one scanned file's CODE nodes and `implements` edges to the graph,
11
66
  * tagged with that file's sourcePath so RtmGraph.removeBySource(f.sourcePath)
@@ -13,16 +68,18 @@ exports.explainImpact = explainImpact;
13
68
  * orchestrator — see Task 4).
14
69
  */
15
70
  // @implements A-SPEC-121.2
16
- function buildFileSubgraph(f, specs, graph) {
71
+ function buildFileSubgraph(f, specs, graph, opts) {
17
72
  for (const sym of f.symbols) {
18
73
  // A-SPEC-121.3: fold source_path into the node id itself so same-named
19
74
  // symbols in different files get distinct CODE nodes instead of
20
75
  // colliding on a bare `CODE:<qualifiedName>` id (see RtmGraph's class
21
76
  // comment). Resolve a bare qn back to these ids via `graph.codeNodeIds`.
22
77
  const codeId = `CODE:${sym.qualifiedName}@${f.sourcePath}`;
23
- graph.addNode(codeId, 'CODE', f.sourcePath);
78
+ const digest = opts?.fileDigest?.(f.sourcePath) ?? null;
79
+ graph.addNode(codeId, 'CODE', f.sourcePath, fact(opts, `${f.sourcePath}:${sym.startLine}`, 'ast-scan', 1, digest));
24
80
  for (const aspec of f.implementsSpecs) {
25
- graph.addEdge(codeId, `SPEC:${aspec}`, 'implements', f.sourcePath);
81
+ // DECLARED by an `@implements` anchor: the fact is stated in the file, not inferred.
82
+ graph.addEdge(codeId, `SPEC:${aspec}`, 'implements', f.sourcePath, fact(opts, `${f.sourcePath}:${sym.startLine}`, 'anchor', 1, digest));
26
83
  }
27
84
  }
28
85
  }
@@ -31,22 +88,28 @@ function buildRtm(specs, scanned, graph, opts) {
31
88
  // ONE transaction for the whole build: measured, per-insert commits were ~20% of the insert cost
32
89
  // on a 56k-insert graph (216 ms -> 173 ms with a transaction; 88 -> 52 ms with cached statements).
33
90
  // It also makes a failed build atomic — no half-graph that could be mistaken for a complete one.
91
+ let resolution = {
92
+ resolved: 0, ambiguous: 0, unknownTarget: 0, callerNotNamed: 0, selfReference: 0, moduleScoped: 0,
93
+ };
34
94
  graph.transaction(() => {
35
95
  // Add spec nodes and dependencies
36
96
  for (const s of specs) {
37
97
  const sourcePath = opts?.specSourcePath?.(s.id);
38
- graph.addNode(`SPEC:${s.id}`, s.type, sourcePath);
98
+ graph.addNode(`SPEC:${s.id}`, s.type, sourcePath, fact(opts, sourcePath ?? null, 'spec-store', 1));
99
+ // DECLARED in frontmatter.
39
100
  for (const p of s.dependsOn) {
40
- graph.addEdge(`SPEC:${s.id}`, `SPEC:${p}`, 'depends_on', sourcePath);
101
+ graph.addEdge(`SPEC:${s.id}`, `SPEC:${p}`, 'depends_on', sourcePath, fact(opts, sourcePath ?? null, 'frontmatter', 1));
41
102
  }
42
103
  }
43
104
  // Add code nodes and implements edges, one file at a time
44
105
  for (const f of scanned) {
45
- buildFileSubgraph(f, specs, graph);
106
+ buildFileSubgraph(f, specs, graph, opts);
46
107
  }
47
108
  // Call edges last: resolving a callee needs every file's symbols to exist as nodes first.
48
- addCallEdges(scanned, graph);
109
+ resolution = addCallEdges(scanned, graph, opts);
110
+ addImportEdges(scanned, graph, opts);
49
111
  });
112
+ return { resolution };
50
113
  }
51
114
  /**
52
115
  * Resolve each file's recovered call relations to graph edges.
@@ -68,56 +131,98 @@ function buildRtm(specs, scanned, graph, opts) {
68
131
  * exact (src,dst,rel) triple — so the first-writer-wins hazard documented on RtmGraph cannot arise
69
132
  * for call edges, and `removeBySource(caller)` drops exactly them.
70
133
  */
71
- function addCallEdges(scanned, graph) {
72
- // qualified name -> the sourcePaths defining it. Built once; ambiguity is decided from counts.
134
+ function addCallEdges(scanned, graph, opts) {
135
+ const report = {
136
+ resolved: 0, ambiguous: 0, unknownTarget: 0, callerNotNamed: 0, selfReference: 0, moduleScoped: 0,
137
+ };
138
+ // @implements A-SPEC-300
139
+ // Indexes are built PER LANGUAGE FAMILY. Resolution used to look across the whole scan, so a Java
140
+ // call could bind to a Rust definition — measured, and it did. Ambiguity is judged inside the
141
+ // family too, which is why two same-named functions in Python and Go now each resolve correctly
142
+ // instead of cancelling each other out: this filter raises precision AND recall here.
143
+ const familyOf = (sourcePath) => (0, language_capability_1.languageFamilyOf)(path.extname(sourcePath));
73
144
  const definedIn = new Map();
74
145
  for (const f of scanned) {
146
+ const family = familyOf(f.sourcePath);
147
+ let table = definedIn.get(family);
148
+ if (!table) {
149
+ table = new Map();
150
+ definedIn.set(family, table);
151
+ }
75
152
  for (const sym of f.symbols) {
76
- const list = definedIn.get(sym.qualifiedName);
153
+ const list = table.get(sym.qualifiedName);
77
154
  if (list) {
78
155
  if (!list.includes(f.sourcePath))
79
156
  list.push(f.sourcePath);
80
157
  }
81
158
  else
82
- definedIn.set(sym.qualifiedName, [f.sourcePath]);
159
+ table.set(sym.qualifiedName, [f.sourcePath]);
83
160
  }
84
161
  }
85
162
  // A bare callee `m` may correspond to a qualified `Class.m`. Index the last segment too, but only
86
163
  // to a UNIQUE owner — a method name shared by several classes stays unresolvable on purpose.
87
164
  const byLastSegment = new Map();
88
- for (const [qn, paths] of definedIn) {
89
- const last = qn.includes('.') ? qn.slice(qn.lastIndexOf('.') + 1) : qn;
90
- if (byLastSegment.has(last))
91
- byLastSegment.set(last, null); // ambiguous — never resolved
92
- else
93
- byLastSegment.set(last, { qn, paths });
165
+ for (const [family, table] of definedIn) {
166
+ const index = new Map();
167
+ byLastSegment.set(family, index);
168
+ for (const [qn, paths] of table) {
169
+ const last = qn.includes('.') ? qn.slice(qn.lastIndexOf('.') + 1) : qn;
170
+ if (index.has(last))
171
+ index.set(last, null); // ambiguous — never resolved
172
+ else
173
+ index.set(last, { qn, paths });
174
+ }
94
175
  }
95
176
  for (const f of scanned) {
96
177
  for (const e of f.edges ?? []) {
97
- if (e.rel !== 'calls' || !e.from || !e.to || e.from === '<module>')
178
+ // @implements A-SPEC-286 `inherits` resolves by the SAME rule as `calls`: symbol-to-symbol,
179
+ // and only when the name is defined in exactly one file across the scan. `imports` is excluded
180
+ // because its target is a module path, not a symbol; resolving that needs module->file
181
+ // resolution this scanner does not do, and the capability table reports the gap rather than
182
+ // letting it pass as coverage.
183
+ if (e.rel !== 'calls' && e.rel !== 'inherits')
98
184
  continue;
185
+ if (!e.from || !e.to || e.from === '<module>') {
186
+ report.moduleScoped++;
187
+ continue;
188
+ }
99
189
  const fromId = `CODE:${e.from}@${f.sourcePath}`;
100
190
  // The caller must be a real node in THIS file; otherwise the walk produced a name the symbol
101
191
  // extractor never emitted and the edge cannot be trusted.
102
- if (!graph.codeNodeIds(e.from).includes(fromId))
192
+ if (!graph.codeNodeIds(e.from).includes(fromId)) {
193
+ report.callerNotNamed++;
103
194
  continue;
195
+ }
196
+ const family = familyOf(f.sourcePath);
197
+ const table = definedIn.get(family);
104
198
  let target = null;
105
- if (definedIn.has(e.to))
106
- target = { qn: e.to, paths: definedIn.get(e.to) };
199
+ if (table?.has(e.to))
200
+ target = { qn: e.to, paths: table.get(e.to) };
107
201
  else
108
- target = byLastSegment.get(e.to) ?? null;
109
- if (!target)
202
+ target = byLastSegment.get(family)?.get(e.to) ?? null;
203
+ if (!target) {
204
+ report.unknownTarget++;
110
205
  continue;
206
+ }
111
207
  const sameFile = target.paths.includes(f.sourcePath);
112
208
  const path = sameFile ? f.sourcePath : (target.paths.length === 1 ? target.paths[0] : null);
113
- if (!path)
114
- continue; // defined in several files — ambiguous, emit nothing
209
+ if (!path) {
210
+ report.ambiguous++;
211
+ continue;
212
+ } // defined in several files — emit nothing
115
213
  const toId = `CODE:${target.qn}@${path}`;
116
- if (toId === fromId)
117
- continue; // self-recursion carries no impact information
118
- graph.addEdge(fromId, toId, 'calls', f.sourcePath);
214
+ if (toId === fromId) {
215
+ report.selfReference++;
216
+ continue;
217
+ } // self-recursion carries no impact
218
+ // INFERRED by name resolution. Its precision has never been measured in this repository, so
219
+ // the confidence stays null rather than carrying a number nobody can defend — an invented
220
+ // figure would be indistinguishable from a measured one to every downstream consumer.
221
+ graph.addEdge(fromId, toId, e.rel, f.sourcePath, fact(opts, `${f.sourcePath}`, 'name-resolution', null));
222
+ report.resolved++;
119
223
  }
120
224
  }
225
+ return report;
121
226
  }
122
227
  exports.DEFAULT_IMPACT = { maxCallDepth: 3, hubInDegree: 12 };
123
228
  /**
@@ -189,3 +294,135 @@ function explainImpact(graph, changedSymbolQNames, opts) {
189
294
  // `rtm_impact` emits this directly, and determinism is the harness's thesis.
190
295
  return { specs: [...set].sort(), reachedByDepth, stoppedAt, ...(seedIsHub ? { seedIsHub } : {}) };
191
296
  }
297
+ /**
298
+ * @implements A-SPEC-284
299
+ * The graph's first temporal layer: `COMMIT:<sha>` nodes and `changed_by` edges to the code they
300
+ * touched.
301
+ *
302
+ * Kept as a SEPARATE ingestion rather than folded into the per-file subgraph. Commit history is not
303
+ * a product of scanning a file, and entangling it with the incremental path would put the
304
+ * incremental/full-rebuild convergence A-SPEC-280 proved at risk for no benefit — the code layer
305
+ * must come out identical whether or not history was ever ingested.
306
+ *
307
+ * An edge is emitted ONLY when the changed file is present in the scan. Precision over recall, the
308
+ * same rule call resolution follows: an edge pointing at a node that does not exist poisons every
309
+ * traversal that walks it, while a missing edge merely leaves a gap. Commits touching files nobody
310
+ * scanned are still recorded as nodes — they happened.
311
+ */
312
+ function addCommitHistory(history, scanned, graph, opts) {
313
+ const codeBySource = new Map();
314
+ for (const f of scanned) {
315
+ codeBySource.set(f.sourcePath, f.symbols.map((s) => `CODE:${s.qualifiedName}@${f.sourcePath}`));
316
+ }
317
+ graph.transaction(() => {
318
+ for (const record of history) {
319
+ const commitId = `COMMIT:${record.commit}`;
320
+ // Git STATES which files a commit touched — a declared fact, not an inference, so confidence 1.
321
+ graph.addNode(commitId, 'COMMIT', undefined, fact(opts, record.commit, 'git-log', 1));
322
+ for (const file of record.files) {
323
+ for (const codeId of codeBySource.get(file) ?? []) {
324
+ graph.addEdge(commitId, codeId, 'changed_by', file, fact(opts, `${file}@${record.commit}`, 'git-log', 1));
325
+ }
326
+ }
327
+ }
328
+ });
329
+ }
330
+ /**
331
+ * @implements A-SPEC-289
332
+ * FILE nodes and resolved `imports` edges.
333
+ *
334
+ * Import relations were extracted and then dropped, because their endpoints are a file and a module
335
+ * path while the graph knew only symbols and specs — measured on this repository, 1,892 extracted
336
+ * and 0 in the graph. `File` is a node kind Goal Phase 3 names, so it becomes one.
337
+ *
338
+ * ONLY relative specifiers are resolved, and only to a file the scan actually contains. A bare
339
+ * specifier (`node:fs`, `js-yaml`) is an external package with no node to point at, and inventing
340
+ * one would be a guess — the same precision-over-recall rule call resolution follows. Measured: 441
341
+ * of 1,892 specifiers (23%) resolve inside the repository, and the rest failing to resolve is a
342
+ * fact about the imports, not a defect in the resolver.
343
+ */
344
+ function addImportEdges(scanned, graph, opts) {
345
+ const known = new Set(scanned.map((f) => f.sourcePath));
346
+ const EXTENSIONS = ['', '.ts', '.tsx', '.mts', '.cts', '.js', '.mjs', '.cjs', '.jsx', '.py',
347
+ '/index.ts', '/index.js', '/__init__.py'];
348
+ const firstKnown = (base) => EXTENSIONS.map((ext) => `${base}${ext}`).find((candidate) => known.has(candidate)) ?? null;
349
+ const resolve = (fromFile, spec) => {
350
+ // @implements A-SPEC-406
351
+ // A specifier without a leading dot used to be refused outright, on the ground that it names an
352
+ // external package. That holds for `node:fs` and `js-yaml`; it does NOT hold for Python, which
353
+ // writes intra-repository modules absolutely — `from src.core.memory_audit import X`. Measured
354
+ // on the jarvis corpus: 1060 specifiers, 0 relative, 542 naming a file the scan already had, and
355
+ // 0 import edges in the graph. The whole import layer was missing for that language because the
356
+ // resolver's shape was JS's.
357
+ //
358
+ // The rule the original was written under is unchanged and is what makes this safe: resolve
359
+ // ONLY to a file the scan actually contains. `os.path` becomes `os/path.py`, which is in no
360
+ // scan, so it still emits nothing. Nothing is invented; the dotted form is simply also read.
361
+ if (!spec.startsWith('.')) {
362
+ if (spec.includes(':'))
363
+ return null; // `node:fs` and friends are never a repository path
364
+ return firstKnown(path.posix.normalize(spec.split('.').filter(Boolean).join('/')));
365
+ }
366
+ // @implements A-SPEC-289
367
+ // Python writes a relative import as `.b` / `..pkg.mod`, where the leading dots are LEVELS and
368
+ // the remaining dots are path separators — not the `./b` form JS uses. Treating `.b` as a path
369
+ // yields the literal name `.b`, which matches nothing. Detected by the absence of a slash: a
370
+ // specifier that already contains one is a JS-style path and is left alone.
371
+ let relative = spec;
372
+ if (!spec.includes('/')) {
373
+ const dots = spec.match(/^\.+/)[0].length;
374
+ const rest = spec.slice(dots).split('.').filter(Boolean).join('/');
375
+ relative = `${'../'.repeat(Math.max(0, dots - 1)) || './'}${rest}`;
376
+ }
377
+ return firstKnown(path.posix.normalize(path.posix.join(path.posix.dirname(fromFile), relative)));
378
+ };
379
+ graph.transaction(() => {
380
+ for (const f of scanned) {
381
+ for (const e of f.edges ?? []) {
382
+ if (e.rel !== 'imports')
383
+ continue;
384
+ const target = resolve(f.sourcePath, e.to);
385
+ if (!target)
386
+ continue;
387
+ // DECLARED: the specifier is written in the source. What is inferred is only WHICH file it
388
+ // names, and an unresolvable one emits nothing rather than a guess.
389
+ graph.addNode(`FILE:${f.sourcePath}`, 'FILE', f.sourcePath, fact(opts, f.sourcePath, 'ast-scan', 1));
390
+ graph.addNode(`FILE:${target}`, 'FILE', target, fact(opts, target, 'ast-scan', 1));
391
+ graph.addEdge(`FILE:${f.sourcePath}`, `FILE:${target}`, 'imports', f.sourcePath, fact(opts, `${f.sourcePath} -> ${e.to}`, 'import-resolution', 1));
392
+ }
393
+ }
394
+ });
395
+ }
396
+ /**
397
+ * @implements A-SPEC-293
398
+ * DECISION nodes and the `constrained_by` / `supersedes` edges around them.
399
+ *
400
+ * Citation is the link, chosen by measurement rather than by taste: on this repository `governs`
401
+ * resolves only 9 of its 65 names to symbols (the rest are free-form design concepts), while 37
402
+ * source files and 71 spec files cite an ADR outright — and a citation sits exactly where the
403
+ * decision actually bears on the work.
404
+ *
405
+ * A citation naming a decision that does not exist creates neither node nor edge: an edge to a node
406
+ * nobody can open is a guess, the same rule import and call resolution follow. `supersedes` is
407
+ * declared in frontmatter and linked only when both decisions exist; this repository currently has
408
+ * none, and the edge kind is supported anyway rather than waiting for the first one.
409
+ */
410
+ function addDecisionEdges(decisions, citations, graph, opts) {
411
+ const known = new Map(decisions.map((d) => [d.id, d]));
412
+ graph.transaction(() => {
413
+ for (const d of decisions) {
414
+ graph.addNode(`DECISION:${d.id}`, 'DECISION', undefined, fact(opts, d.id, 'decision-record', 1));
415
+ }
416
+ for (const d of decisions) {
417
+ if (!d.supersedes || !known.has(d.supersedes))
418
+ continue;
419
+ graph.addEdge(`DECISION:${d.id}`, `DECISION:${d.supersedes}`, 'supersedes', undefined, fact(opts, d.id, 'decision-record', 1));
420
+ }
421
+ for (const c of citations) {
422
+ if (!known.has(c.adr))
423
+ continue;
424
+ // DECLARED: the citation is written in the file or spec that carries it.
425
+ graph.addEdge(c.from, `DECISION:${c.adr}`, 'constrained_by', undefined, fact(opts, c.from, 'citation', 1));
426
+ }
427
+ });
428
+ }
@@ -22,13 +22,41 @@
22
22
  * The result is identical to the full build, so the incremental pass is currently redundant work
23
23
  * rather than a saving. It becomes meaningful the moment a persisted graph exists.
24
24
  */
25
+ /**
26
+ * @implements A-SPEC-281
27
+ * What is known about HOW a fact got into the graph.
28
+ *
29
+ * `confidence: null` is load-bearing: it means the derivation gave us no measured basis for a
30
+ * number, and it must stay distinguishable from `1`. A `calls` edge is INFERRED by name resolution
31
+ * whose precision this repository has never measured, so it records the derivation and leaves the
32
+ * number empty rather than inventing one — the same rule the ablation follows for an unmeasured
33
+ * recall. An `implements` or `depends_on` edge is DECLARED (an anchor, a frontmatter field), so it
34
+ * carries 1: the fact is stated, not guessed.
35
+ *
36
+ * `observedAt` is supplied by the CALLER. Reading a clock in here would make the same inputs build a
37
+ * different graph on every run and break the incremental/full-rebuild convergence A-SPEC-280 proved.
38
+ */
39
+ export interface Provenance {
40
+ sourceCommit: string | null;
41
+ extractorVersion: string | null;
42
+ sourceLocation: string | null;
43
+ confidence: number | null;
44
+ evidenceDigest: string | null;
45
+ observedAt: string | null;
46
+ validUntil: string | null;
47
+ derivation: string | null;
48
+ }
25
49
  export declare class RtmGraph {
26
50
  private readonly db;
27
51
  constructor(dbPath?: string);
28
52
  private nodeStmt?;
29
53
  private edgeStmt?;
30
- addNode(id: string, kind: string, sourcePath?: string): void;
31
- addEdge(src: string, dst: string, rel: string, sourcePath?: string): void;
54
+ addNode(id: string, kind: string, sourcePath?: string, provenance?: Provenance): void;
55
+ addEdge(src: string, dst: string, rel: string, sourcePath?: string, provenance?: Provenance): void;
56
+ /** @implements A-SPEC-281 — how this node got here, or null if the node is unknown. */
57
+ provenanceOfNode(id: string): Provenance | null;
58
+ /** @implements A-SPEC-281 — how this edge got here, or null if the edge is unknown. */
59
+ provenanceOfEdge(src: string, dst: string, rel: string): Provenance | null;
32
60
  /**
33
61
  * @implements A-SPEC-139
34
62
  * Run `fn` inside ONE SQLite transaction, so a build commits once instead of once per insert.
@@ -45,6 +73,64 @@ export declare class RtmGraph {
45
73
  * "who calls me" question into "what is attached to me in any way at all".
46
74
  */
47
75
  callersOf(id: string): string[];
76
+ /**
77
+ * @implements A-SPEC-288
78
+ * Everything a change to this symbol can reach: its callers PLUS its subclasses. Changing a base
79
+ * method affects a subclass that never calls it, so impact must follow inheritance.
80
+ *
81
+ * Deliberately SEPARATE from `callersOf`, which stays calls-only. Taint follows data flow and hub
82
+ * detection counts call sites; folding inheritance into either would change what those two words
83
+ * mean rather than making them more complete.
84
+ *
85
+ * @implements A-SPEC-304
86
+ * A `references` relation (a type-position use of a contract) was built, wired in here, and
87
+ * measured: 586 correct edges on this repository, and EVERY replay metric identical with the
88
+ * relation on and off — including the impact-reach count, 26 truth files either way. The reason is
89
+ * now understood and is not about the edges: the replay enters from a REQUEST STRING, so impact is
90
+ * never seeded from contract nodes at all. What this feature improves, nothing measures yet.
91
+ */
92
+ impactSourcesOf(id: string): string[];
93
+ /**
94
+ * @implements A-SPEC-309
95
+ * Both directions of impact: who reaches this symbol, and what this symbol reaches.
96
+ *
97
+ * Goal Phase 5 asks for upstream callers AND downstream dependencies, and only the upstream half
98
+ * existed. Changing a function usually means changing what it calls, so the callee side is not a
99
+ * refinement — it is half the question.
100
+ *
101
+ * Kept as its own name rather than widening `impactSourcesOf`, for the same reason A-SPEC-288 split
102
+ * `impactSourcesOf` off `callersOf`: once one function answers two questions, both get blurred.
103
+ */
104
+ impactNeighborsOf(id: string, opts?: {
105
+ /**
106
+ * @implements A-SPEC-311
107
+ * Drop callees with more callers than this. A function everything calls is a shared tool, not a
108
+ * thing that changes when you change one of its callers — changing a logger does not change
109
+ * every caller, and calling a logger does not change the logger.
110
+ *
111
+ * The same number means different things in the two directions, which is why the upstream side
112
+ * applies it past depth 1 and this applies at depth 1: upstream, a high in-degree node is one
113
+ * many things depend ON and the damping stops the walk spreading THROUGH it; downstream, a high
114
+ * in-degree node is a utility and is not a signal at all.
115
+ *
116
+ * Optional, and off by default: silently filtering would change what this method means for
117
+ * every other caller of it.
118
+ */
119
+ maxCalleeInDegree?: number;
120
+ }): string[];
121
+ /**
122
+ * @implements A-SPEC-289
123
+ * Repo-relative paths of the files that import this one, via resolved FILE->FILE `imports` edges.
124
+ * A coarser dependency than a call, and the only one that reaches a file which depends on another
125
+ * without calling any symbol the resolver could pin down.
126
+ */
127
+ importersOf(sourcePath: string): string[];
128
+ /**
129
+ * @implements A-SPEC-293
130
+ * The decisions constraining any of the given node ids (`FILE:<path>` / `SPEC:<id>`), by ADR id.
131
+ * What a change must not break, rather than what it touches.
132
+ */
133
+ decisionsConstraining(nodeIds: readonly string[]): string[];
48
134
  /** How many distinct symbols call this one. The hub signal for bounded propagation (REQ-127). */
49
135
  callerCount(id: string): number;
50
136
  /**
@@ -82,6 +168,35 @@ export declare class RtmGraph {
82
168
  allCodeNodeIds(): string[];
83
169
  /** @implements A-SPEC-138 — direct calls-only callees of a node (one hop), for BFS path assembly. */
84
170
  callees(fromId: string): string[];
171
+ /**
172
+ * @implements A-SPEC-280
173
+ * A canonical, order-independent dump of the whole graph: sorted nodes then sorted edges. Exists
174
+ * so two graphs built by different routes (a full rebuild vs. an incremental update) can be
175
+ * compared as VALUES rather than by spot-checking a few lookups — the only way the convergence
176
+ * property Goal completion criterion 5 asks for can actually be evidence.
177
+ */
178
+ dumpCanonical(): string;
179
+ /**
180
+ * @implements A-SPEC-280
181
+ * Drop every edge of one relation. Needed because call resolution is GLOBAL: whether `B.two`
182
+ * resolves depends on how many files define it across the whole scan, so a file-local update
183
+ * cannot repair a call edge owned by a file that did not change. Clearing the relation and
184
+ * re-resolving is the only way an incremental update lands on the full-rebuild answer.
185
+ */
186
+ removeEdgesByRel(rel: string): void;
187
+ /** @implements A-SPEC-282 — every stored meta key/value. */
188
+ readMeta(): Record<string, string>;
189
+ /** @implements A-SPEC-282 — replace the stored meta wholesale, so a stale key cannot survive. */
190
+ writeMeta(meta: Record<string, string>): void;
191
+ /**
192
+ * @implements A-SPEC-282
193
+ * Empty the graph, keeping the file. Used when a stored graph's basis no longer holds: the caller
194
+ * has just been told it cannot trust these facts, so leaving any of them behind to be built on top
195
+ * of would be worse than having had no cache at all.
196
+ */
197
+ clear(): void;
198
+ /** @implements A-SPEC-283 — the journal mode actually in force, so the pragma can be asserted. */
199
+ journalMode(): string;
85
200
  nodeCount(): number;
86
201
  edgeCount(): number;
87
202
  close(): void;