@holmes-lab/holmes-kit 0.19.6 → 0.20.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 (128) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +8 -1
  3. package/dist/.build-id +1 -1
  4. package/dist/holmes/cli/approve-status.d.ts +24 -0
  5. package/dist/holmes/cli/approve-status.js +166 -0
  6. package/dist/holmes/cli/approve-watch.d.ts +6 -0
  7. package/dist/holmes/cli/approve-watch.js +6 -0
  8. package/dist/holmes/cli/approve.d.ts +27 -0
  9. package/dist/holmes/cli/approve.js +68 -7
  10. package/dist/holmes/cli/cli-execution.d.ts +2 -0
  11. package/dist/holmes/cli/cli-execution.js +24 -0
  12. package/dist/holmes/cli/gitignore-merge.d.ts +0 -5
  13. package/dist/holmes/cli/gitignore-merge.js +6 -2
  14. package/dist/holmes/cli/index.js +176 -139
  15. package/dist/holmes/cli/playbook-skills.d.ts +2 -22
  16. package/dist/holmes/cli/playbook-skills.js +10 -54
  17. package/dist/holmes/governance/approval-grants.d.ts +22 -0
  18. package/dist/holmes/governance/approval-grants.js +55 -1
  19. package/dist/holmes/governance/approval-queue.d.ts +43 -4
  20. package/dist/holmes/governance/approval-queue.js +86 -12
  21. package/dist/holmes/governance/display-id-aliases.d.ts +29 -0
  22. package/dist/holmes/governance/display-id-aliases.js +86 -0
  23. package/dist/holmes/governance/ledger-store.d.ts +42 -14
  24. package/dist/holmes/governance/ledger-store.js +49 -3
  25. package/dist/holmes/governance/ledger-timeline.d.ts +3 -1
  26. package/dist/holmes/governance/ledger-timeline.js +11 -1
  27. package/dist/holmes/governance/provenance-chain.d.ts +2 -0
  28. package/dist/holmes/governance/provenance-chain.js +13 -2
  29. package/dist/holmes/governance/provenance-ledger.d.ts +28 -0
  30. package/dist/holmes/governance/provenance-ledger.js +69 -16
  31. package/dist/holmes/governance/session-context.d.ts +3 -0
  32. package/dist/holmes/governance/session-context.js +26 -8
  33. package/dist/holmes/guardrail/risk-classifier.js +2 -2
  34. package/dist/holmes/guardrail/write-target.js +2 -1
  35. package/dist/holmes/hooks/pre-tool-use.js +12 -1
  36. package/dist/holmes/hooks/stop.js +14 -3
  37. package/dist/holmes/mcp/handlers/adoption.d.ts +118 -0
  38. package/dist/holmes/mcp/handlers/adoption.js +186 -0
  39. package/dist/holmes/mcp/handlers/entity-integration.d.ts +100 -0
  40. package/dist/holmes/mcp/handlers/entity-integration.js +324 -0
  41. package/dist/holmes/mcp/handlers/entity-renumber.d.ts +137 -0
  42. package/dist/holmes/mcp/handlers/entity-renumber.js +228 -0
  43. package/dist/holmes/mcp/handlers/entity-store.d.ts +78 -0
  44. package/dist/holmes/mcp/handlers/entity-store.js +212 -0
  45. package/dist/holmes/mcp/handlers/graph-operations.d.ts +90 -0
  46. package/dist/holmes/mcp/handlers/graph-operations.js +260 -0
  47. package/dist/holmes/mcp/handlers/maintenance-analysis.d.ts +34 -0
  48. package/dist/holmes/mcp/handlers/maintenance-analysis.js +288 -0
  49. package/dist/holmes/mcp/handlers/maintenance-evidence.d.ts +40 -0
  50. package/dist/holmes/mcp/handlers/maintenance-evidence.js +104 -0
  51. package/dist/holmes/mcp/handlers/operator-inspection.d.ts +167 -0
  52. package/dist/holmes/mcp/handlers/operator-inspection.js +284 -0
  53. package/dist/holmes/mcp/handlers/phase-queries.d.ts +34 -0
  54. package/dist/holmes/mcp/handlers/phase-queries.js +86 -0
  55. package/dist/holmes/mcp/handlers/review-evidence.d.ts +47 -0
  56. package/dist/holmes/mcp/handlers/review-evidence.js +370 -0
  57. package/dist/holmes/mcp/handlers/review-queries.d.ts +68 -0
  58. package/dist/holmes/mcp/handlers/review-queries.js +79 -0
  59. package/dist/holmes/mcp/handlers/risk-assessment.d.ts +25 -0
  60. package/dist/holmes/mcp/handlers/risk-assessment.js +181 -0
  61. package/dist/holmes/mcp/handlers/slice-orchestration.d.ts +55 -0
  62. package/dist/holmes/mcp/handlers/slice-orchestration.js +324 -0
  63. package/dist/holmes/mcp/handlers/spec-approval.d.ts +105 -0
  64. package/dist/holmes/mcp/handlers/spec-approval.js +504 -0
  65. package/dist/holmes/mcp/handlers/spec-authoring.d.ts +57 -0
  66. package/dist/holmes/mcp/handlers/spec-authoring.js +326 -0
  67. package/dist/holmes/mcp/handlers/spec-lifecycle.d.ts +122 -0
  68. package/dist/holmes/mcp/handlers/spec-lifecycle.js +339 -0
  69. package/dist/holmes/mcp/handlers/spec-queries.d.ts +30 -0
  70. package/dist/holmes/mcp/handlers/spec-queries.js +74 -0
  71. package/dist/holmes/mcp/handlers/taint-screening.d.ts +48 -0
  72. package/dist/holmes/mcp/handlers/taint-screening.js +183 -0
  73. package/dist/holmes/mcp/handlers/test-execution.d.ts +63 -0
  74. package/dist/holmes/mcp/handlers/test-execution.js +210 -0
  75. package/dist/holmes/mcp/handlers/workspace-identity.d.ts +30 -0
  76. package/dist/holmes/mcp/handlers/workspace-identity.js +68 -0
  77. package/dist/holmes/mcp/handlers/workspace-queries.d.ts +77 -0
  78. package/dist/holmes/mcp/handlers/workspace-queries.js +193 -0
  79. package/dist/holmes/mcp/handlers.d.ts +653 -424
  80. package/dist/holmes/mcp/handlers.js +267 -3250
  81. package/dist/holmes/mcp/server.js +2 -1
  82. package/dist/holmes/mcp/tool-schemas.js +43 -2
  83. package/dist/holmes/project/execution-context.d.ts +17 -0
  84. package/dist/holmes/project/execution-context.js +120 -0
  85. package/dist/holmes/project/installer-markers.d.ts +34 -0
  86. package/dist/holmes/project/installer-markers.js +65 -0
  87. package/dist/holmes/project/root.d.ts +3 -1
  88. package/dist/holmes/project/root.js +4 -3
  89. package/dist/holmes/project/workspace-identity.d.ts +29 -0
  90. package/dist/holmes/project/workspace-identity.js +181 -0
  91. package/dist/holmes/review/test-outcomes.d.ts +6 -1
  92. package/dist/holmes/review/test-outcomes.js +23 -5
  93. package/dist/holmes/review/test-runner.d.ts +18 -0
  94. package/dist/holmes/review/test-runner.js +132 -4
  95. package/dist/holmes/rtm/graph-store.d.ts +50 -1
  96. package/dist/holmes/rtm/graph-store.js +244 -3
  97. package/dist/holmes/rtm/incremental.d.ts +1 -0
  98. package/dist/holmes/rtm/incremental.js +12 -3
  99. package/dist/holmes/rtm/localize.js +7 -0
  100. package/dist/holmes/rtm/rtm-graph.d.ts +9 -0
  101. package/dist/holmes/rtm/rtm-graph.js +13 -0
  102. package/dist/holmes/semantic/vector-cache.d.ts +16 -1
  103. package/dist/holmes/semantic/vector-cache.js +155 -19
  104. package/dist/holmes/spec/approval-status.d.ts +10 -0
  105. package/dist/holmes/spec/approval-status.js +7 -3
  106. package/dist/holmes/spec/entity-git-snapshot.d.ts +50 -0
  107. package/dist/holmes/spec/entity-git-snapshot.js +276 -0
  108. package/dist/holmes/spec/entity-integration-discharge.d.ts +73 -0
  109. package/dist/holmes/spec/entity-integration-discharge.js +233 -0
  110. package/dist/holmes/spec/entity-integration-transaction.d.ts +179 -0
  111. package/dist/holmes/spec/entity-integration-transaction.js +1221 -0
  112. package/dist/holmes/spec/entity-integration.d.ts +207 -0
  113. package/dist/holmes/spec/entity-integration.js +747 -0
  114. package/dist/holmes/spec/entity-renumber-transaction.d.ts +82 -0
  115. package/dist/holmes/spec/entity-renumber-transaction.js +711 -0
  116. package/dist/holmes/spec/entity-renumber.d.ts +32 -0
  117. package/dist/holmes/spec/entity-renumber.js +156 -0
  118. package/dist/holmes/spec/entity-store.d.ts +135 -0
  119. package/dist/holmes/spec/entity-store.js +1051 -0
  120. package/dist/holmes/spec/entity-transaction.d.ts +89 -0
  121. package/dist/holmes/spec/entity-transaction.js +701 -0
  122. package/dist/holmes/spec/renumber.d.ts +58 -0
  123. package/dist/holmes/spec/renumber.js +200 -1
  124. package/dist/holmes/spec/spec-store.d.ts +3 -2
  125. package/dist/holmes/spec/spec-store.js +23 -2
  126. package/dist/holmes/spec/transition-policy.d.ts +36 -0
  127. package/dist/holmes/spec/transition-policy.js +135 -0
  128. package/package.json +1 -1
@@ -0,0 +1,260 @@
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.publishCurrentGraph = publishCurrentGraph;
37
+ exports.warmSemanticCache = warmSemanticCache;
38
+ exports.createGraphOperationsHandlers = createGraphOperationsHandlers;
39
+ // @implements A-SPEC-614, A-SPEC-613, A-SPEC-100.2, A-SPEC-121.2, A-SPEC-128, A-SPEC-139, A-SPEC-189, A-SPEC-280, A-SPEC-282, A-SPEC-283, A-SPEC-419, A-SPEC-433, A-SPEC-469, A-SPEC-478, A-SPEC-568.1, A-SPEC-568.2, A-SPEC-569.1, A-SPEC-569.2, A-SPEC-569.3, A-SPEC-589
40
+ // @implements A-SPEC-632
41
+ const fs = __importStar(require("node:fs"));
42
+ const path = __importStar(require("node:path"));
43
+ const hash_cache_1 = require("../../cpg/hash-cache");
44
+ const root_1 = require("../../project/root");
45
+ const rtm_builder_1 = require("../../rtm/rtm-builder");
46
+ const graph_store_1 = require("../../rtm/graph-store");
47
+ const assoc_arm_1 = require("../../assoc/assoc-arm");
48
+ // @implements A-SPEC-478 — the semantic tier runtime stays at the async reindex edge.
49
+ const tier_1 = require("../../semantic/tier");
50
+ const vector_cache_1 = require("../../semantic/vector-cache");
51
+ const embedder_1 = require("../../semantic/embedder");
52
+ // @implements A-SPEC-283 — bumped whenever the graph's node/edge shape changes, so a store written
53
+ // by an older build is rebuilt rather than read with new assumptions.
54
+ // @implements A-SPEC-568.1 — /3: nodes gained the intent `summary` column.
55
+ const RTM_GRAPH_SCHEMA = 'rtm-graph/3';
56
+ // @implements A-SPEC-569.3 — how many impacted specs get their intent sentence attached. A prose
57
+ // constant, never a verdict input: the impacted list itself is never truncated.
58
+ const SUMMARY_CAP = 40;
59
+ // @implements A-SPEC-569.1 (revision) — /2: pre-fix 0.16.0 builds could persist forged structural
60
+ // characters in the summary column, and every other basis field would still match after upgrading.
61
+ // A-SPEC-283's own rule applies to us too: an older build's artifact is rebuilt, never read with
62
+ // new assumptions.
63
+ const RTM_EXTRACTOR_VERSION = 'holmes-rtm/2';
64
+ // @implements A-SPEC-632 — the two rebuild paths a completed integration's discharge runs are the
65
+ // SAME functions explicit reindex runs, so a discharged graph and a reindexed graph are one artifact.
66
+ /** Publish the complete graph for `specs` over `scanned` at the live path (A-SPEC-631 primitive). */
67
+ function publishCurrentGraph(root, specs, scanned) {
68
+ // The publication basis: the same five fields rtm_impact expects (A-SPEC-283), so what this
69
+ // publishes is exactly what the next query reuses.
70
+ const basis = {
71
+ graphSchema: RTM_GRAPH_SCHEMA,
72
+ extractorVersion: RTM_EXTRACTOR_VERSION,
73
+ sourceCommit: (0, graph_store_1.headCommitOf)(root, (0, root_1.cleanSubprocessEnv)()) ?? '',
74
+ specFingerprint: (0, graph_store_1.specFingerprint)(specs),
75
+ scanDigest: (0, graph_store_1.scanDigest)(scanned),
76
+ };
77
+ const { nodes, edges } = (0, graph_store_1.publishGraph)(path.join(root, '.ax', 'rtm.sqlite'), basis, (g) => (0, rtm_builder_1.buildRtm)(specs, scanned, g));
78
+ return { basis, nodes, edges };
79
+ }
80
+ /**
81
+ * @implements A-SPEC-478 — the semantic cache WARMING lives in explicit heavy operations, so the
82
+ * ranking hot path only ever LOOKS UP vectors. Idempotent through the cache. The outcome says
83
+ * whether there was nothing to do (tier none), whether it happened, or whether a configured tier
84
+ * could not embed — a discharge must not record the last one as done.
85
+ */
86
+ async function warmSemanticCache(root, scanned) {
87
+ const tier = (0, tier_1.resolveSemanticTier)();
88
+ if (tier.tier === 'none')
89
+ return { status: 'not-applicable', tier: 'none' };
90
+ const configured = tier.model ? `${tier.tier}:${tier.model}` : tier.tier;
91
+ const rt = (0, embedder_1.makeTierEmbedder)(tier, new vector_cache_1.VectorCache(root));
92
+ if (rt === null)
93
+ return { status: 'unavailable', tier: configured };
94
+ const docTexts = scanned.map((f) => (f.sourcePath + ' ' + f.symbols.map((sy) => sy.qualifiedName).join(' ')).slice(0, 2000));
95
+ const w = await rt.warm(docTexts);
96
+ if (docTexts.length > 0 && w.computed + w.cached === 0)
97
+ return { status: 'unavailable', tier: rt.label };
98
+ return { status: 'discharged', tier: rt.label, computed: w.computed, cached: w.cached };
99
+ }
100
+ function createGraphOperationsHandlers(context) {
101
+ return {
102
+ async rtm_impact(a) {
103
+ context.assertStoreReachable('rtm_impact', a.root); // @implements A-SPEC-433
104
+ // @implements A-SPEC-614 — do not scan another workspace with this bound spec store.
105
+ const foreign = context.foreignRootReason(a.root);
106
+ if (foreign)
107
+ throw context.refusal(foreign);
108
+ // @implements A-SPEC-189 §14 (round 13) — `context.projectRootOf(a.root)` was called as a bare
109
+ // statement: its only effect was to throw on a bad path, and the advertised contract ("a
110
+ // subdirectory resolves up to it") was dropped on the floor. `taint_scan` one function away
111
+ // honours it, so the same argument answered about two different trees depending on which tool
112
+ // was asked. Bind the derivation and use it.
113
+ const root = context.projectRootOf(a.root);
114
+ const scanned = context.cachedScan(root);
115
+ // @implements A-SPEC-569.2 — the impact/advisory graph is APPROVED-ONLY. A draft needs no
116
+ // approval to exist, and the 0.16.0 adversarial round showed one reaching the agent-visible
117
+ // channels (impacted closure, advisory anchor summaries) — the trust boundary for those
118
+ // channels is the act of approval. NOT filterGoverned: that predicate passes drafts (it only
119
+ // drops outdated/legacy), which is exactly what let this in. Diagnosis (rtm_check) and
120
+ // matching (issue_localize / maintenance_analyze) keep their own populations — the replay
121
+ // pins were measured on them.
122
+ const specs = (await context.listSpecs()).filter((s) => s.status === 'approved');
123
+ // @implements A-SPEC-283
124
+ // Reuse the persisted graph when its basis still holds. Measured: on the warm path the graph
125
+ // build is ~81% of the cost and reopening is ~0ms. `scanDigest` is the field that makes this
126
+ // safe — an uncommitted edit never moves HEAD, so a commit-only basis would happily serve a
127
+ // graph describing content the working tree no longer has.
128
+ const expected = {
129
+ graphSchema: RTM_GRAPH_SCHEMA,
130
+ extractorVersion: RTM_EXTRACTOR_VERSION,
131
+ sourceCommit: (0, graph_store_1.headCommitOf)(root, (0, root_1.cleanSubprocessEnv)()) ?? '',
132
+ specFingerprint: (0, graph_store_1.specFingerprint)(specs),
133
+ scanDigest: (0, graph_store_1.scanDigest)(scanned),
134
+ };
135
+ // @implements A-SPEC-631 — the live file is NEVER cleared in place. `publishable`: reuse it
136
+ // while every basis field holds; otherwise build the complete graph in a private temporary
137
+ // database, answer from it, and publish it by one atomic rename only after the answer stands.
138
+ // Reproduced 2026-09-13: the previous in-place clear+rebuild (this is also the Stop hook's
139
+ // refresh path) let a concurrent approval read zero nodes as "no impact", and a refresher
140
+ // killed mid-way left an empty, basis-less file behind for every later reader.
141
+ const opened = (0, graph_store_1.openReusableGraph)(path.join(root, '.ax', 'rtm.sqlite'), expected, { publishable: true });
142
+ const g = opened.graph;
143
+ let answered = false;
144
+ try {
145
+ if (!opened.reusable) {
146
+ (0, rtm_builder_1.buildRtm)(specs, scanned, g);
147
+ opened.commitBasis();
148
+ }
149
+ // explainImpact, not impactedBy: the bounds and the breadth signal must reach the caller.
150
+ // An impact set is not just a list — a broad one means "review the contract", and a consumer
151
+ // that cannot tell the difference will try to bundle two hundred call sites.
152
+ const { specs: impactedRaw, reachedByDepth, stoppedAt, seedIsHub } = (0, rtm_builder_1.explainImpact)(g, a.changed);
153
+ // @implements A-SPEC-569.2 — the closure walks EDGES, and an implements edge is owned by
154
+ // the code file, so an anchor naming a draft (or a spec nobody wrote) still emits one —
155
+ // deliberately, for rtm_check's dangling diagnosis. The CHANNEL filter is node existence:
156
+ // approved-only specs were given nodes above, so only sealed intent reaches the caller.
157
+ const impacted = impactedRaw.filter((id) => g.hasNode(id));
158
+ // @implements A-SPEC-469 — the graded FILE surface beside the spec closure, same code path
159
+ // as the S-484 measurement (identity, not reimplementation). Seeds are the changed symbols'
160
+ // nodes; the files that own them are excluded — a prediction naming the change itself is
161
+ // vacuously true. The closure fields above stay exactly as they were: mass never enters a
162
+ // gate decision.
163
+ const riSeeds = new Map(a.changed.flatMap((qn) => g.codeNodeIds(qn)).map((id) => [id, 1]));
164
+ const riExclude = new Set([...riSeeds.keys()]
165
+ .map((id) => (id.includes('@') ? id.slice(id.lastIndexOf('@') + 1) : ''))
166
+ .filter((f) => f !== ''));
167
+ const rankedImpact = (0, assoc_arm_1.pprImpactRanked)((0, assoc_arm_1.graphViewOf)(g.dumpCanonical()), riSeeds, riExclude, assoc_arm_1.RANKED_IMPACT_K, assoc_arm_1.RANKED_IMPACT_CONFIG);
168
+ // @implements A-SPEC-568.2 — the intent sentence beside every impacted spec id, same order
169
+ // as `impacted` (which stays a bare id list for its existing consumers). Information only:
170
+ // nothing reads it back into the walk, the ranking or any gate.
171
+ // @implements A-SPEC-569.3 — capped: measured on this repository, uncapped summaries were
172
+ // 94% of a 104,706-byte response (a hub-grade impact of 339 specs). The omission is COUNTED,
173
+ // never silent, and `impacted` itself stays complete — only the annotation is bounded.
174
+ const shownSummaries = impacted.slice(0, SUMMARY_CAP);
175
+ const impactedSummaries = shownSummaries.map((id) => {
176
+ let summary = null;
177
+ try {
178
+ summary = g.summaryOf(id);
179
+ }
180
+ catch { /* summary stays null */ }
181
+ return { id, summary };
182
+ });
183
+ const summariesOmitted = impacted.length - shownSummaries.length;
184
+ answered = true;
185
+ return {
186
+ impacted,
187
+ impactedSummaries,
188
+ ...(summariesOmitted > 0 ? { summariesOmitted } : {}),
189
+ rankedImpact,
190
+ reachedByDepth,
191
+ bounded: stoppedAt.length > 0 ? stoppedAt.slice(0, 20) : undefined,
192
+ ...(seedIsHub ? {
193
+ breadthWarning: `a changed symbol has ${seedIsHub.callers} callers (hub threshold ${seedIsHub.threshold}) — this is a CONTRACT change; review the contract and its tests rather than bundling every impacted spec`,
194
+ } : {}),
195
+ };
196
+ }
197
+ finally {
198
+ // Release the native SQLite handle even if build/query throws. A failed answer discards a
199
+ // private rebuild; a successful one publishes it — and a publication failure never
200
+ // overturns the answer, it only means the cache was not refreshed this time.
201
+ if (!answered)
202
+ opened.discard();
203
+ else {
204
+ try {
205
+ opened.publish();
206
+ }
207
+ catch { /* answer stands; live graph unchanged */ }
208
+ }
209
+ }
210
+ },
211
+ async rtm_reindex(a) {
212
+ context.assertStoreReachable('rtm_reindex', a.root); // @implements A-SPEC-419
213
+ // Removal/re-derivation of changedFiles/changedSymbols is not needed
214
+ // here (rtm_reindex works off changes/specs/scanned/bySourcePath
215
+ // directly), but the guard+diff+scan prologue is identical to
216
+ // review_scope/review_prepare — see deriveChangedContext.
217
+ const { root, changes, specs, scanned, changeSource } = await context.deriveChangedContext(a.root, a, 'rtm_reindex');
218
+ const changed = changes.added.length + changes.modified.length + changes.deleted.length + changes.renamed.length;
219
+ // The HashCache records every changed file's content hash (A-SPEC-128 — project-root cache,
220
+ // no subdir leak) so an incremental consumer can skip files whose content did not move. The
221
+ // reindex itself publishes the FULL build below and no longer applies the diff on top of it.
222
+ const cache = new hash_cache_1.HashCache(context.cacheDirFor(root)); // @implements A-SPEC-128
223
+ const record = (files) => {
224
+ for (const relPath of files) {
225
+ let content;
226
+ try {
227
+ content = fs.readFileSync(path.join(root, relPath), 'utf8');
228
+ }
229
+ catch {
230
+ continue;
231
+ }
232
+ if (!cache.unchanged(relPath, content))
233
+ cache.put(relPath, content);
234
+ }
235
+ };
236
+ record(changes.added);
237
+ record(changes.modified);
238
+ // @implements A-SPEC-631 — explicit reindex is a persistence operation: build the COMPLETE
239
+ // graph in a private temporary database, commit the matching basis, close it, and replace
240
+ // the live file by one atomic rename (A-SPEC-589's publication contract, now the same
241
+ // primitive rtm_impact uses). The post-build applyIncremental pass is gone: measured
242
+ // 2026-09-13, its per-file replacement rebuilt symbols but not the changed file's FILE node
243
+ // or imports edge, so the first reindex after a change published 4 nodes / 3 edges where the
244
+ // next run over the same tree published 5 / 4. The full build re-resolves every call edge
245
+ // against the whole scan, which is what A-SPEC-280 required (@implements A-SPEC-280).
246
+ const published = publishCurrentGraph(root, specs, scanned);
247
+ // @implements A-SPEC-478 — warming is best-effort here: reindex's own result is unaffected,
248
+ // and only an actual warm is reported (tier none or an unavailable tier adds no field).
249
+ let semanticWarm;
250
+ try {
251
+ const w = await warmSemanticCache(root, scanned);
252
+ if (w.status === 'discharged')
253
+ semanticWarm = { tier: w.tier, computed: w.computed, cached: w.cached };
254
+ }
255
+ catch { /* warming is best-effort; reindex's own result is unaffected */ }
256
+ return { changed, nodes: published.nodes, edges: published.edges, changeSource,
257
+ ...(semanticWarm !== undefined ? { semanticWarm } : {}) };
258
+ },
259
+ };
260
+ }
@@ -0,0 +1,34 @@
1
+ import { Spec } from '../../spec/spec-parser';
2
+ import { ScannedFile } from '../../cpg/cpg-scanner';
3
+ import { DecisionRecord, DecisionCitation } from '../../rtm/rtm-builder';
4
+ import { ContentSource } from '../../context/bundler';
5
+ import { MaintenanceGroundTruth, MaintenanceAnalysis } from '../maintenance-analyze';
6
+ import { Basis } from '../basis';
7
+ export interface MaintenanceAnalysisContext {
8
+ listSpecs(): Promise<Spec[]>;
9
+ foreignRootReason(root?: string): string | null;
10
+ refusal(reason: string): Error;
11
+ projectRootOf(root: string): string;
12
+ basisFor(root: string, withDisk: boolean): Basis;
13
+ collectDecisions(root: string, scanned: readonly ScannedFile[], specs: readonly Spec[]): {
14
+ decisions: DecisionRecord[];
15
+ citations: DecisionCitation[];
16
+ };
17
+ buildContentSource(specs: Spec[], scanned: ScannedFile[]): ContentSource;
18
+ specStoreBlindReason(root: string, count: number): string | null;
19
+ fileDigestOf(root: string, rel: string): string | null;
20
+ }
21
+ export declare function createMaintenanceAnalysisHandlers(context: MaintenanceAnalysisContext): {
22
+ maintenance_analyze(a: {
23
+ root: string;
24
+ request: string;
25
+ historyLimit?: number;
26
+ contextBudget?: number;
27
+ groundTruth?: MaintenanceGroundTruth;
28
+ persist?: boolean;
29
+ changedFiles?: string[];
30
+ }): Promise<MaintenanceAnalysis & {
31
+ persistedTo?: string;
32
+ bounded?: boolean;
33
+ }>;
34
+ };
@@ -0,0 +1,288 @@
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.createMaintenanceAnalysisHandlers = createMaintenanceAnalysisHandlers;
37
+ // @implements A-SPEC-612, A-SPEC-100.2, A-SPEC-128, A-SPEC-189, A-SPEC-267, A-SPEC-268, A-SPEC-269, A-SPEC-277, A-SPEC-283, A-SPEC-290, A-SPEC-292, A-SPEC-293, A-SPEC-378, A-SPEC-388, A-SPEC-418, A-SPEC-478, A-SPEC-571.2, A-SPEC-573.4, A-SPEC-578.5
38
+ const fs = __importStar(require("node:fs"));
39
+ const path = __importStar(require("node:path"));
40
+ const node_child_process_1 = require("node:child_process");
41
+ const spec_types_1 = require("../../spec/spec-types");
42
+ const cpg_scanner_1 = require("../../cpg/cpg-scanner");
43
+ const rtm_graph_1 = require("../../rtm/rtm-graph");
44
+ const rtm_builder_1 = require("../../rtm/rtm-builder");
45
+ const test_scope_1 = require("../../rtm/test-scope");
46
+ const root_1 = require("../../project/root");
47
+ const test_evidence_1 = require("../../review/test-evidence");
48
+ const temporal_prior_1 = require("../../review/temporal-prior");
49
+ const commit_text_1 = require("../../review/commit-text");
50
+ const bundler_1 = require("../../context/bundler");
51
+ const tier_1 = require("../../semantic/tier");
52
+ const vector_cache_1 = require("../../semantic/vector-cache");
53
+ const embedder_1 = require("../../semantic/embedder");
54
+ const maintenance_analyze_1 = require("../maintenance-analyze");
55
+ const maintenance_evidence_1 = require("../maintenance-evidence");
56
+ const defuse_bound_1 = require("../defuse-bound");
57
+ function createMaintenanceAnalysisHandlers(context) {
58
+ return {
59
+ async maintenance_analyze(a) {
60
+ const foreign = context.foreignRootReason(a.root);
61
+ if (foreign)
62
+ throw context.refusal(foreign);
63
+ const root = context.projectRootOf(a.root);
64
+ // This tool is advertised as read-only. Do not attach ScanFileCache: its normal save/prune
65
+ // behavior mutates `.ax/cpg_cache` even though the analysis itself is observational.
66
+ const scanner = new cpg_scanner_1.CpgScanner();
67
+ const scanned = scanner.scan(root, root);
68
+ const skipped = scanner.scanSkipped();
69
+ const specs = (0, spec_types_1.filterGoverned)(await context.listSpecs());
70
+ const anchors = (0, test_scope_1.scanTestAnchors)(root);
71
+ const detail = context.basisFor(root, true);
72
+ let head = '';
73
+ try {
74
+ head = (0, node_child_process_1.execFileSync)('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf8', env: (0, root_1.cleanSubprocessEnv)(), stdio: ['ignore', 'pipe', 'pipe'] }).trim();
75
+ }
76
+ catch { /* non-git */ }
77
+ const supportedExtensions = new Set(cpg_scanner_1.SCANNABLE_EXTENSIONS);
78
+ const knownCodeExtensions = new Set(['.rb', '.php', '.kt', '.kts', '.swift', '.scala', '.sh', '.ex', '.exs', '.lua', '.r']);
79
+ const unsupportedLanguages = new Set();
80
+ try {
81
+ const files = (0, node_child_process_1.execFileSync)('git', ['-C', root, 'ls-files', '-z', '--cached', '--others', '--exclude-standard'], { encoding: 'utf8', env: (0, root_1.cleanSubprocessEnv)(), maxBuffer: 8 * 1024 * 1024 }).split('\0');
82
+ for (const file of files) {
83
+ const ext = path.extname(file).toLowerCase();
84
+ if (knownCodeExtensions.has(ext) && !supportedExtensions.has(ext))
85
+ unsupportedLanguages.add(ext);
86
+ }
87
+ }
88
+ catch {
89
+ // A non-git tree cannot be enumerated cheaply without reimplementing the scanner's ignore
90
+ // rules. Surface the missing axis rather than claiming every language was covered.
91
+ unsupportedLanguages.add('not-enumerated(non-git)');
92
+ }
93
+ const graph = new rtm_graph_1.RtmGraph();
94
+ try {
95
+ // @implements A-SPEC-292 — the resolution loop's own tally of what it declined to link.
96
+ const { resolution } = (0, rtm_builder_1.buildRtm)(specs, scanned, graph);
97
+ // @implements A-SPEC-293 — what constrains this change, not just what it touches.
98
+ const { decisions, citations } = context.collectDecisions(root, scanned, specs);
99
+ (0, rtm_builder_1.addDecisionEdges)(decisions, citations, graph);
100
+ const common = {
101
+ request: a.request,
102
+ scanned,
103
+ specs,
104
+ graph,
105
+ testAnchors: anchors,
106
+ basis: {
107
+ head,
108
+ loadedBuild: detail.loadedBuild,
109
+ diskBuild: detail.diskBuild ?? 'unknown',
110
+ specFingerprint: `count:${specs.length}/fp:${detail.specs ?? 'unknown'}`,
111
+ },
112
+ changedFiles: a.changedFiles,
113
+ // @implements A-SPEC-290 — the stored run record. Freshness is judged inside the analysis
114
+ // against its own basis, so a record from another commit is reported as stale rather than
115
+ // quietly used.
116
+ runtimeEvidence: (0, test_evidence_1.readTestEvidence)(root),
117
+ resolution,
118
+ coverage: {
119
+ scannedFiles: scanned.length,
120
+ skippedFiles: skipped.map((entry) => entry.file),
121
+ unsupportedLanguages: [...unsupportedLanguages],
122
+ historyStatus: 'not-requested',
123
+ },
124
+ };
125
+ // @implements A-SPEC-478 — resolve the tier and precompute the query vector at the
126
+ // async edge; the sync analysis receives only a closure over cached vectors. Any failure
127
+ // (no tier, no module, no key, embed error) leaves `semantic` undefined = today's behavior.
128
+ let semantic;
129
+ try {
130
+ const rt = (0, embedder_1.makeTierEmbedder)((0, tier_1.resolveSemanticTier)(), new vector_cache_1.VectorCache(root));
131
+ if (rt !== null) {
132
+ const qv = await rt.embedQuery(a.request);
133
+ semantic = { label: rt.label, queryVector: qv, cachedDocVector: rt.cachedDocVector };
134
+ }
135
+ }
136
+ catch { /* the hot path never breaks on the semantic layer */ }
137
+ const preliminary = (0, maintenance_analyze_1.analyzeMaintenance)({ ...common, history: [], semantic });
138
+ const candidateFiles = preliminary.candidates.map((candidate) => candidate.file);
139
+ const limit = Math.max(0, Math.min(100, Math.floor(a.historyLimit ?? 20)));
140
+ const history = [];
141
+ let historyStatus = limit === 0 || candidateFiles.length === 0 ? 'not-requested' : 'unavailable';
142
+ if (limit > 0 && candidateFiles.length > 0) {
143
+ try {
144
+ const raw = (0, node_child_process_1.execFileSync)('git', ['-C', root, 'log', `-${limit}`, '--date=short', '--format=%H%x00%ad%x00%s', '--name-only', '--', ...candidateFiles], { encoding: 'utf8', env: (0, root_1.cleanSubprocessEnv)(), maxBuffer: 2 * 1024 * 1024 });
145
+ let current = null;
146
+ for (const line of raw.split('\n')) {
147
+ if (line.includes('\0')) {
148
+ const [commit, date, subject] = line.split('\0');
149
+ current = { commit, date, subject, files: [] };
150
+ history.push(current);
151
+ }
152
+ else if (current && line.trim() !== '') {
153
+ // git C-quotes any non-ASCII pathname here (core.quotepath). Left raw, it never
154
+ // matched a candidate file, so the rationale came back empty while historyStatus
155
+ // still reported `available` — a silent false negative with confident provenance.
156
+ current.files.push((0, maintenance_analyze_1.unquoteGitPath)(line.trim()));
157
+ }
158
+ }
159
+ historyStatus = 'available';
160
+ }
161
+ catch { /* history is evidence enrichment, not a prerequisite */ }
162
+ }
163
+ // Preserve localization rank for context: `impacts.direct` is deliberately sorted for a
164
+ // stable evidence contract, but sorting is not relevance. Seeding from it once selected a
165
+ // lexically earlier vendored symbol while the first ranked hit correctly named live code.
166
+ const directSeed = preliminary.candidates
167
+ .flatMap((candidate) => candidate.symbols)
168
+ .flatMap((qualifiedName) => graph.codeNodeIds(qualifiedName))[0];
169
+ const specSeed = preliminary.candidateSpecs.find((id) => id.startsWith('A-SPEC-'));
170
+ const seedId = directSeed ?? (specSeed ? `SPEC:${specSeed}` : undefined);
171
+ const budget = Math.max(100, Math.min(10_000, Math.floor(a.contextBudget ?? 2_000)));
172
+ const contextBundle = seedId
173
+ ? new bundler_1.ContextBundler(graph, context.buildContentSource(specs, scanned)).getContextBundle(seedId, budget)
174
+ : null;
175
+ // @implements A-SPEC-378
176
+ // Change recency over a WIDE window and ALL files — the enrichment history above is
177
+ // deliberately narrow (candidate files, 20 commits) and cannot serve as a prior: a file the
178
+ // lexical layer never ranked has no history there, which is precisely the file recency is
179
+ // supposed to promote. Failure is silent and total: no prior means the previous ranking,
180
+ // never a partial one, because a prior built from half the history would reorder on evidence
181
+ // nobody could audit.
182
+ const changePrior = {};
183
+ try {
184
+ const prior = (0, temporal_prior_1.temporalPrior)(root, 'HEAD', 300);
185
+ for (const [file, age] of prior.recency)
186
+ changePrior[file] = 1 + Math.pow(0.5, age / 10);
187
+ }
188
+ catch { /* no history, no prior — the ranking falls back to lexical evidence alone */ }
189
+ // @implements A-SPEC-388
190
+ // What each file's commits have SAID, normalised to 0..1. Measured on history sampled away
191
+ // from the benchmarker's own recent work (A-SPEC-387): Top-1 0.244 -> 0.333 here and 0.400 ->
192
+ // 0.500 on a second repository, Top-10 recall 0.393 -> 0.601 there. Precision falls on both,
193
+ // and that is the trade, not a footnote to it.
194
+ const commitTextBoost = {};
195
+ try {
196
+ const profile = (0, commit_text_1.commitTextProfile)(root, 'HEAD', 400);
197
+ const ranked = (0, commit_text_1.rankByCommitText)(a.request, profile, 300);
198
+ const top = ranked[0]?.score ?? 0;
199
+ if (top > 0)
200
+ for (const hit of ranked)
201
+ commitTextBoost[hit.file] = hit.score / top;
202
+ }
203
+ catch { /* no history, no boost — the ranking falls back to lexical evidence alone */ }
204
+ const analyzeWith = (defUse) => (0, maintenance_analyze_1.analyzeMaintenance)({
205
+ semantic,
206
+ ...common,
207
+ coverage: { ...common.coverage, historyStatus },
208
+ history,
209
+ changePrior,
210
+ commitTextBoost,
211
+ contextBundle,
212
+ groundTruth: a.groundTruth,
213
+ defUse,
214
+ });
215
+ // @implements A-SPEC-573.4 — def-use for the TOP CANDIDATES ONLY. Extracting it for the whole
216
+ // repository costs +88.8% (measured 2026-09-08), well past this slice's budget; the first pass
217
+ // says which handful of files are worth parsing, and the second pass reads their data flow.
218
+ // Every step is fail-open: a parse failure, an unsupported language or a missing file leaves
219
+ // the candidate's symbols exactly as the first pass produced them.
220
+ const firstPass = analyzeWith();
221
+ const analysis = (() => {
222
+ const targets = firstPass.candidates.slice(0, defuse_bound_1.DEFUSE_TOP_FILES).map((c) => c.file);
223
+ if (targets.length === 0)
224
+ return firstPass;
225
+ const defUse = {};
226
+ try {
227
+ const { TreeSitterTsParser, hasDataFlowWalk } = require('../../cpg/language-parser');
228
+ const { langForPath } = require('../../cpg/cpg-scanner');
229
+ const parser = new TreeSitterTsParser();
230
+ for (const file of targets) {
231
+ try {
232
+ // The LANGUAGE matters: the first wiring omitted it and parsed Python as TypeScript,
233
+ // which produced wrong facts and cost the second corpus 0.2376 -> 0.1741 on the
234
+ // symbol axis. A language with no walk is skipped rather than guessed at.
235
+ const lang = langForPath(file);
236
+ if (!hasDataFlowWalk(lang))
237
+ continue;
238
+ const facts = parser.extractDataFlow(fs.readFileSync(path.join(root, file), 'utf8'), lang);
239
+ if (facts !== undefined)
240
+ defUse[file] = facts;
241
+ }
242
+ catch { /* one unreadable or unparseable file must not cost the other nine */ }
243
+ }
244
+ }
245
+ catch {
246
+ return firstPass;
247
+ }
248
+ return Object.keys(defUse).length === 0 ? firstPass : analyzeWith(defUse);
249
+ })();
250
+ // @implements A-SPEC-268 — persistence is OPT-IN. The tool is advertised read-only, and a
251
+ // regression pins that a cold project gains no `.ax/cpg_cache`; writing evidence by default
252
+ // would break that contract for every caller who only wanted to look.
253
+ // One shape either way: `persistedTo` is present-or-undefined rather than a union, so a
254
+ // caller never has to narrow the result to ask whether anything was written.
255
+ const result = { ...analysis };
256
+ // @implements A-SPEC-418 — first line, because everything below it was computed without specs.
257
+ const blind = context.specStoreBlindReason(root, specs.length);
258
+ if (blind !== null)
259
+ result.unknowns = [blind, ...result.unknowns];
260
+ if (a.persist) {
261
+ // @implements A-SPEC-277 — record WHAT WAS ANALYSED, not just which paths. HEAD cannot see
262
+ // an uncommitted edit to the analysed file; a content digest can.
263
+ const fileDigests = {};
264
+ for (const file of analysis.impacts.files) {
265
+ const digest = context.fileDigestOf(root, file);
266
+ if (digest)
267
+ fileDigests[file] = digest;
268
+ }
269
+ // The artifact takes the WHOLE analysis, never the bounded one below: a file has no
270
+ // context window, and the calibration that scores this prediction must score what the
271
+ // product actually predicted.
272
+ result.persistedTo = (0, maintenance_evidence_1.writeArtifact)(path.join(root, maintenance_evidence_1.EVIDENCE_DIR), (0, maintenance_evidence_1.artifactFrom)(analysis, new Date().toISOString(), fileDigests));
273
+ }
274
+ // @implements A-SPEC-578.5 — the response, and only the response, is bounded. Measured
275
+ // 2026-09-09: the full shape is 187,174 characters (~47k tokens) and the harness refuses
276
+ // it, which is why AGENTS.md step 3 had never once been obeyed with `persist: true`.
277
+ // `persistedTo` rides along so the caller knows where the whole thing is.
278
+ const bounded = (0, maintenance_analyze_1.boundAnalysis)(result);
279
+ if (JSON.stringify(bounded).length !== JSON.stringify(result).length)
280
+ bounded.bounded = true;
281
+ return bounded;
282
+ }
283
+ finally {
284
+ graph.close();
285
+ }
286
+ },
287
+ };
288
+ }
@@ -0,0 +1,40 @@
1
+ interface MaintenanceEvidenceContext {
2
+ foreignRootReason(root: string): string | null;
3
+ projectRootOf(root: string): string;
4
+ refusal(reason: string): Error;
5
+ fileDigestOf(root: string, relativePath: string): string | null;
6
+ }
7
+ /** Raw handlers: the public facade attaches basis exactly once. */
8
+ export declare function createMaintenanceEvidenceHandlers(context: MaintenanceEvidenceContext): {
9
+ maintenance_outcome(a: {
10
+ root: string;
11
+ digest: string;
12
+ actualFiles?: string[];
13
+ actualSymbols?: string[];
14
+ actualTests?: string[];
15
+ actualClassification?: string;
16
+ }): Promise<import("../maintenance-evidence").EvidenceArtifact>;
17
+ impact_gate_check(a: {
18
+ root: string;
19
+ target: string;
20
+ digest?: string;
21
+ }): Promise<import("../../guardrail/impact-gate").EditEvidenceVerdict>;
22
+ maintenance_calibration(a: {
23
+ root: string;
24
+ minSamples?: number;
25
+ }): Promise<{
26
+ artifacts: number;
27
+ unreadable: string[];
28
+ skippedSchema: number;
29
+ schema: string;
30
+ n: number;
31
+ withoutClassification: number;
32
+ bins: import("../maintenance-evidence").CalibrationBin[];
33
+ brierScore: number | null;
34
+ falsePositives: number;
35
+ falseNegatives: number;
36
+ minSamples: number;
37
+ status: "measured" | "insufficient-data";
38
+ }>;
39
+ };
40
+ export {};