@holmes-lab/holmes-kit 0.19.6 → 0.20.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 (139) hide show
  1. package/CHANGELOG.md +143 -0
  2. package/README.md +9 -2
  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/doctor-check.d.ts +15 -0
  13. package/dist/holmes/cli/doctor-check.js +2 -0
  14. package/dist/holmes/cli/doctor.d.ts +2 -8
  15. package/dist/holmes/cli/doctor.js +12 -3
  16. package/dist/holmes/cli/gitignore-merge.d.ts +0 -5
  17. package/dist/holmes/cli/gitignore-merge.js +6 -2
  18. package/dist/holmes/cli/index.js +176 -139
  19. package/dist/holmes/cli/npx-cache-check.d.ts +16 -0
  20. package/dist/holmes/cli/npx-cache-check.js +130 -0
  21. package/dist/holmes/cli/playbook-skills.d.ts +2 -22
  22. package/dist/holmes/cli/playbook-skills.js +10 -54
  23. package/dist/holmes/cli/probe-process.d.ts +17 -2
  24. package/dist/holmes/cli/probe-process.js +44 -11
  25. package/dist/holmes/governance/approval-grants.d.ts +22 -0
  26. package/dist/holmes/governance/approval-grants.js +55 -1
  27. package/dist/holmes/governance/approval-queue.d.ts +43 -4
  28. package/dist/holmes/governance/approval-queue.js +91 -13
  29. package/dist/holmes/governance/display-id-aliases.d.ts +29 -0
  30. package/dist/holmes/governance/display-id-aliases.js +86 -0
  31. package/dist/holmes/governance/ledger-store.d.ts +42 -14
  32. package/dist/holmes/governance/ledger-store.js +49 -3
  33. package/dist/holmes/governance/ledger-timeline.d.ts +3 -1
  34. package/dist/holmes/governance/ledger-timeline.js +11 -1
  35. package/dist/holmes/governance/provenance-chain.d.ts +2 -0
  36. package/dist/holmes/governance/provenance-chain.js +13 -2
  37. package/dist/holmes/governance/provenance-ledger.d.ts +28 -0
  38. package/dist/holmes/governance/provenance-ledger.js +69 -16
  39. package/dist/holmes/governance/session-context.d.ts +3 -0
  40. package/dist/holmes/governance/session-context.js +26 -8
  41. package/dist/holmes/guardrail/risk-classifier.js +2 -2
  42. package/dist/holmes/guardrail/write-target.js +2 -1
  43. package/dist/holmes/hooks/pre-tool-use.js +12 -1
  44. package/dist/holmes/hooks/stop.js +14 -3
  45. package/dist/holmes/mcp/handlers/adoption.d.ts +118 -0
  46. package/dist/holmes/mcp/handlers/adoption.js +186 -0
  47. package/dist/holmes/mcp/handlers/entity-integration.d.ts +100 -0
  48. package/dist/holmes/mcp/handlers/entity-integration.js +324 -0
  49. package/dist/holmes/mcp/handlers/entity-renumber.d.ts +137 -0
  50. package/dist/holmes/mcp/handlers/entity-renumber.js +228 -0
  51. package/dist/holmes/mcp/handlers/entity-store.d.ts +78 -0
  52. package/dist/holmes/mcp/handlers/entity-store.js +212 -0
  53. package/dist/holmes/mcp/handlers/graph-operations.d.ts +90 -0
  54. package/dist/holmes/mcp/handlers/graph-operations.js +260 -0
  55. package/dist/holmes/mcp/handlers/maintenance-analysis.d.ts +34 -0
  56. package/dist/holmes/mcp/handlers/maintenance-analysis.js +288 -0
  57. package/dist/holmes/mcp/handlers/maintenance-evidence.d.ts +40 -0
  58. package/dist/holmes/mcp/handlers/maintenance-evidence.js +104 -0
  59. package/dist/holmes/mcp/handlers/operator-inspection.d.ts +167 -0
  60. package/dist/holmes/mcp/handlers/operator-inspection.js +284 -0
  61. package/dist/holmes/mcp/handlers/phase-queries.d.ts +34 -0
  62. package/dist/holmes/mcp/handlers/phase-queries.js +86 -0
  63. package/dist/holmes/mcp/handlers/review-evidence.d.ts +47 -0
  64. package/dist/holmes/mcp/handlers/review-evidence.js +370 -0
  65. package/dist/holmes/mcp/handlers/review-queries.d.ts +68 -0
  66. package/dist/holmes/mcp/handlers/review-queries.js +79 -0
  67. package/dist/holmes/mcp/handlers/risk-assessment.d.ts +25 -0
  68. package/dist/holmes/mcp/handlers/risk-assessment.js +181 -0
  69. package/dist/holmes/mcp/handlers/slice-orchestration.d.ts +72 -0
  70. package/dist/holmes/mcp/handlers/slice-orchestration.js +373 -0
  71. package/dist/holmes/mcp/handlers/spec-approval.d.ts +105 -0
  72. package/dist/holmes/mcp/handlers/spec-approval.js +504 -0
  73. package/dist/holmes/mcp/handlers/spec-authoring.d.ts +57 -0
  74. package/dist/holmes/mcp/handlers/spec-authoring.js +326 -0
  75. package/dist/holmes/mcp/handlers/spec-lifecycle.d.ts +122 -0
  76. package/dist/holmes/mcp/handlers/spec-lifecycle.js +339 -0
  77. package/dist/holmes/mcp/handlers/spec-queries.d.ts +30 -0
  78. package/dist/holmes/mcp/handlers/spec-queries.js +74 -0
  79. package/dist/holmes/mcp/handlers/taint-screening.d.ts +48 -0
  80. package/dist/holmes/mcp/handlers/taint-screening.js +183 -0
  81. package/dist/holmes/mcp/handlers/test-execution.d.ts +63 -0
  82. package/dist/holmes/mcp/handlers/test-execution.js +210 -0
  83. package/dist/holmes/mcp/handlers/workspace-identity.d.ts +30 -0
  84. package/dist/holmes/mcp/handlers/workspace-identity.js +68 -0
  85. package/dist/holmes/mcp/handlers/workspace-queries.d.ts +77 -0
  86. package/dist/holmes/mcp/handlers/workspace-queries.js +193 -0
  87. package/dist/holmes/mcp/handlers.d.ts +661 -424
  88. package/dist/holmes/mcp/handlers.js +270 -3251
  89. package/dist/holmes/mcp/server-specs-dir.d.ts +2 -0
  90. package/dist/holmes/mcp/server-specs-dir.js +71 -0
  91. package/dist/holmes/mcp/server.js +12 -2
  92. package/dist/holmes/mcp/tool-schemas.js +43 -2
  93. package/dist/holmes/project/execution-context.d.ts +17 -0
  94. package/dist/holmes/project/execution-context.js +121 -0
  95. package/dist/holmes/project/installer-markers.d.ts +34 -0
  96. package/dist/holmes/project/installer-markers.js +65 -0
  97. package/dist/holmes/project/root.d.ts +12 -1
  98. package/dist/holmes/project/root.js +22 -3
  99. package/dist/holmes/project/workspace-identity.d.ts +29 -0
  100. package/dist/holmes/project/workspace-identity.js +181 -0
  101. package/dist/holmes/review/test-outcomes.d.ts +6 -1
  102. package/dist/holmes/review/test-outcomes.js +23 -5
  103. package/dist/holmes/review/test-runner.d.ts +18 -0
  104. package/dist/holmes/review/test-runner.js +136 -5
  105. package/dist/holmes/rtm/graph-store.d.ts +50 -1
  106. package/dist/holmes/rtm/graph-store.js +244 -3
  107. package/dist/holmes/rtm/incremental.d.ts +1 -0
  108. package/dist/holmes/rtm/incremental.js +12 -3
  109. package/dist/holmes/rtm/localize.js +7 -0
  110. package/dist/holmes/rtm/rtm-graph.d.ts +9 -0
  111. package/dist/holmes/rtm/rtm-graph.js +13 -0
  112. package/dist/holmes/rtm/test-scope.js +4 -1
  113. package/dist/holmes/semantic/vector-cache.d.ts +16 -1
  114. package/dist/holmes/semantic/vector-cache.js +155 -19
  115. package/dist/holmes/spec/approval-status.d.ts +10 -0
  116. package/dist/holmes/spec/approval-status.js +7 -3
  117. package/dist/holmes/spec/entity-git-snapshot.d.ts +50 -0
  118. package/dist/holmes/spec/entity-git-snapshot.js +276 -0
  119. package/dist/holmes/spec/entity-integration-discharge.d.ts +73 -0
  120. package/dist/holmes/spec/entity-integration-discharge.js +233 -0
  121. package/dist/holmes/spec/entity-integration-transaction.d.ts +179 -0
  122. package/dist/holmes/spec/entity-integration-transaction.js +1221 -0
  123. package/dist/holmes/spec/entity-integration.d.ts +216 -0
  124. package/dist/holmes/spec/entity-integration.js +760 -0
  125. package/dist/holmes/spec/entity-renumber-transaction.d.ts +82 -0
  126. package/dist/holmes/spec/entity-renumber-transaction.js +711 -0
  127. package/dist/holmes/spec/entity-renumber.d.ts +32 -0
  128. package/dist/holmes/spec/entity-renumber.js +156 -0
  129. package/dist/holmes/spec/entity-store.d.ts +135 -0
  130. package/dist/holmes/spec/entity-store.js +1053 -0
  131. package/dist/holmes/spec/entity-transaction.d.ts +105 -0
  132. package/dist/holmes/spec/entity-transaction.js +741 -0
  133. package/dist/holmes/spec/renumber.d.ts +58 -0
  134. package/dist/holmes/spec/renumber.js +200 -1
  135. package/dist/holmes/spec/spec-store.d.ts +3 -2
  136. package/dist/holmes/spec/spec-store.js +23 -2
  137. package/dist/holmes/spec/transition-policy.d.ts +36 -0
  138. package/dist/holmes/spec/transition-policy.js +135 -0
  139. package/package.json +5 -2
@@ -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 {};
@@ -0,0 +1,104 @@
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.createMaintenanceEvidenceHandlers = createMaintenanceEvidenceHandlers;
37
+ // @implements A-SPEC-603
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 maintenance_evidence_1 = require("../maintenance-evidence");
42
+ const impact_gate_1 = require("../../guardrail/impact-gate");
43
+ const root_1 = require("../../project/root");
44
+ /** Raw handlers: the public facade attaches basis exactly once. */
45
+ function createMaintenanceEvidenceHandlers(context) {
46
+ return {
47
+ // @implements A-SPEC-268
48
+ async maintenance_outcome(a) {
49
+ const foreign = context.foreignRootReason(a.root);
50
+ if (foreign)
51
+ throw context.refusal(foreign);
52
+ const root = context.projectRootOf(a.root);
53
+ return (0, maintenance_evidence_1.recordOutcome)(path.join(root, maintenance_evidence_1.EVIDENCE_DIR), a.digest, {
54
+ actualFiles: a.actualFiles,
55
+ actualSymbols: a.actualSymbols,
56
+ actualTests: a.actualTests,
57
+ actualClassification: a.actualClassification,
58
+ }, new Date().toISOString());
59
+ },
60
+ // @implements A-SPEC-277
61
+ async impact_gate_check(a) {
62
+ const foreign = context.foreignRootReason(a.root);
63
+ if (foreign)
64
+ throw context.refusal(foreign);
65
+ const root = context.projectRootOf(a.root);
66
+ let head = '';
67
+ try {
68
+ 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();
69
+ }
70
+ catch { /* non-git */ }
71
+ let artifact = null;
72
+ if (a.digest) {
73
+ const file = path.join(root, maintenance_evidence_1.EVIDENCE_DIR, (0, maintenance_evidence_1.digestFilename)(a.digest));
74
+ // A digest naming an artifact this project never stored is NOT the same as no evidence at
75
+ // all — say which one happened rather than collapsing both into "refused".
76
+ if (!fs.existsSync(file))
77
+ throw context.refusal(`이 프로젝트에 저장된 분석이 아닙니다: ${a.digest}`);
78
+ artifact = JSON.parse(fs.readFileSync(file, 'utf8'));
79
+ }
80
+ return (0, impact_gate_1.evaluateEditEvidence)({
81
+ target: a.target.replace(/\\/g, '/'),
82
+ artifact,
83
+ currentHead: head,
84
+ currentTargetDigest: context.fileDigestOf(root, a.target),
85
+ });
86
+ },
87
+ // @implements A-SPEC-268
88
+ async maintenance_calibration(a) {
89
+ const foreign = context.foreignRootReason(a.root);
90
+ if (foreign)
91
+ throw context.refusal(foreign);
92
+ const root = context.projectRootOf(a.root);
93
+ const read = (0, maintenance_evidence_1.readArtifacts)(path.join(root, maintenance_evidence_1.EVIDENCE_DIR));
94
+ // Corrupt and off-schema records travel WITH the score. A calibration that quietly dropped
95
+ // them would report a clean number over an unstated subset of the evidence.
96
+ return {
97
+ ...(0, maintenance_evidence_1.computeCalibration)(read.artifacts, { minSamples: a.minSamples }),
98
+ artifacts: read.artifacts.length,
99
+ unreadable: read.unreadable,
100
+ skippedSchema: read.skippedSchema,
101
+ };
102
+ },
103
+ };
104
+ }
@@ -0,0 +1,167 @@
1
+ import { Spec } from '../../spec/spec-parser';
2
+ import { SpecStore } from '../../spec/spec-store';
3
+ import type { Approval } from '../../guardrail/risk-gate';
4
+ export interface OperatorInspectionContext {
5
+ listSpecs(): Promise<Spec[]>;
6
+ readSpec: SpecStore['read'];
7
+ specsRoot(): unknown;
8
+ assertStoreReachable(tool: string, root: unknown): void;
9
+ resolver(specs: Spec[]): (id: string) => Spec | null;
10
+ resolveLedgerRoot(root?: string): {
11
+ ok: true;
12
+ root: string;
13
+ } | {
14
+ ok: false;
15
+ reason: string;
16
+ };
17
+ fetchJson(url: string): Promise<any>;
18
+ /** A-SPEC-642 — the shared approval channel and queue hint, for the reconciliation act. */
19
+ resolveHandlerApproval(root: string | undefined, approval: Approval | undefined, action: {
20
+ kind: string;
21
+ target: string;
22
+ }, now: string): {
23
+ approval: Approval;
24
+ source: 'env' | 'grant';
25
+ root?: string;
26
+ } | undefined;
27
+ refusalQueueHint(root: string | undefined, request: {
28
+ kind: string;
29
+ target: string;
30
+ why: string;
31
+ }): string;
32
+ }
33
+ export declare function createOperatorInspectionHandlers(context: OperatorInspectionContext): {
34
+ /**
35
+ * @implements A-SPEC-538.2
36
+ * Read-only: report a spec's approval/seal state — sealed?, approved_digest, each parent's seal
37
+ * state, and the concrete blockers still standing between it and approval — so a caller need not
38
+ * parse files to ask "what is the approval state right now". Reuses `sealOf` and `approvalBlockers`
39
+ * (the same predicates the code gate and spec_approve read) so the report cannot drift from the
40
+ * acts it describes. No writes, no ledger append.
41
+ */
42
+ approval_status(a: {
43
+ root?: string;
44
+ id: string;
45
+ }): Promise<{
46
+ ok: boolean;
47
+ reason: string;
48
+ } | {
49
+ graphPreview?: {
50
+ impact?: import("../../rtm/impact-advisory").ImpactAdvisory;
51
+ density?: Array<import("../../rtm/anchor-density").AnchorDensityFinding>;
52
+ cycles?: {
53
+ findings: Array<import("../../cpg/cycle-detect").CycleFinding>;
54
+ note: string;
55
+ };
56
+ architecture?: Array<import("../../cpg/arch-observe").ArchObservation>;
57
+ graphAsOf?: string;
58
+ graph?: {
59
+ status: "current" | "stale" | "unverified";
60
+ sourceCommit: string | null;
61
+ head: string | null;
62
+ };
63
+ unavailable?: "empty" | "unreadable";
64
+ } | undefined;
65
+ id: string;
66
+ type?: string;
67
+ status: string;
68
+ sealed: boolean;
69
+ approvedDigest?: string;
70
+ parents: import("../../spec/approval-status").ParentApproval[];
71
+ blockers: string[];
72
+ resealBlockers: string[];
73
+ ok: boolean;
74
+ reason?: undefined;
75
+ }>;
76
+ /**
77
+ * @implements A-SPEC-538.3
78
+ * Read-only: return the provenance ledger's events in time order (optionally narrowed to one
79
+ * spec) so the governance history — approved / unsealed / retired / review-needed … — is legible
80
+ * at a glance without reading raw JSONL. Reuses FileLedgerStore.loadAll(); the ordering/filtering/
81
+ * projection is the pure `timelineFrom`. No writes. An absent ledger is an empty history, not an
82
+ * error.
83
+ */
84
+ ledger_timeline(a: {
85
+ root?: string;
86
+ id?: string;
87
+ }): Promise<{
88
+ ok: boolean;
89
+ reason: string;
90
+ events?: undefined;
91
+ } | {
92
+ ok: boolean;
93
+ events: import("../../governance/ledger-timeline").TimelineEntry[];
94
+ reason?: undefined;
95
+ }>;
96
+ /**
97
+ * @implements A-SPEC-642
98
+ * The one exit from a merged cross-replica double-spend. Under out-of-band authority of kind
99
+ * `ledger-reconcile` for that nonce, append `nonce-double-spend-reconciled` on THIS replica's chain
100
+ * naming the nonce and every consuming event's hash; `verifyAll`, the Stop hook, baseline trust and
101
+ * the ledger-conflict refusals then clear. Nothing is erased: both consumptions stay in their
102
+ * chains, and a later consumption of the same nonce is a new, open double-spend. Refuses to extend
103
+ * a broken chain, because a reconciliation that itself cannot be verified lifts nothing.
104
+ */
105
+ ledger_reconcile(a: {
106
+ root?: string;
107
+ nonce: string;
108
+ rationale?: string;
109
+ }): Promise<{
110
+ ok: boolean;
111
+ reason: string;
112
+ reconciled?: undefined;
113
+ nonce?: undefined;
114
+ pending?: undefined;
115
+ code?: undefined;
116
+ events?: undefined;
117
+ remaining?: undefined;
118
+ } | {
119
+ ok: boolean;
120
+ reconciled: boolean;
121
+ nonce: string;
122
+ pending: string[];
123
+ reason: string;
124
+ code?: undefined;
125
+ events?: undefined;
126
+ remaining?: undefined;
127
+ } | {
128
+ ok: boolean;
129
+ code: string;
130
+ reason: string;
131
+ reconciled?: undefined;
132
+ nonce?: undefined;
133
+ pending?: undefined;
134
+ events?: undefined;
135
+ remaining?: undefined;
136
+ } | {
137
+ ok: boolean;
138
+ reconciled: boolean;
139
+ nonce: string;
140
+ events: {
141
+ replicaId: string | null;
142
+ seq: number;
143
+ hash: string;
144
+ ts: string;
145
+ }[];
146
+ remaining: string[];
147
+ reason?: undefined;
148
+ pending?: undefined;
149
+ code?: undefined;
150
+ }>;
151
+ rtm_dashboard(a: {
152
+ root?: string;
153
+ port?: number;
154
+ }): Promise<{
155
+ ok: boolean;
156
+ reason: string;
157
+ url?: undefined;
158
+ running?: undefined;
159
+ census?: undefined;
160
+ } | {
161
+ ok: boolean;
162
+ url: string;
163
+ running: boolean;
164
+ census: import("../../server/dashboard-launcher").DashboardCensus;
165
+ reason?: undefined;
166
+ }>;
167
+ };