@holmes-lab/holmes-kit 0.18.0 → 0.19.2

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 (81) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/README.md +3 -0
  3. package/dist/.build-id +1 -1
  4. package/dist/holmes/cli/agents.d.ts +22 -0
  5. package/dist/holmes/cli/agents.js +76 -1
  6. package/dist/holmes/cli/approve.js +6 -1
  7. package/dist/holmes/cli/doctor.d.ts +36 -1
  8. package/dist/holmes/cli/doctor.js +182 -35
  9. package/dist/holmes/cli/index.js +7 -1
  10. package/dist/holmes/cli/init.js +12 -0
  11. package/dist/holmes/cli/native-deps.d.ts +65 -0
  12. package/dist/holmes/cli/native-deps.js +131 -0
  13. package/dist/holmes/cli/release-docs.d.ts +27 -0
  14. package/dist/holmes/cli/release-docs.js +68 -0
  15. package/dist/holmes/cpg/arch-observe.d.ts +15 -0
  16. package/dist/holmes/cpg/arch-observe.js +19 -0
  17. package/dist/holmes/cpg/cpg-scanner.d.ts +10 -36
  18. package/dist/holmes/cpg/cpg-scanner.js +27 -3
  19. package/dist/holmes/cpg/cycle-detect.d.ts +87 -0
  20. package/dist/holmes/cpg/cycle-detect.js +251 -0
  21. package/dist/holmes/cpg/cycle-observation.d.ts +65 -0
  22. package/dist/holmes/cpg/cycle-observation.js +146 -0
  23. package/dist/holmes/cpg/scan-cache.d.ts +1 -1
  24. package/dist/holmes/cpg/scanned-file.d.ts +36 -0
  25. package/dist/holmes/cpg/scanned-file.js +2 -0
  26. package/dist/holmes/governance/approval-queue.d.ts +23 -4
  27. package/dist/holmes/governance/approval-queue.js +44 -6
  28. package/dist/holmes/governance/constitution.d.ts +20 -0
  29. package/dist/holmes/governance/constitution.js +17 -0
  30. package/dist/holmes/governance/ledger-store.d.ts +9 -0
  31. package/dist/holmes/governance/ledger-store.js +47 -0
  32. package/dist/holmes/governance/provenance-chain.d.ts +16 -1
  33. package/dist/holmes/governance/provenance-chain.js +5 -3
  34. package/dist/holmes/hooks/pre-tool-use.js +3 -1
  35. package/dist/holmes/hooks/stop.d.ts +29 -0
  36. package/dist/holmes/hooks/stop.js +119 -3
  37. package/dist/holmes/mcp/defuse-bound.d.ts +1 -0
  38. package/dist/holmes/mcp/defuse-bound.js +8 -0
  39. package/dist/holmes/mcp/handlers.d.ts +7 -0
  40. package/dist/holmes/mcp/handlers.js +132 -6
  41. package/dist/holmes/mcp/history-admission.d.ts +15 -0
  42. package/dist/holmes/mcp/history-admission.js +37 -0
  43. package/dist/holmes/mcp/maintenance-analyze.d.ts +45 -0
  44. package/dist/holmes/mcp/maintenance-analyze.js +117 -9
  45. package/dist/holmes/mcp/maintenance-evidence.d.ts +41 -0
  46. package/dist/holmes/mcp/maintenance-evidence.js +71 -4
  47. package/dist/holmes/project/install-scripts-policy.d.ts +76 -0
  48. package/dist/holmes/project/install-scripts-policy.js +131 -0
  49. package/dist/holmes/project/npx-bin.d.ts +6 -0
  50. package/dist/holmes/project/npx-bin.js +10 -0
  51. package/dist/holmes/review/evaluation-metrics.d.ts +6 -0
  52. package/dist/holmes/review/evaluation-metrics.js +18 -1
  53. package/dist/holmes/review/failed-test-names.d.ts +19 -0
  54. package/dist/holmes/review/failed-test-names.js +43 -0
  55. package/dist/holmes/review/paired-power.d.ts +14 -0
  56. package/dist/holmes/review/paired-power.js +57 -0
  57. package/dist/holmes/review/replay-corpus.d.ts +11 -0
  58. package/dist/holmes/review/replay-corpus.js +34 -0
  59. package/dist/holmes/review/run-replay.d.ts +23 -0
  60. package/dist/holmes/review/run-replay.js +90 -4
  61. package/dist/holmes/review/symbol-truth.d.ts +14 -0
  62. package/dist/holmes/review/symbol-truth.js +23 -0
  63. package/dist/holmes/review/test-runner.d.ts +27 -0
  64. package/dist/holmes/review/test-runner.js +59 -3
  65. package/dist/holmes/rtm/defuse-symbols.d.ts +17 -0
  66. package/dist/holmes/rtm/defuse-symbols.js +91 -0
  67. package/dist/holmes/rtm/incremental.js +5 -0
  68. package/dist/holmes/rtm/rtm-builder.d.ts +8 -0
  69. package/dist/holmes/rtm/rtm-builder.js +32 -5
  70. package/dist/holmes/rtm/rtm-graph.d.ts +11 -0
  71. package/dist/holmes/rtm/rtm-graph.js +13 -0
  72. package/dist/holmes/spec/legacy-fields.d.ts +2 -0
  73. package/dist/holmes/spec/legacy-fields.js +9 -0
  74. package/dist/holmes/spec/legacy-format.d.ts +1 -1
  75. package/dist/holmes/spec/legacy-format.js +4 -1
  76. package/dist/holmes/spec/spec-parser.js +5 -3
  77. package/docs/install-guide.md +54 -5
  78. package/package.json +4 -1
  79. package/playbooks/author-slice/PLAYBOOK.md +14 -0
  80. package/playbooks/publish/PLAYBOOK.md +32 -0
  81. package/playbooks/tdd-slice/PLAYBOOK.md +14 -0
@@ -42,6 +42,7 @@ exports.escalateReappraisals = escalateReappraisals;
42
42
  exports.unrecordedApprovals = unrecordedApprovals;
43
43
  exports.rolledBackLedgers = rolledBackLedgers;
44
44
  exports.governanceLostPreflight = governanceLostPreflight;
45
+ exports.trackedLines = trackedLines;
45
46
  exports.evaluateStop = evaluateStop;
46
47
  exports.stopDebtAction = stopDebtAction;
47
48
  exports.acknowledgeStop = acknowledgeStop;
@@ -62,6 +63,8 @@ const node_child_process_1 = require("node:child_process");
62
63
  const path = __importStar(require("node:path"));
63
64
  const test_scope_1 = require("../rtm/test-scope");
64
65
  const constitution_1 = require("../governance/constitution");
66
+ // @implements A-SPEC-574.4 — same detector as the design-time advisory and the strict article.
67
+ const cycle_detect_1 = require("../cpg/cycle-detect");
65
68
  const provenance_chain_1 = require("../governance/provenance-chain");
66
69
  const test_evidence_1 = require("../review/test-evidence");
67
70
  const test_outcomes_1 = require("../review/test-outcomes");
@@ -430,6 +433,36 @@ function governanceLostPreflight(specsDir, projectRoot) {
430
433
  const root = projectRoot ?? path.resolve(specsDir, '..', '..');
431
434
  return (0, governance_history_1.hasGovernanceHistory)(root) ? governance_history_1.GOVERNANCE_LOST_HINT : null;
432
435
  }
436
+ // @implements A-SPEC-578.1
437
+ /** What each article's track observations are called on the operator's screen. */
438
+ const TRACK_LABELS = {
439
+ 'ART-8': 'RED-first',
440
+ 'ART-2': 'code-graph cycles',
441
+ };
442
+ /**
443
+ * One line per ARTICLE, each under its own name.
444
+ *
445
+ * This used to be a single line reading `ART-8 RED-first (track)` for everything in `tracked` —
446
+ * and the cycle ratchet pushes ART-2 findings into that same array, so an import cycle was
447
+ * reported to the operator as a RED-first violation. Two observers sharing one sentence means the
448
+ * sentence is wrong for at least one of them.
449
+ *
450
+ * An article with no label still speaks, under its bare name: a new observer that says nothing is
451
+ * worse than one that says something plain.
452
+ */
453
+ function trackedLines(tracked) {
454
+ if (!tracked || tracked.length === 0)
455
+ return [];
456
+ const byArticle = new Map();
457
+ for (const t of tracked) {
458
+ const key = String(t?.article ?? '');
459
+ byArticle.set(key, [...(byArticle.get(key) ?? []), String(t?.detail ?? '')]);
460
+ }
461
+ return [...byArticle.entries()].map(([article, details]) => {
462
+ const label = TRACK_LABELS[article];
463
+ return `[Holmes-Kit] ${article}${label ? ` ${label}` : ''} (track): ${details.join(' | ')}`;
464
+ });
465
+ }
433
466
  function evaluateStop(specs, evidence) {
434
467
  // L1: the Stop gate IS the constitution's re-verification point — every turn boundary re-runs the
435
468
  // inviolable articles (ART-2 RTM, ART-3 validity incl. 4-quadrant GWT, ART-4 coverage evidence).
@@ -439,6 +472,7 @@ function evaluateStop(specs, evidence) {
439
472
  const violations = (0, constitution_1.verifyConstitution)({
440
473
  specs, testCasesByAspec: evidence?.testCasesByAspec, executedByAspec: evidence?.executedByAspec, findings: evidence?.findings,
441
474
  redFirstMode: evidence?.redFirstMode, changedAspecs: evidence?.changedAspecs, outcomesByAspec: evidence?.outcomesByAspec,
475
+ cycles: evidence?.cycles,
442
476
  });
443
477
  // @implements A-SPEC-534.4 — `track` records ART-8 findings without blocking the turn. Computed
444
478
  // separately (the constitution stays silent on ART-8 outside strict) and returned in `tracked` for
@@ -449,6 +483,17 @@ function evaluateStop(specs, evidence) {
449
483
  if (t.length)
450
484
  tracked = t;
451
485
  }
486
+ // @implements A-SPEC-574.4 — the cycle ratchet's track posture: recorded, never blocking. Promotion
487
+ // to `strict` waits on the observation ledger answering the false-positive rate, which is the same
488
+ // path impactAdvisory and anchorDensity took.
489
+ if (evidence?.cycles !== undefined && evidence.cycles.mode === 'track') {
490
+ const scope = evidence.cycles.scope.unavailable.length > 0
491
+ ? ` (NOT judged: ${evidence.cycles.scope.unavailable.join(', ')})` : '';
492
+ const t = (0, cycle_detect_1.cycleRatchetViolations)(evidence.cycles.current, evidence.cycles.allowed)
493
+ .map((x) => ({ article: 'ART-2', detail: `code ${x.detail}${scope}` }));
494
+ if (t.length)
495
+ tracked = [...(tracked ?? []), ...t];
496
+ }
452
497
  const problems = violations.map((x) => `[${x.article}] ${x.detail}`);
453
498
  // @implements A-SPEC-247 — structured list so the caller can ask acknowledgeStop which of these
454
499
  // are waiting on an owner. Mirrors `problems` exactly, including the two synthesized below.
@@ -876,6 +921,77 @@ if (require.main === module) {
876
921
  changedAspecs = undefined;
877
922
  outcomesByAspec = undefined;
878
923
  }
924
+ // @implements A-SPEC-574.4 — the code graph's acyclicity, judged where the code exists. The
925
+ // scan is the same one every other layer uses; the classification reads only the files an
926
+ // import edge starts from. Fail-open in every direction: any error leaves the article silent,
927
+ // because "we could not look" must never be reported as "there is nothing there".
928
+ let cycles;
929
+ try {
930
+ const root = stopProjectRoot();
931
+ const { CpgScanner } = require('../cpg/cpg-scanner');
932
+ const scanned = new CpgScanner().scan(root, root);
933
+ const known = new Set(scanned.map((f) => f.sourcePath));
934
+ const edges = [];
935
+ const judged = new Set();
936
+ const sawImports = new Set();
937
+ for (const f of scanned) {
938
+ const ext = f.sourcePath.slice(f.sourcePath.lastIndexOf('.'));
939
+ let text = '';
940
+ try {
941
+ text = fs.readFileSync(path.join(root, f.sourcePath), 'utf8');
942
+ }
943
+ catch {
944
+ continue;
945
+ }
946
+ let sawImport = false;
947
+ for (const e of f.edges ?? []) {
948
+ if (e.rel !== 'imports' || !e.to)
949
+ continue;
950
+ sawImport = true;
951
+ if (!e.to.startsWith('.'))
952
+ continue;
953
+ const base = path.posix.normalize(path.posix.join(path.posix.dirname(f.sourcePath), e.to));
954
+ const to = ['.ts', '.tsx', '.js', '/index.ts'].map((x) => base + x).find((c) => known.has(c))
955
+ ?? (known.has(base) ? base : null);
956
+ if (to === null || to === f.sourcePath)
957
+ continue;
958
+ edges.push({ from: f.sourcePath, to, kind: (0, cycle_detect_1.classifyEdgeByTarget)(text, f.sourcePath, to) });
959
+ judged.add(ext);
960
+ }
961
+ // A file whose imports are all bare specifiers is not evidence that its LANGUAGE is
962
+ // unresolvable — subtract the judged set at the end instead of marking per file, or one
963
+ // such file makes the whole language read as unjudged (measured: `.ts` and `.js` landed in
964
+ // both lists).
965
+ if (sawImport)
966
+ sawImports.add(ext);
967
+ }
968
+ cycles = {
969
+ current: (0, cycle_detect_1.detectCycles)(edges).cycles,
970
+ allowed: [],
971
+ mode: 'track',
972
+ // Unjudged = saw imports, resolved none, anywhere. Naming these is the difference between
973
+ // "no cycles" and "no cycles in the languages we could follow".
974
+ scope: {
975
+ judged: [...judged].sort(),
976
+ unavailable: [...sawImports].filter((e) => !judged.has(e)).sort(),
977
+ },
978
+ };
979
+ // @implements A-SPEC-578.1 — record the observation the promotion criterion waits on.
980
+ // EVERY run, including a clean one: a false-positive rate is violations over chances, and a
981
+ // ledger that only speaks when something is wrong keeps the numerator and drops the
982
+ // denominator. Append failure is swallowed by the outer catch below — an observation that
983
+ // could change a verdict would be a gate wearing another name.
984
+ // Its OWN try: sharing the outer one would let a fault in the observation discard the
985
+ // article's evidence, which is the coupling this comment exists to deny.
986
+ try {
987
+ const { appendCycleObservation, buildCycleObservation } = require('../cpg/cycle-observation');
988
+ appendCycleObservation(root, buildCycleObservation(cycles, new Date().toISOString()));
989
+ }
990
+ catch { /* an observation never touches the verdict */ }
991
+ }
992
+ catch {
993
+ cycles = undefined;
994
+ }
879
995
  // Provenance-chain verification (fail-open: a verify error skips the check, never crashes).
880
996
  let provenance;
881
997
  // @implements A-SPEC-148
@@ -973,9 +1089,9 @@ if (require.main === module) {
973
1089
  }
974
1090
  }
975
1091
  catch { /* the reappraisal signal is advisory; a failure never affects the stop verdict */ }
976
- if (out.tracked && out.tracked.length > 0) {
977
- process.stderr.write(`[Holmes-Kit] ART-8 RED-first (track): ${out.tracked.map((t) => t.detail).join(' | ')}\n`);
978
- }
1092
+ // @implements A-SPEC-578.1 one line per article, each under its own name.
1093
+ for (const line of trackedLines(out.tracked))
1094
+ process.stderr.write(`${line}\n`);
979
1095
  // @implements A-SPEC-247 — before deciding to re-block, ask whether every unresolved debt is
980
1096
  // already queued for the owner. If so, tell the user ONCE and let the turn finish; a single
981
1097
  // non-waiting violation and we block exactly as before.
@@ -0,0 +1 @@
1
+ export declare const DEFUSE_TOP_FILES = 10;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFUSE_TOP_FILES = void 0;
4
+ // @implements A-SPEC-573.4
5
+ // One constant, imported by both consumers, so the product path and the benchmark cannot drift on
6
+ // how much data flow they read. Ten is the emission tier a caller acts on; whole-repository
7
+ // extraction was measured at +88.8% (2572ms -> 4857ms, 2026-09-08) and ten files at roughly 95ms.
8
+ exports.DEFUSE_TOP_FILES = 10;
@@ -223,6 +223,11 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
223
223
  graphPreview?: {
224
224
  impact?: import("../rtm/impact-advisory").ImpactAdvisory;
225
225
  density?: import("../rtm/anchor-density").AnchorDensityFinding[];
226
+ cycles?: {
227
+ findings: import("../cpg/cycle-detect").CycleFinding[];
228
+ note: string;
229
+ };
230
+ architecture?: import("../cpg/arch-observe").ArchObservation[];
226
231
  graphAsOf?: string;
227
232
  } | undefined;
228
233
  id: string;
@@ -485,6 +490,7 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
485
490
  survivors: import("../spec/kills").Mutation[];
486
491
  };
487
492
  } | {
493
+ calibrationClosed?: string[] | undefined;
488
494
  baselineRecorded?: string | undefined;
489
495
  scopeFallback?: "full" | undefined;
490
496
  tier: import("../rtm/test-scope").RegressionTier;
@@ -513,6 +519,7 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
513
519
  changedFiles?: string[];
514
520
  }): Promise<MaintenanceAnalysis & {
515
521
  persistedTo?: string;
522
+ bounded?: boolean;
516
523
  }>;
517
524
  maintenance_outcome(a: {
518
525
  root: string;
@@ -45,6 +45,8 @@ exports.collectDecisions = collectDecisions;
45
45
  // @implements A-SPEC-277
46
46
  // @implements A-SPEC-269
47
47
  // @implements A-SPEC-267
48
+ // @implements A-SPEC-573.4 — shared with the benchmark so the two cannot drift.
49
+ const defuse_bound_1 = require("./defuse-bound");
48
50
  const fs = __importStar(require("node:fs"));
49
51
  const http = __importStar(require("node:http"));
50
52
  const assoc_arm_1 = require("../assoc/assoc-arm");
@@ -205,6 +207,8 @@ const ledger_timeline_1 = require("../governance/ledger-timeline");
205
207
  const version_conflict_1 = require("../spec/version-conflict");
206
208
  const ledger_store_1 = require("../governance/ledger-store");
207
209
  const provenance_chain_1 = require("../governance/provenance-chain");
210
+ // @implements A-SPEC-574.3 — the caller owns the store and hands the chain the question.
211
+ const ledger_store_2 = require("../governance/ledger-store");
208
212
  const ledger_lock_1 = require("../governance/ledger-lock");
209
213
  const decision_ledger_1 = require("../guardrail/decision-ledger");
210
214
  const cspec_change_1 = require("../guardrail/cspec-change");
@@ -1375,7 +1379,63 @@ function makeRawHandlers(store, opts) {
1375
1379
  return null;
1376
1380
  } });
1377
1381
  const density = anchorDensityFindings(ftt, graph.implementsAnchorCounts());
1378
- if (impact || density.length > 0) {
1382
+ // @implements A-SPEC-574.2 the computation lives in cycle-detect; this file holds
1383
+ // the wiring only (measured: 85 anchors here against a p90 of 7).
1384
+ const { cycleAdvisory, classifyEdgeByTarget, CYCLE_ADVISORY_NOTE } = require('../cpg/cycle-detect');
1385
+ const readCache = new Map();
1386
+ const readSource = (rel) => {
1387
+ const hit = readCache.get(rel);
1388
+ if (hit !== undefined)
1389
+ return hit;
1390
+ // A read failure degrades to the conservative kind rather than losing the finding.
1391
+ let text = '';
1392
+ try {
1393
+ text = fs.readFileSync(path.join(a.root, rel), 'utf8');
1394
+ }
1395
+ catch {
1396
+ text = '';
1397
+ }
1398
+ readCache.set(rel, text);
1399
+ return text;
1400
+ };
1401
+ const cycleFindings = cycleAdvisory(ftt, graph.importEdges().map((edge) => ({
1402
+ ...edge,
1403
+ kind: classifyEdgeByTarget(readSource(edge.from), edge.from, edge.to),
1404
+ })));
1405
+ // @implements A-SPEC-574.5 — the same import edges the cycle pass already read, plus
1406
+ // the parent-time symbol spans the graph already holds. No new scan, no new parse.
1407
+ const { architectureObservation } = require('../cpg/arch-observe');
1408
+ const { TreeSitterTsParser } = require('../cpg/language-parser');
1409
+ const { langForPath } = require('../cpg/cpg-scanner');
1410
+ // Bounded to the DECLARED files, whose text is read once and used for both numbers.
1411
+ const archParser = new TreeSitterTsParser();
1412
+ const archText = new Map();
1413
+ const readArch = (f) => {
1414
+ if (!archText.has(f)) {
1415
+ try {
1416
+ archText.set(f, fs.readFileSync(path.join(a.root, f), 'utf8'));
1417
+ }
1418
+ catch {
1419
+ archText.set(f, null);
1420
+ }
1421
+ }
1422
+ return archText.get(f) ?? null;
1423
+ };
1424
+ const spans = new Map(ftt.map((f) => {
1425
+ const text = readArch(f);
1426
+ if (text === null)
1427
+ return [f, []];
1428
+ try {
1429
+ return [f, archParser.extractSymbols(text, langForPath(f))
1430
+ .filter((sy) => sy.kind !== 'class')
1431
+ .map((sy) => ({ startLine: sy.startLine, endLine: sy.endLine }))];
1432
+ }
1433
+ catch {
1434
+ return [f, []];
1435
+ }
1436
+ }));
1437
+ const arch = architectureObservation(ftt, spans, graph.importEdges(), (f) => { const t = readArch(f); return t === null ? null : t.split('\n').length; });
1438
+ if (impact || density.length > 0 || cycleFindings.length > 0 || arch.length > 0) {
1379
1439
  const graphAsOf = (() => { try {
1380
1440
  return fs.statSync(dbPath).mtime.toISOString();
1381
1441
  }
@@ -1385,6 +1445,9 @@ function makeRawHandlers(store, opts) {
1385
1445
  graphPreview = {
1386
1446
  ...(impact ? { impact } : {}),
1387
1447
  ...(density.length > 0 ? { density } : {}),
1448
+ ...(cycleFindings.length > 0
1449
+ ? { cycles: { findings: cycleFindings, note: CYCLE_ADVISORY_NOTE } } : {}),
1450
+ ...(arch.length > 0 ? { architecture: arch } : {}),
1388
1451
  ...(graphAsOf ? { graphAsOf } : {}),
1389
1452
  };
1390
1453
  }
@@ -2304,6 +2367,9 @@ function makeRawHandlers(store, opts) {
2304
2367
  // Record ONLY an actually-executed, GREEN run (review C4/C7): a red suite must not stand as
2305
2368
  // coverage evidence at the moment the code is broken, and a no-op run must not overwrite a real
2306
2369
  // record with a dishonest `passed: true` over an empty result.
2370
+ // @implements A-SPEC-578.4 — read BEFORE the write below replaces it: the calibration window
2371
+ // is "since the previous evidence run", and this line is the only moment that value exists.
2372
+ const previousEvidenceTs = (0, test_evidence_1.readTestEvidence)(root)?.ts;
2307
2373
  const verified = (0, baseline_1.shouldRecordBaseline)(result);
2308
2374
  if (verified) {
2309
2375
  (0, test_evidence_1.writeTestEvidence)(root, { ts: new Date().toISOString(), head, tier: testScope.tier, passed: true, executedByAspec });
@@ -2319,6 +2385,19 @@ function makeRawHandlers(store, opts) {
2319
2385
  // actually executed and passed. A red or skipped run must never become the reference point for
2320
2386
  // "since the last verified state" — that would silently narrow every later scope against a
2321
2387
  // state nobody verified.
2388
+ // @implements A-SPEC-578.4 — close the calibration loop on real work.
2389
+ // The window is the PREVIOUS evidence run's timestamp, read before this run overwrote it —
2390
+ // an analysis older than that belonged to a previous slice, and attributing today's changes
2391
+ // to it would be contamination rather than measurement. Its own try: an observation that
2392
+ // could change `passed` would be a gate wearing another name.
2393
+ let calibrationClosed;
2394
+ try {
2395
+ const closed = (0, maintenance_evidence_1.closeOpenArtifacts)(path.join(root, maintenance_evidence_1.EVIDENCE_DIR), { files: changedFiles }, new Date().toISOString(), previousEvidenceTs).closed;
2396
+ if (closed.length > 0)
2397
+ calibrationClosed = closed;
2398
+ }
2399
+ catch { /* the loop is observation; it never touches the verdict */ }
2400
+ // @implements A-SPEC-128
2322
2401
  let baseline;
2323
2402
  if (verified) {
2324
2403
  baseline = a.mark ?? DEFAULT_BASELINE;
@@ -2330,7 +2409,8 @@ function makeRawHandlers(store, opts) {
2330
2409
  ranFiles: result.ranFiles, executedByAspec, tail: result.tail,
2331
2410
  // @implements A-SPEC-130 — the remediation rides in the answer: these are the files to anchor.
2332
2411
  unresolvedFiles: testScope.unresolvedFiles,
2333
- changeSource, ...(scopeFallback ? { scopeFallback } : {}), ...(baseline ? { baselineRecorded: baseline } : {}) };
2412
+ changeSource, ...(scopeFallback ? { scopeFallback } : {}), ...(baseline ? { baselineRecorded: baseline } : {}),
2413
+ ...(calibrationClosed ? { calibrationClosed } : {}) };
2334
2414
  },
2335
2415
  async issue_localize(a) {
2336
2416
  assertSpecStoreReachable('issue_localize', store, a.root); // @implements A-SPEC-419
@@ -2515,7 +2595,7 @@ function makeRawHandlers(store, opts) {
2515
2595
  commitTextBoost[hit.file] = hit.score / top;
2516
2596
  }
2517
2597
  catch { /* no history, no boost — the ranking falls back to lexical evidence alone */ }
2518
- const analysis = (0, maintenance_analyze_1.analyzeMaintenance)({
2598
+ const analyzeWith = (defUse) => (0, maintenance_analyze_1.analyzeMaintenance)({
2519
2599
  semantic,
2520
2600
  ...common,
2521
2601
  coverage: { ...common.coverage, historyStatus },
@@ -2524,7 +2604,43 @@ function makeRawHandlers(store, opts) {
2524
2604
  commitTextBoost,
2525
2605
  contextBundle,
2526
2606
  groundTruth: a.groundTruth,
2607
+ defUse,
2527
2608
  });
2609
+ // @implements A-SPEC-573.4 — def-use for the TOP CANDIDATES ONLY. Extracting it for the whole
2610
+ // repository costs +88.8% (measured 2026-09-08), well past this slice's budget; the first pass
2611
+ // says which handful of files are worth parsing, and the second pass reads their data flow.
2612
+ // Every step is fail-open: a parse failure, an unsupported language or a missing file leaves
2613
+ // the candidate's symbols exactly as the first pass produced them.
2614
+ const firstPass = analyzeWith();
2615
+ const analysis = (() => {
2616
+ const targets = firstPass.candidates.slice(0, defuse_bound_1.DEFUSE_TOP_FILES).map((c) => c.file);
2617
+ if (targets.length === 0)
2618
+ return firstPass;
2619
+ const defUse = {};
2620
+ try {
2621
+ const { TreeSitterTsParser, hasDataFlowWalk } = require('../cpg/language-parser');
2622
+ const { langForPath } = require('../cpg/cpg-scanner');
2623
+ const parser = new TreeSitterTsParser();
2624
+ for (const file of targets) {
2625
+ try {
2626
+ // The LANGUAGE matters: the first wiring omitted it and parsed Python as TypeScript,
2627
+ // which produced wrong facts and cost the second corpus 0.2376 -> 0.1741 on the
2628
+ // symbol axis. A language with no walk is skipped rather than guessed at.
2629
+ const lang = langForPath(file);
2630
+ if (!hasDataFlowWalk(lang))
2631
+ continue;
2632
+ const facts = parser.extractDataFlow(fs.readFileSync(path.join(root, file), 'utf8'), lang);
2633
+ if (facts !== undefined)
2634
+ defUse[file] = facts;
2635
+ }
2636
+ catch { /* one unreadable or unparseable file must not cost the other nine */ }
2637
+ }
2638
+ }
2639
+ catch {
2640
+ return firstPass;
2641
+ }
2642
+ return Object.keys(defUse).length === 0 ? firstPass : analyzeWith(defUse);
2643
+ })();
2528
2644
  // @implements A-SPEC-268 — persistence is OPT-IN. The tool is advertised read-only, and a
2529
2645
  // regression pins that a cold project gains no `.ax/cpg_cache`; writing evidence by default
2530
2646
  // would break that contract for every caller who only wanted to look.
@@ -2544,9 +2660,19 @@ function makeRawHandlers(store, opts) {
2544
2660
  if (digest)
2545
2661
  fileDigests[file] = digest;
2546
2662
  }
2663
+ // The artifact takes the WHOLE analysis, never the bounded one below: a file has no
2664
+ // context window, and the calibration that scores this prediction must score what the
2665
+ // product actually predicted.
2547
2666
  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));
2548
2667
  }
2549
- return result;
2668
+ // @implements A-SPEC-578.5 — the response, and only the response, is bounded. Measured
2669
+ // 2026-09-09: the full shape is 187,174 characters (~47k tokens) and the harness refuses
2670
+ // it, which is why AGENTS.md step 3 had never once been obeyed with `persist: true`.
2671
+ // `persistedTo` rides along so the caller knows where the whole thing is.
2672
+ const bounded = (0, maintenance_analyze_1.boundAnalysis)(result);
2673
+ if (JSON.stringify(bounded).length !== JSON.stringify(result).length)
2674
+ bounded.bounded = true;
2675
+ return bounded;
2550
2676
  }
2551
2677
  finally {
2552
2678
  graph.close();
@@ -3092,7 +3218,7 @@ function makeRawHandlers(store, opts) {
3092
3218
  summary: `consumed single-use approval for: review-resolve ${envLiftedCriticals.join(', ')}`.slice(0, 200),
3093
3219
  inputs: [(0, provenance_chain_1.nonceFingerprint)(String(approval.nonce))], rationale: approval.rationale,
3094
3220
  authorization: (0, provenance_chain_1.authorizationRef)(approval.actor, approval.token),
3095
- });
3221
+ }, { isNonceConsumed: (0, ledger_store_2.nonceConsumedIn)(ledgerFile) });
3096
3222
  if (!won) {
3097
3223
  throw new HandlerRefusal(`review_record: 단일 사용 승인(nonce)이 이미 소비되었습니다 — 재사용은 거부됩니다. 새 승인을 발급받으십시오`);
3098
3224
  }
@@ -3222,7 +3348,7 @@ function makeRawHandlers(store, opts) {
3222
3348
  summary: `consumed single-use approval for: ${coverTarget.kind} ${(0, provenance_chain_1.redactTarget)('command', coverTarget.target)}`.slice(0, 200),
3223
3349
  inputs: [(0, provenance_chain_1.nonceFingerprint)(String(a.approval.nonce))], rationale: a.approval.rationale,
3224
3350
  authorization: (0, provenance_chain_1.authorizationRef)(a.approval.actor, a.approval.token),
3225
- });
3351
+ }, { isNonceConsumed: (0, ledger_store_2.nonceConsumedIn)(ledgerFile) });
3226
3352
  }
3227
3353
  catch {
3228
3354
  won = false;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Narrow history-derived candidates to files that could be source at all.
3
+ *
4
+ * The predicate is the SCANNER's, not the replay corpus's truth predicate — scoring against a
5
+ * filter copied from the metric would be gaming it. Deliberately NOT membership in the scanned
6
+ * set: a file a commit CREATES does not exist in the parent-time scan, and admitting exactly such
7
+ * files is A-SPEC-388's boundary contract. Every offender measured above is excluded by the
8
+ * extension test alone (.jsonl / .json / .md), so the stronger predicate would have cost that
9
+ * contract and bought nothing.
10
+ *
11
+ * Order is preserved because the downstream RRF lists are position-indexed.
12
+ */
13
+ export declare function admitHistoryFiles(historyFiles: readonly string[]): string[];
14
+ /** The lexical path's predicate (localize.ts), restated so both channels demote the same trees. */
15
+ export declare const isVendorPath: (p: string) => boolean;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isVendorPath = void 0;
4
+ exports.admitHistoryFiles = admitHistoryFiles;
5
+ // @implements A-SPEC-573.1
6
+ // The commit-prose channel (A-SPEC-388) lets files the lexical layer never scored ENTER the
7
+ // candidate pool — that is where its measured gain comes from (Top-10 recall 0.393 -> 0.601). But
8
+ // its keys come from git history, so it also admits files that CANNOT be the answer: this
9
+ // repository's ledger JSONL, the last-green baseline, CHANGELOG.md. Measured 2026-09-08 over 12
10
+ // real commit-subject requests: 76 of 120 emitted candidate slots (63.3%) went to such files, and
11
+ // one took the emission head. The channel is not the problem; its POPULATION is.
12
+ const cpg_scanner_1 = require("../cpg/cpg-scanner");
13
+ /**
14
+ * Narrow history-derived candidates to files that could be source at all.
15
+ *
16
+ * The predicate is the SCANNER's, not the replay corpus's truth predicate — scoring against a
17
+ * filter copied from the metric would be gaming it. Deliberately NOT membership in the scanned
18
+ * set: a file a commit CREATES does not exist in the parent-time scan, and admitting exactly such
19
+ * files is A-SPEC-388's boundary contract. Every offender measured above is excluded by the
20
+ * extension test alone (.jsonl / .json / .md), so the stronger predicate would have cost that
21
+ * contract and bought nothing.
22
+ *
23
+ * Order is preserved because the downstream RRF lists are position-indexed.
24
+ */
25
+ function admitHistoryFiles(historyFiles) {
26
+ const admitted = historyFiles.filter((file) => !file.endsWith('.d.ts') && cpg_scanner_1.SCANNABLE_EXTENSIONS.some((ext) => file.endsWith(ext)));
27
+ // @implements A-SPEC-573.2 — the lexical path halves a vendored file's score AND orders vendored
28
+ // last (localize.ts). Entering at score 0, this channel bypassed both: measured 2026-09-08, the
29
+ // only two non-source candidates the extension test let through were vendored files and BOTH held
30
+ // the emission head. Demote, never drop — vendored code is the answer in some projects, which is
31
+ // why the lexical path orders rather than filters. Stable within each group: the downstream RRF
32
+ // lists are position-indexed.
33
+ return [...admitted.filter((f) => !(0, exports.isVendorPath)(f)), ...admitted.filter(exports.isVendorPath)];
34
+ }
35
+ /** The lexical path's predicate (localize.ts), restated so both channels demote the same trees. */
36
+ const isVendorPath = (p) => /(^|\/)(reference|vendor|vendors|third_party|third-party|external)\//i.test(p);
37
+ exports.isVendorPath = isVendorPath;
@@ -117,6 +117,14 @@ export interface MaintenanceAnalysisInput {
117
117
  * that is the whole result.
118
118
  */
119
119
  commitTextBoost?: Record<string, number>;
120
+ /**
121
+ * @implements A-SPEC-573.4
122
+ * Def-use facts for a BOUNDED set of files — the caller extracts them, because this core does no
123
+ * I/O. Extracting them for the whole repository costs +88.8% (measured 2026-09-08, 2572ms ->
124
+ * 4857ms), which is why the callers pass the top candidates only. Absent means "not extracted",
125
+ * and the symbols then read exactly as they did before this existed.
126
+ */
127
+ defUse?: Record<string, import('../cpg/language-parser').DataFlowFacts>;
120
128
  contextBundle?: ContextBundle | null;
121
129
  /**
122
130
  * @implements A-SPEC-290
@@ -436,3 +444,40 @@ export declare function unquoteGitPath(line: string): string;
436
444
  * reproduce and compare an analysis from the exact same captured inputs.
437
445
  */
438
446
  export declare function analyzeMaintenance(input: MaintenanceAnalysisInput): MaintenanceAnalysis;
447
+ /**
448
+ * Items a response lists before it starts counting instead.
449
+ *
450
+ * TEN, because that is the unit this product is measured in: localization emits Top-10 and every
451
+ * recall figure in this repository is recall@10. Aligning the response cap with the emission unit
452
+ * is a reason; picking a number that happens to hit a size target is a knob. Measured on the real
453
+ * 187,174-character response: cap 20 gave 34,811 with `intent` alone at 11,037 (32%), cap 10 fits.
454
+ */
455
+ export declare const ANALYZE_LIST_CAP = 10;
456
+ /** Characters one listed item keeps. */
457
+ export declare const ANALYZE_TEXT_CAP = 400;
458
+ /**
459
+ * @implements A-SPEC-578.5
460
+ * The RESPONSE-EDGE projection. The artifact on disk keeps everything.
461
+ *
462
+ * Measured 2026-09-09: one `maintenance_analyze` call returns 187,174 characters (~47k tokens) and
463
+ * the harness refuses it outright. AGENTS.md step 3 instructs every agent to make that call before
464
+ * editing source — and across 2,779 spec approvals `persist:true` had never been used once. An
465
+ * instruction nobody can afford to follow is not an instruction.
466
+ *
467
+ * The cap is applied RECURSIVELY, which the first cut got wrong. Capping only the top level took
468
+ * the response from 187,174 to 142,701 and no further, because the weight is in NESTED DUPLICATES:
469
+ * `impacts.test` and `testScope.impactedTestFiles` carry the same 158 paths, and `impacts.contract`
470
+ * and `testScope.impactedSpecs` the same 160 spec ids. A cap that stops at depth one caps the
471
+ * cheapest arrays in the document.
472
+ *
473
+ * NEVER CALLED FROM `analyzeMaintenance`. The design-time advisory named the replay benchmark
474
+ * (`run-replay.ts`, `replay-corpus.ts`, `commit-text.ts`, `temporal-prior.ts`) as readers of this
475
+ * type, and among their anchors sits A-SPEC-402 — "the numbers reported are not what the product
476
+ * does". Bounding inside the analysis would make every pinned benchmark score a truncated
477
+ * pipeline, which is the most expensive instrument failure this repository has recorded. The
478
+ * benchmark keeps seeing exactly what it sees today; only the conversation gets the short form.
479
+ *
480
+ * What is dropped is COUNTED, at whatever depth it was dropped. A response that quietly shrank
481
+ * would be a worse lie than a long one.
482
+ */
483
+ export declare function boundAnalysis(a: MaintenanceAnalysis): MaintenanceAnalysis;