@holmes-lab/holmes-kit 0.21.0 → 0.22.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 (33) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/.build-id +1 -1
  3. package/dist/holmes/cli/agents.js +1 -0
  4. package/dist/holmes/cli/doctor.d.ts +1 -0
  5. package/dist/holmes/cli/doctor.js +42 -0
  6. package/dist/holmes/cli/index.js +1 -0
  7. package/dist/holmes/cli/init.js +1 -0
  8. package/dist/holmes/cpg/language-parser-walk.js +1 -0
  9. package/dist/holmes/hooks/stop.d.ts +7 -0
  10. package/dist/holmes/hooks/stop.js +54 -1
  11. package/dist/holmes/mcp/handlers/operator-inspection.d.ts +27 -1
  12. package/dist/holmes/mcp/handlers/operator-inspection.js +68 -2
  13. package/dist/holmes/mcp/handlers/spec-approval.d.ts +5 -0
  14. package/dist/holmes/mcp/handlers/spec-approval.js +59 -1
  15. package/dist/holmes/mcp/handlers/test-execution.d.ts +4 -0
  16. package/dist/holmes/mcp/handlers/test-execution.js +6 -2
  17. package/dist/holmes/mcp/handlers.d.ts +31 -1
  18. package/dist/holmes/mcp/handlers.js +1 -0
  19. package/dist/holmes/mcp/maintenance-analyze.js +1 -0
  20. package/dist/holmes/mcp/tool-schemas.js +1 -0
  21. package/dist/holmes/project/ci-runs.d.ts +46 -0
  22. package/dist/holmes/project/ci-runs.js +137 -0
  23. package/dist/holmes/project/install-scripts-policy.js +1 -0
  24. package/dist/holmes/review/evaluation-metrics.js +1 -0
  25. package/dist/holmes/review/kills-check.d.ts +40 -0
  26. package/dist/holmes/review/kills-check.js +147 -0
  27. package/dist/holmes/review/manual-baseline.js +1 -0
  28. package/dist/holmes/rtm/advisory-outcomes.d.ts +137 -0
  29. package/dist/holmes/rtm/advisory-outcomes.js +314 -0
  30. package/dist/holmes/rtm/rtm-graph.js +1 -0
  31. package/dist/holmes/rtm/taint-benchmark.js +1 -0
  32. package/package.json +1 -1
  33. package/playbooks/author-slice/PLAYBOOK.md +19 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,58 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
  <!-- @implements A-SPEC-209 -->
8
+ ## [0.22.0] - 2026-09-18
9
+
10
+ The advisories learn what happened next. Until now this repository issued findings and recorded that
11
+ it had issued them; nothing recorded whether anyone acted. That missing half is the numerator every
12
+ "promote this to a hard gate once we know the false-positive rate" sentence needed.
13
+
14
+ ### Added
15
+ - **Advisory reaction ledger** (A-SPEC-663). Every finding — impact-advisory, anchor-density,
16
+ Files-to-Touch fulfilment, trace-gap, kills-unapplicable — now has a deterministic id (kind + spec +
17
+ canonical payload). `spec_approve` records it as `issued` and answers with `advisoryIds`; the next
18
+ `approval_status` re-runs the SAME finding functions and records `resolved` when the cause is gone or
19
+ `persisted` when it is not, at most one row per finding per day. An author who judges a finding
20
+ unhelpful passes `dismiss: [id]` to `spec_approve`; an id the ledger does not know comes back in
21
+ `dismissUnknown` rather than inventing a row. `rtm_dashboard`'s census gains `advisoryOutcomes`
22
+ (issued / resolved / persisted / dismissed per kind) and `approval_status` gains `advisoryHistory`.
23
+ Rows live in `.ax/ledger/advisory-outcomes.<replica>.jsonl` and carry ids, spec ids, outcome words and
24
+ commit hashes only. Queries never issue a finding the seal did not, so the observation denominator
25
+ stays the seal's.
26
+ - **`kills` applicability** (A-SPEC-662). `test_run --mutate` now reports mutations that never applied
27
+ as `unapplied`, separately from `survivors` — measured here, all 22 `kills` entries in this repository
28
+ write `where` as a file path and `mutate` as prose, so the literal-replacement engine applied none of
29
+ them and the response still read `survivors: []`, the shape of a clean run. Sealing a T-SPEC (and
30
+ previewing it with `approval_status`) reports entries whose `where` literal is absent from the
31
+ A-SPEC's anchored production source as a `kills-unapplicable` finding. Never blocking. The
32
+ author-slice playbook now shows the grammar.
33
+ - **CI matrix, minimal form** (A-SPEC-664). `scripts/ci-orb-linux.sh` runs one commit's full suite on an
34
+ OrbStack Linux machine and appends exactly one row to `.ax/ledger/ci-runs.<host>.jsonl` — for every
35
+ outcome, including the ones where the run could not judge (`clone-failed`, `install-failed`,
36
+ `vm-unreachable`). A watch script and a launchd agent trigger it. The Stop hook reports the matrix's
37
+ last word on a tracked line and `doctor` gains a `ci matrix` check, and neither ever reads a missing
38
+ row as green: absence is "not run". The scripts are maintainer tools and are not shipped; a workspace
39
+ with no `ci-runs` ledger hears nothing about the matrix at all.
40
+
41
+ ### Fixed
42
+ - **Trace-gap anchors: 21 approved specs joined the graph.** Each declared a scanned production file in
43
+ its Files to Touch while anchoring only tests, so `rtm_impact` could not see them. Their anchors now
44
+ sit in the files they declared. Unlinked approved specs fell from 73 to 52 and trace gaps from 21 to
45
+ 0; `codeLinkedPct` rose from 88.6 to 91.9. The remaining 52 declare no scanned production file — they
46
+ are test- and document-only specs, not gaps.
47
+ - **The runner counted focused suites as failures.** jest reports a suite that used `test.only` with
48
+ status `focused`; filtering on `!== 'passed'` made five healthy suites look red in the first two
49
+ Linux rows. Only `failed` counts now.
50
+
51
+ ### Measured
52
+ - macOS: 524 suites / 6,581 passed / 11 skipped; every slice recorded red-assertion → green; official
53
+ `test_run` green at each step.
54
+ - Linux (OrbStack Ubuntu 24.04 arm64, Node 22): five recorded runs, the last one unattended via launchd.
55
+ The residual reds are load-dependent suites (`dashboard.test`, `entity-store-boundaries`,
56
+ `entity-git-snapshot`) that pass when run alone; the host was running other agents' work at the time.
57
+ - First reaction rows in this repository: 22 trace-gap findings issued, 22 resolved by the anchor move.
58
+ - Windows: not re-run for this release; no Windows-specific change landed.
59
+
8
60
  ## [0.21.0] - 2026-09-17
9
61
 
10
62
  The backlog-hardening cycle: the RTM stops claiming coverage it cannot see, sealing reports what a
package/dist/.build-id CHANGED
@@ -1 +1 @@
1
- bdbd4f85-mu4ywfad
1
+ 71586207-mu68mv0a
@@ -40,6 +40,7 @@ exports.antigravityHookWarnings = antigravityHookWarnings;
40
40
  exports.mergeAgentsMd = mergeAgentsMd;
41
41
  exports.agentFiles = agentFiles;
42
42
  exports.agentLinks = agentLinks;
43
+ // @implements A-SPEC-202, A-SPEC-250, A-SPEC-341
43
44
  // @implements A-SPEC-442
44
45
  // @implements A-SPEC-193
45
46
  const path = __importStar(require("node:path"));
@@ -171,6 +171,7 @@ export declare function wiringSpawnCheck(command: string, args: string[], timeou
171
171
  * the same honest-diagnosis lineage as the codex-wiring WARN (never a gate).
172
172
  */
173
173
  export declare function pushGateCheck(target: string): Check | null;
174
+ export declare function ciMatrixCheck(target: string): Check | null;
174
175
  export declare function formatChecks(checks: Check[]): string;
175
176
  /**
176
177
  * Prove that EVERY wired harness can actually start a server, not merely that its file parses.
@@ -47,10 +47,12 @@ exports.resolveWiringPath = resolveWiringPath;
47
47
  exports.runDoctor = runDoctor;
48
48
  exports.wiringSpawnCheck = wiringSpawnCheck;
49
49
  exports.pushGateCheck = pushGateCheck;
50
+ exports.ciMatrixCheck = ciMatrixCheck;
50
51
  exports.formatChecks = formatChecks;
51
52
  exports.wiringHandshakeChecks = wiringHandshakeChecks;
52
53
  exports.semanticTierVerdict = semanticTierVerdict;
53
54
  exports.detectTreeKeyTemporary = detectTreeKeyTemporary;
55
+ // @implements A-SPEC-264, A-SPEC-423, A-SPEC-549.1, A-SPEC-590
54
56
  // @implements A-SPEC-594
55
57
  // @implements A-SPEC-592
56
58
  // @implements A-SPEC-591
@@ -65,6 +67,7 @@ const native_deps_1 = require("./native-deps");
65
67
  const tier_1 = require("../semantic/tier");
66
68
  const probe_process_1 = require("./probe-process");
67
69
  const npx_cache_check_1 = require("./npx-cache-check");
70
+ const ci_runs_1 = require("../project/ci-runs");
68
71
  const path = __importStar(require("node:path"));
69
72
  const role_policy_1 = require("../governance/role-policy");
70
73
  const blind_spots_1 = require("../guardrail/blind-spots");
@@ -1261,6 +1264,9 @@ async function runDoctor(packageRoot, target, opts, extraChecks) {
1261
1264
  const pg = pushGateCheck(target ?? process.cwd());
1262
1265
  if (pg)
1263
1266
  checks.push(pg);
1267
+ const ci = ciMatrixCheck(target ?? process.cwd()); // @implements A-SPEC-664
1268
+ if (ci)
1269
+ checks.push(ci);
1264
1270
  }
1265
1271
  if (extraChecks) {
1266
1272
  checks.push(...extraChecks);
@@ -1554,6 +1560,42 @@ function pushGateCheck(target) {
1554
1560
  return null;
1555
1561
  } // diagnosis must never crash doctor
1556
1562
  }
1563
+ // @implements A-SPEC-664
1564
+ // The CI matrix as doctor sees it: PASS only for a green row at HEAD; everything else is a WARN that
1565
+ // says exactly what the ledger says — "not run" when there is no row, the failed suites when red,
1566
+ // the infrastructure status when the run could not judge. Never a FAIL: the matrix informs.
1567
+ function ciMatrixCheck(target) {
1568
+ try {
1569
+ if (!fs.existsSync(path.join(target, '.ax')))
1570
+ return null;
1571
+ if (!(0, ci_runs_1.hasCiLedger)(target))
1572
+ return null; // @implements A-SPEC-664 — same adoption predicate as the hook
1573
+ const runs = (0, ci_runs_1.readCiRuns)(target);
1574
+ let head;
1575
+ try {
1576
+ head = (0, node_child_process_1.execFileSync)('git', ['rev-parse', 'HEAD'], { cwd: target, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
1577
+ }
1578
+ catch {
1579
+ head = undefined;
1580
+ }
1581
+ const behind = (rev) => {
1582
+ try {
1583
+ return Number((0, node_child_process_1.execFileSync)('git', ['rev-list', '--count', `${rev}..HEAD`], { cwd: target, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim());
1584
+ }
1585
+ catch {
1586
+ return undefined;
1587
+ }
1588
+ };
1589
+ const v = (0, ci_runs_1.ciVerdict)(runs, 'linux', head, behind);
1590
+ const detail = (0, ci_runs_1.ciStatusLine)(v);
1591
+ if (v.state === 'green' && v.behind === 0)
1592
+ return { name: 'ci matrix', level: 'PASS', detail };
1593
+ return { name: 'ci matrix', level: 'WARN', detail, fix: 'Run scripts/ci-orb-linux.sh HEAD (or install the launchd agent: scripts/ci-orb-linux-install.sh)' };
1594
+ }
1595
+ catch {
1596
+ return null;
1597
+ } // diagnosis must never crash doctor
1598
+ }
1557
1599
  function formatChecks(checks) {
1558
1600
  const lines = checks.map((c) => {
1559
1601
  const head = `${c.level.padEnd(4)} ${c.name} — ${c.detail}`;
@@ -37,6 +37,7 @@ exports.isBrokenPipe = void 0;
37
37
  exports.packageRoot = packageRoot;
38
38
  exports.main = main;
39
39
  exports.installPipeGuard = installPipeGuard;
40
+ // @implements A-SPEC-549.2
40
41
  // @implements A-SPEC-591, A-SPEC-626
41
42
  const cli_execution_1 = require("./cli-execution");
42
43
  // @implements A-SPEC-100.2
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.settingsPathOf = exports.MATCHERS = exports.SERVER_NAME = void 0;
37
37
  exports.buildHookPlan = buildHookPlan;
38
38
  exports.runInit = runInit;
39
+ // @implements A-SPEC-340
39
40
  // @implements A-SPEC-100.2
40
41
  const fs = __importStar(require("node:fs"));
41
42
  const path = __importStar(require("node:path"));
@@ -1,3 +1,4 @@
1
+ // @implements A-SPEC-302
1
2
  'use strict';
2
3
  // Single source of truth for the per-language symbol/edge tree-walking
3
4
  // logic. Called from BOTH the inline (production) parse path and the
@@ -2,7 +2,9 @@ import { PendingRequest } from '../governance/approval-queue';
2
2
  import { Spec } from '../spec/spec-parser';
3
3
  import type { TestOutcome } from '../review/test-runner';
4
4
  import { type KnownDefectJudgement } from '../rtm/known-defects';
5
+ import { type CiVerdict } from '../project/ci-runs';
5
6
  export declare function collectKnownDefects(root: string, now: Date): KnownDefectJudgement | undefined;
7
+ export declare function collectCiVerdicts(root: string, now?: Date): CiVerdict[];
6
8
  /**
7
9
  * @implements A-SPEC-100.2
8
10
  * Stop-hook governance gate (Phase-2 #1: push, not pull).
@@ -31,6 +33,11 @@ export interface StopEvidence {
31
33
  * `undefined` is no signal — a walk that could not run must not read as marker-free.
32
34
  */
33
35
  knownDefects?: KnownDefectJudgement;
36
+ /**
37
+ * @implements A-SPEC-664 — what the CI matrix last said about this tree, per OS. Absent when the
38
+ * hook could not look; a `not-run` verdict when it looked and found no row (never silence).
39
+ */
40
+ ci?: CiVerdict[];
34
41
  /** Provenance-chain verification result (CLI-supplied). A broken chain blocks the stop. */
35
42
  provenance?: {
36
43
  ok: boolean;
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.MAX_CONSECUTIVE_BLOCKS = void 0;
37
37
  exports.collectKnownDefects = collectKnownDefects;
38
+ exports.collectCiVerdicts = collectCiVerdicts;
38
39
  exports.changedAnchoredAspecs = changedAnchoredAspecs;
39
40
  exports.unanchoredChangedSources = unanchoredChangedSources;
40
41
  exports.dependencyReappraisals = dependencyReappraisals;
@@ -79,6 +80,7 @@ const constitution_debt_1 = require("../governance/constitution-debt");
79
80
  const root_1 = require("../project/root");
80
81
  const known_defects_1 = require("../rtm/known-defects");
81
82
  const test_files_1 = require("../cpg/test-files");
83
+ const ci_runs_1 = require("../project/ci-runs");
82
84
  // @implements A-SPEC-660 — the I/O half of the known-defect marker: walk the workspace's test files
83
85
  // (the same directory rule and test predicate ART-4's anchor scan uses), parse each for markers,
84
86
  // judge them against the injected clock. A walk that cannot START is NO SIGNAL (undefined) — never
@@ -126,6 +128,40 @@ function collectKnownDefects(root, now) {
126
128
  return undefined;
127
129
  }
128
130
  }
131
+ // @implements A-SPEC-664 — the I/O half of the CI line: read the ci-runs ledger, judge Linux (always)
132
+ // and any other OS that has a row, with git measuring how far each judged commit sits behind HEAD.
133
+ // A tree without a ledger yields a `not-run` verdict — a line, not an absence.
134
+ function collectCiVerdicts(root, now = new Date()) {
135
+ void now;
136
+ // @implements A-SPEC-664 — a workspace that never ran the matrix hears nothing about it. Past this
137
+ // line the matrix IS adopted here, so a missing row is reported rather than passed over in silence.
138
+ if (!(0, ci_runs_1.hasCiLedger)(root))
139
+ return [];
140
+ let runs = [];
141
+ try {
142
+ runs = (0, ci_runs_1.readCiRuns)(root);
143
+ }
144
+ catch {
145
+ runs = [];
146
+ }
147
+ let head;
148
+ try {
149
+ head = (0, node_child_process_1.execFileSync)('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], env: (0, root_1.cleanSubprocessEnv)() }).trim();
150
+ }
151
+ catch {
152
+ head = undefined;
153
+ }
154
+ const behind = (rev) => {
155
+ try {
156
+ return Number((0, node_child_process_1.execFileSync)('git', ['rev-list', '--count', `${rev}..HEAD`], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], env: (0, root_1.cleanSubprocessEnv)() }).trim());
157
+ }
158
+ catch {
159
+ return undefined;
160
+ }
161
+ };
162
+ const oses = ['linux', ...[...new Set(runs.map((r) => r.os))].filter((o) => o !== 'linux').sort()];
163
+ return oses.map((os) => (0, ci_runs_1.ciVerdict)(runs, os, head, behind));
164
+ }
129
165
  /**
130
166
  * @implements A-SPEC-534.4
131
167
  * ART-8 evidence (I/O half): the A-SPECs whose DIRTY source files carry an @implements anchor. git is
@@ -492,6 +528,7 @@ const TRACK_LABELS = {
492
528
  'ART-8': 'RED-first',
493
529
  'ART-2': 'code-graph cycles',
494
530
  'ART-9': 'known-defect debt',
531
+ 'CI': 'matrix',
495
532
  };
496
533
  /**
497
534
  * One line per ARTICLE, each under its own name.
@@ -557,6 +594,14 @@ function evaluateStop(specs, evidence) {
557
594
  const t = evidence.knownDefects.unexpired.map((k) => ({ article: 'ART-9', detail: `${k.file}:${k.line}: known defect "${k.reason}" until ${k.expires}` }));
558
595
  tracked = [...(tracked ?? []), ...t];
559
596
  }
597
+ // @implements A-SPEC-664 — the CI matrix's last word, one tracked line per OS. Never a block: a
598
+ // red Linux run is information for the person closing the turn, and "not run" is said out loud
599
+ // because the alternative — silence — reads as green (measured 2026-09-17: 136 environmental reds
600
+ // surfaced only because someone ran the suite by hand before a release).
601
+ if (evidence?.ci && evidence.ci.length > 0) {
602
+ const t = evidence.ci.map((v) => ({ article: 'CI', detail: (0, ci_runs_1.ciStatusLine)(v) }));
603
+ tracked = [...(tracked ?? []), ...t];
604
+ }
560
605
  const problems = violations.map((x) => `[${x.article}] ${x.detail}`);
561
606
  // @implements A-SPEC-247 — structured list so the caller can ask acknowledgeStop which of these
562
607
  // are waiting on an owner. Mirrors `problems` exactly, including the two synthesized below.
@@ -1141,7 +1186,15 @@ if (require.main === module) {
1141
1186
  catch { /* maintenance, never a hook failure */ }
1142
1187
  // @implements A-SPEC-660 — the marker walk: no signal when it cannot run (never a clean verdict).
1143
1188
  const knownDefects = collectKnownDefects(stopProjectRoot(), new Date());
1144
- let out = evaluateStop(specs, { testCasesByAspec, provenance, executedByAspec, findings, findingsUnreadable, unanchoredChangedSources: unanchored, unrecordedApprovals: unrecorded, rolledBackLedgers: rolledBack, redFirstMode, changedAspecs, outcomesByAspec, ...(knownDefects ? { knownDefects } : {}) });
1189
+ // @implements A-SPEC-664 the CI matrix line rides beside the constitution's verdict.
1190
+ let ci;
1191
+ try {
1192
+ ci = collectCiVerdicts(stopProjectRoot());
1193
+ }
1194
+ catch {
1195
+ ci = undefined;
1196
+ }
1197
+ let out = evaluateStop(specs, { testCasesByAspec, provenance, executedByAspec, findings, findingsUnreadable, unanchoredChangedSources: unanchored, unrecordedApprovals: unrecorded, rolledBackLedgers: rolledBack, redFirstMode, changedAspecs, outcomesByAspec, ...(knownDefects ? { knownDefects } : {}), ...(ci ? { ci } : {}) });
1145
1198
  // @implements A-SPEC-534.4 — track mode records ART-8 findings without blocking: surface them so
1146
1199
  // the operator observes RED-first gaps before an owner promotes the posture to strict.
1147
1200
  // @implements A-SPEC-559.2 — spec-evolution trigger (observe-first, NEVER blocks): a dirty
@@ -15,6 +15,11 @@ export interface OperatorInspectionContext {
15
15
  reason: string;
16
16
  };
17
17
  fetchJson(url: string): Promise<any>;
18
+ /** A-SPEC-663 — the production scan, for the trace-gap finding (same closure test_run uses). */
19
+ cachedScan?(root: string, repoRoot?: string): Array<{
20
+ path: string;
21
+ implementsSpecs?: string[];
22
+ }>;
18
23
  /** A-SPEC-642 — the shared approval channel and queue hint, for the reconciliation act. */
19
24
  resolveHandlerApproval(root: string | undefined, approval: Approval | undefined, action: {
20
25
  kind: string;
@@ -46,6 +51,9 @@ export declare function createOperatorInspectionHandlers(context: OperatorInspec
46
51
  ok: boolean;
47
52
  reason: string;
48
53
  } | {
54
+ advisoryHistory?: import("../../rtm/advisory-outcomes").AdvisoryState[] | undefined;
55
+ advisoryIds?: Partial<Record<string, string>> | undefined;
56
+ killsAdvisory?: import("../../review/kills-check").KillsAdvisory | undefined;
49
57
  fttFulfilment?: import("../../rtm/ftt-fulfilment").FttFulfilment | undefined;
50
58
  graphPreview?: {
51
59
  impact?: import("../../rtm/impact-advisory").ImpactAdvisory;
@@ -162,7 +170,25 @@ export declare function createOperatorInspectionHandlers(context: OperatorInspec
162
170
  ok: boolean;
163
171
  url: string;
164
172
  running: boolean;
165
- census: import("../../server/dashboard-launcher").DashboardCensusExtended;
173
+ census: {
174
+ advisoryOutcomes?: import("../../rtm/advisory-outcomes").AdvisoryCensus | undefined;
175
+ codeLinkedPct: number;
176
+ codeLinkedCount: number;
177
+ unlinkedCount: number;
178
+ unlinkedByReason: Record<import("../../rtm/link-census").UnlinkedReason, number>;
179
+ excluded: {
180
+ total: number;
181
+ retired: number;
182
+ unmapped: number;
183
+ nonSpec: number;
184
+ };
185
+ reqCount: number;
186
+ pipelineCount: number;
187
+ coveredCount: number;
188
+ coveragePct: number;
189
+ retiredCount: number;
190
+ findingsScanned: boolean;
191
+ };
166
192
  reason?: undefined;
167
193
  }>;
168
194
  };
@@ -210,8 +210,60 @@ function createOperatorInspectionHandlers(context) {
210
210
  catch {
211
211
  fttFulfilment = undefined;
212
212
  }
213
+ // @implements A-SPEC-662 — the same kills-applicability finding sealing will report, delivered
214
+ // before sealing and without any write. Same pure function as the seal.
215
+ let killsAdvisory;
216
+ try {
217
+ if (cur.spec.type === 'T-SPEC' && a.root) {
218
+ const { parseKills } = require('../../spec/kills');
219
+ const check = require('../../review/kills-check');
220
+ const kills = parseKills(cur.spec.frontmatter);
221
+ const aspecId = cur.spec.dependsOn[0];
222
+ if (kills.length > 0 && aspecId) {
223
+ const found = check.killsAdvisory(cur.spec.id, aspecId, kills, check.anchoredSourceTexts(a.root, aspecId));
224
+ if (found)
225
+ killsAdvisory = found;
226
+ }
227
+ }
228
+ }
229
+ catch {
230
+ killsAdvisory = undefined;
231
+ }
232
+ // @implements A-SPEC-663 — the same findings, re-judged: a finding the seal issued that this read
233
+ // no longer produces is `resolved`; one still produced is `persisted` (once per day). The query
234
+ // writes only reaction rows, never issues a finding the seal did not — the observation
235
+ // denominator (REQ-572) stays the seal's.
236
+ let advisoryIds;
237
+ let advisoryHistory;
238
+ try {
239
+ if (a.root) {
240
+ const ao = require('../../rtm/advisory-outcomes');
241
+ let traceGap = null;
242
+ if (cur.spec.type === 'A-SPEC' && cur.spec.status === 'approved' && context.cachedScan) {
243
+ try {
244
+ traceGap = ao.traceGapFor(a.id, cur.spec, context.cachedScan(a.root, a.root), a.root);
245
+ }
246
+ catch {
247
+ traceGap = null;
248
+ }
249
+ }
250
+ const impactAdvisory = graphPreview?.impact ?? null;
251
+ const anchorDensity = graphPreview?.density ?? null;
252
+ const { keys, ids } = ao.currentAdvisoryKeys(a.id, { impactAdvisory, anchorDensity, fttFulfilment, killsAdvisory, traceGap });
253
+ const r = ao.recordAdvisoryOutcomes(a.root, a.id, keys, (s) => s.aspec === a.id, ao.gitHeadOf(a.root), [], new Date().toISOString(), { issue: false });
254
+ if (keys.length > 0)
255
+ advisoryIds = ids;
256
+ if (r.history.length > 0)
257
+ advisoryHistory = r.history;
258
+ }
259
+ }
260
+ catch {
261
+ advisoryIds = undefined;
262
+ advisoryHistory = undefined;
263
+ }
213
264
  return { ok: true, ...(0, approval_status_1.describeApproval)(cur.spec, context.resolver(all)), ...(graphPreview ? { graphPreview } : {}),
214
- ...(fttFulfilment ? { fttFulfilment } : {}) };
265
+ ...(fttFulfilment ? { fttFulfilment } : {}), ...(killsAdvisory ? { killsAdvisory } : {}),
266
+ ...(advisoryIds ? { advisoryIds } : {}), ...(advisoryHistory ? { advisoryHistory } : {}) };
215
267
  },
216
268
  /**
217
269
  * @implements A-SPEC-538.3
@@ -296,7 +348,21 @@ function createOperatorInspectionHandlers(context) {
296
348
  const launch = await ensureDashboard(dest.root, a.port, (opts) => startDashboardServer(opts));
297
349
  const rtm = await context.fetchJson(`${launch.url}/api/rtm`);
298
350
  const heatmap = await context.fetchJson(`${launch.url}/api/rtm/heatmap`);
299
- return { ok: true, url: launch.url, running: launch.running, census: dashboardCensusExtended(rtm, heatmap) };
351
+ // @implements A-SPEC-663 the whole-store trace-gap judgement and the reaction census: how many
352
+ // findings of each kind were issued, and how many stand resolved / persisted / dismissed now.
353
+ let advisoryOutcomes;
354
+ try {
355
+ const ao = require('../../rtm/advisory-outcomes');
356
+ if (context.cachedScan) {
357
+ const keys = ao.traceGapAdvisories(await context.listSpecs(), context.cachedScan(dest.root, dest.root), dest.root);
358
+ ao.recordAdvisoryOutcomes(dest.root, '*', keys, (s) => s.kind === 'trace-gap', ao.gitHeadOf(dest.root));
359
+ }
360
+ advisoryOutcomes = ao.advisoryCensus(ao.readAdvisoryOutcomes(dest.root));
361
+ }
362
+ catch {
363
+ advisoryOutcomes = undefined;
364
+ }
365
+ return { ok: true, url: launch.url, running: launch.running, census: { ...dashboardCensusExtended(rtm, heatmap), ...(advisoryOutcomes ? { advisoryOutcomes } : {}) } };
300
366
  }
301
367
  catch (err) {
302
368
  return { ok: false, reason: `대시보드 기동 실패: ${err?.message ?? String(err)}` };
@@ -41,6 +41,7 @@ export declare function createSpecApprovalHandlers(context: SpecApprovalContext)
41
41
  spec_approve(a: {
42
42
  root?: string;
43
43
  id: string;
44
+ dismiss?: string[];
44
45
  }): Promise<{
45
46
  ok: false;
46
47
  reason: string;
@@ -57,6 +58,10 @@ export declare function createSpecApprovalHandlers(context: SpecApprovalContext)
57
58
  reason: string;
58
59
  conflict: import("../../spec/version-conflict").ConflictDetail;
59
60
  } | {
61
+ dismissUnknown?: string[] | undefined;
62
+ advisoryHistory?: import("../../rtm/advisory-outcomes").AdvisoryState[] | undefined;
63
+ advisoryIds?: Partial<Record<string, string>> | undefined;
64
+ killsAdvisory?: import("../../review/kills-check").KillsAdvisory | undefined;
60
65
  fttFulfilment?: import("../../rtm/ftt-fulfilment").FttFulfilment | undefined;
61
66
  impactAdvisoryUnavailable?: "empty" | "unreadable" | undefined;
62
67
  impactGraph?: {
@@ -442,9 +442,67 @@ function createSpecApprovalHandlers(context) {
442
442
  catch {
443
443
  fttFulfilment = undefined;
444
444
  }
445
+ // @implements A-SPEC-662 — the `kills` whose `where` will never apply, reported when the T-SPEC
446
+ // seals (measured 2026-09-17: 22/22 entries were paths and prose; nothing said so). Advisory:
447
+ // the seal is done, the field appears only when something is wrong, a failed walk drops it.
448
+ // No ledger here — the reaction ledger for every advisory kind is REQ-663's.
449
+ let killsAdvisory;
450
+ try {
451
+ if (spec.type === 'T-SPEC' && a.root) {
452
+ const { parseKills } = require('../../spec/kills');
453
+ const check = require('../../review/kills-check');
454
+ const kills = parseKills(candidate.frontmatter);
455
+ const aspecId = candidate.dependsOn[0];
456
+ if (kills.length > 0 && aspecId) {
457
+ const found = check.killsAdvisory(a.id, aspecId, kills, check.anchoredSourceTexts(a.root, aspecId));
458
+ if (found)
459
+ killsAdvisory = found;
460
+ }
461
+ }
462
+ }
463
+ catch {
464
+ killsAdvisory = undefined;
465
+ }
466
+ // @implements A-SPEC-663 — the REACTION ledger. Every finding this seal just reported gets a
467
+ // deterministic id; the ledger learns `issued` now and, from the next approval_status on, whether
468
+ // it was resolved, persisted or dismissed. Measured 2026-09-18: 153 issue rows across two
469
+ // advisory ledgers and no row anywhere saying what happened next — the numerator every "promote
470
+ // once the false-positive rate is known" sentence lacked. Advisory: failures drop the fields.
471
+ let advisoryIds;
472
+ let advisoryHistory;
473
+ let dismissUnknown;
474
+ try {
475
+ if (a.root) {
476
+ const ao = require('../../rtm/advisory-outcomes');
477
+ let traceGap = null;
478
+ if (spec.type === 'A-SPEC') {
479
+ try {
480
+ traceGap = ao.traceGapFor(a.id, candidate, cachedScan(a.root, a.root), a.root);
481
+ }
482
+ catch {
483
+ traceGap = null;
484
+ }
485
+ }
486
+ const { keys, ids } = ao.currentAdvisoryKeys(a.id, { impactAdvisory, anchorDensity, fttFulfilment, killsAdvisory, traceGap });
487
+ const head = ao.gitHeadOf(a.root);
488
+ const r = ao.recordAdvisoryOutcomes(a.root, a.id, keys, (s) => s.aspec === a.id, head, a.dismiss ?? []);
489
+ if (keys.length > 0)
490
+ advisoryIds = ids;
491
+ if (r.history.length > 0)
492
+ advisoryHistory = r.history;
493
+ if (a.dismiss && r.dismissUnknown.length > 0)
494
+ dismissUnknown = r.dismissUnknown;
495
+ }
496
+ }
497
+ catch {
498
+ advisoryIds = undefined;
499
+ advisoryHistory = undefined;
500
+ dismissUnknown = a.dismiss;
501
+ }
445
502
  return { approved: a.id, digest, ...(impactAdvisory ? { impactAdvisory } : {}), ...(anchorDensity ? { anchorDensity } : {}),
446
503
  ...(impactGraph ? { impactGraph } : {}), ...(impactAdvisoryUnavailable ? { impactAdvisoryUnavailable } : {}),
447
- ...(fttFulfilment ? { fttFulfilment } : {}) };
504
+ ...(fttFulfilment ? { fttFulfilment } : {}), ...(killsAdvisory ? { killsAdvisory } : {}),
505
+ ...(advisoryIds ? { advisoryIds } : {}), ...(advisoryHistory ? { advisoryHistory } : {}), ...(dismissUnknown ? { dismissUnknown } : {}) };
448
506
  },
449
507
  /**
450
508
  * Pin a REQ's citations: compute the content digest of every cited source that resolves inside
@@ -44,6 +44,10 @@ export declare function createTestExecutionHandlers(context: TestExecutionContex
44
44
  reason?: undefined;
45
45
  })[];
46
46
  survivors: import("../../spec/kills").Mutation[];
47
+ unapplied: {
48
+ where: string;
49
+ reason: string;
50
+ }[];
47
51
  };
48
52
  } | {
49
53
  calibrationClosed?: string[] | undefined;
@@ -49,6 +49,7 @@ const scope_1 = require("../../review/scope");
49
49
  const test_runner_1 = require("../../review/test-runner");
50
50
  const kills_1 = require("../../spec/kills");
51
51
  const mutate_1 = require("../../review/mutate");
52
+ const kills_check_1 = require("../../review/kills-check");
52
53
  const test_evidence_1 = require("../../review/test-evidence");
53
54
  const test_outcomes_1 = require("../../review/test-outcomes");
54
55
  const maintenance_evidence_1 = require("../maintenance-evidence");
@@ -122,8 +123,11 @@ function createTestExecutionHandlers(context) {
122
123
  const r = (0, mutate_1.runKillsOnFile)(src, m, coveringFiles, (files) => (0, test_runner_1.runJestOutcomes)(files, root));
123
124
  return { mutation: m, ...r };
124
125
  });
125
- const survivors = results.filter((r) => r.verdict === 'survived').map((r) => r.mutation);
126
- return { mutate: { tspec: a.mutate, aspec: aspecId, coveringFiles, results, survivors } };
126
+ // @implements A-SPEC-662 a mutation that never applied is UNAPPLIED, not a survivor: measured
127
+ // 2026-09-17, 22/22 entries here failed to apply and the response read `survivors: []` — the
128
+ // shape of a clean run. The pair (unapplied, survivors) now says "unverified" when it is.
129
+ const { survivors, unapplied } = (0, kills_check_1.partitionMutateResults)(results);
130
+ return { mutate: { tspec: a.mutate, aspec: aspecId, coveringFiles, results, survivors, unapplied } };
127
131
  }
128
132
  const g = new rtm_graph_1.RtmGraph();
129
133
  let testScope;
@@ -771,6 +771,9 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
771
771
  ok: boolean;
772
772
  reason: string;
773
773
  } | {
774
+ advisoryHistory?: import("../rtm/advisory-outcomes").AdvisoryState[] | undefined;
775
+ advisoryIds?: Partial<Record<string, string>> | undefined;
776
+ killsAdvisory?: import("../review/kills-check").KillsAdvisory | undefined;
774
777
  fttFulfilment?: import("../rtm/ftt-fulfilment").FttFulfilment | undefined;
775
778
  graphPreview?: {
776
779
  impact?: import("../rtm/impact-advisory").ImpactAdvisory;
@@ -824,12 +827,31 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
824
827
  ok: boolean;
825
828
  url: string;
826
829
  running: boolean;
827
- census: import("../server/dashboard-launcher").DashboardCensusExtended;
830
+ census: {
831
+ advisoryOutcomes?: import("../rtm/advisory-outcomes").AdvisoryCensus | undefined;
832
+ codeLinkedPct: number;
833
+ codeLinkedCount: number;
834
+ unlinkedCount: number;
835
+ unlinkedByReason: Record<import("../rtm/link-census").UnlinkedReason, number>;
836
+ excluded: {
837
+ total: number;
838
+ retired: number;
839
+ unmapped: number;
840
+ nonSpec: number;
841
+ };
842
+ reqCount: number;
843
+ pipelineCount: number;
844
+ coveredCount: number;
845
+ coveragePct: number;
846
+ retiredCount: number;
847
+ findingsScanned: boolean;
848
+ };
828
849
  reason?: undefined;
829
850
  }>;
830
851
  spec_approve: (a: {
831
852
  root?: string;
832
853
  id: string;
854
+ dismiss?: string[];
833
855
  }) => Promise<{
834
856
  ok: false;
835
857
  reason: string;
@@ -846,6 +868,10 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
846
868
  reason: string;
847
869
  conflict: import("../spec/version-conflict").ConflictDetail;
848
870
  } | {
871
+ dismissUnknown?: string[] | undefined;
872
+ advisoryHistory?: import("../rtm/advisory-outcomes").AdvisoryState[] | undefined;
873
+ advisoryIds?: Partial<Record<string, string>> | undefined;
874
+ killsAdvisory?: import("../review/kills-check").KillsAdvisory | undefined;
849
875
  fttFulfilment?: import("../rtm/ftt-fulfilment").FttFulfilment | undefined;
850
876
  impactAdvisoryUnavailable?: "empty" | "unreadable" | undefined;
851
877
  impactGraph?: {
@@ -1028,6 +1054,10 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
1028
1054
  reason?: undefined;
1029
1055
  })[];
1030
1056
  survivors: import("../spec/kills").Mutation[];
1057
+ unapplied: {
1058
+ where: string;
1059
+ reason: string;
1060
+ }[];
1031
1061
  };
1032
1062
  } | {
1033
1063
  calibrationClosed?: string[] | undefined;
@@ -859,6 +859,7 @@ function makeRawHandlers(store, opts) {
859
859
  resolver,
860
860
  resolveLedgerRoot,
861
861
  fetchJson,
862
+ cachedScan, // @implements A-SPEC-663
862
863
  resolveHandlerApproval: (root, approval, action, now) => resolveHandlerApproval(root, store, approval, action, now),
863
864
  refusalQueueHint: (root, request) => refusalQueueHint(root, store, request),
864
865
  });
@@ -4,6 +4,7 @@ exports.ANALYZE_TEXT_CAP = exports.ANALYZE_LIST_CAP = exports.PRIMARY_TIER_WIDTH
4
4
  exports.unquoteGitPath = unquoteGitPath;
5
5
  exports.analyzeMaintenance = analyzeMaintenance;
6
6
  exports.boundAnalysis = boundAnalysis;
7
+ // @implements A-SPEC-310
7
8
  // @implements A-SPEC-295
8
9
  // @implements A-SPEC-294
9
10
  // @implements A-SPEC-293