@holmes-lab/holmes-kit 0.20.2 → 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 (59) hide show
  1. package/CHANGELOG.md +100 -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/governance/constitution.d.ts +11 -0
  10. package/dist/holmes/governance/constitution.js +15 -1
  11. package/dist/holmes/guardrail/impact-gate.d.ts +10 -1
  12. package/dist/holmes/guardrail/impact-gate.js +19 -0
  13. package/dist/holmes/guardrail/risk-classifier.d.ts +1 -0
  14. package/dist/holmes/guardrail/risk-classifier.js +26 -3
  15. package/dist/holmes/hooks/stop.d.ts +16 -0
  16. package/dist/holmes/hooks/stop.js +116 -1
  17. package/dist/holmes/mcp/handlers/graph-operations.d.ts +1 -0
  18. package/dist/holmes/mcp/handlers/graph-operations.js +18 -1
  19. package/dist/holmes/mcp/handlers/maintenance-evidence.d.ts +4 -0
  20. package/dist/holmes/mcp/handlers/maintenance-evidence.js +16 -1
  21. package/dist/holmes/mcp/handlers/operator-inspection.d.ts +28 -1
  22. package/dist/holmes/mcp/handlers/operator-inspection.js +91 -3
  23. package/dist/holmes/mcp/handlers/spec-approval.d.ts +6 -0
  24. package/dist/holmes/mcp/handlers/spec-approval.js +87 -1
  25. package/dist/holmes/mcp/handlers/test-execution.d.ts +4 -0
  26. package/dist/holmes/mcp/handlers/test-execution.js +6 -2
  27. package/dist/holmes/mcp/handlers.d.ts +34 -1
  28. package/dist/holmes/mcp/handlers.js +5 -0
  29. package/dist/holmes/mcp/maintenance-analyze.js +1 -0
  30. package/dist/holmes/mcp/tool-schemas.js +1 -0
  31. package/dist/holmes/project/ci-runs.d.ts +46 -0
  32. package/dist/holmes/project/ci-runs.js +137 -0
  33. package/dist/holmes/project/install-scripts-policy.js +1 -0
  34. package/dist/holmes/review/evaluation-metrics.js +1 -0
  35. package/dist/holmes/review/kills-check.d.ts +40 -0
  36. package/dist/holmes/review/kills-check.js +147 -0
  37. package/dist/holmes/review/manual-baseline.js +1 -0
  38. package/dist/holmes/rtm/advisory-outcomes.d.ts +137 -0
  39. package/dist/holmes/rtm/advisory-outcomes.js +314 -0
  40. package/dist/holmes/rtm/anchor-comment.d.ts +2 -0
  41. package/dist/holmes/rtm/anchor-comment.js +8 -0
  42. package/dist/holmes/rtm/file-anchors.d.ts +9 -0
  43. package/dist/holmes/rtm/file-anchors.js +128 -0
  44. package/dist/holmes/rtm/ftt-fulfilment.d.ts +42 -0
  45. package/dist/holmes/rtm/ftt-fulfilment.js +195 -0
  46. package/dist/holmes/rtm/known-defects.d.ts +26 -0
  47. package/dist/holmes/rtm/known-defects.js +77 -0
  48. package/dist/holmes/rtm/link-census.d.ts +61 -0
  49. package/dist/holmes/rtm/link-census.js +90 -0
  50. package/dist/holmes/rtm/rtm-graph.js +1 -0
  51. package/dist/holmes/rtm/taint-benchmark.js +1 -0
  52. package/dist/holmes/rtm/trace-gaps.d.ts +20 -0
  53. package/dist/holmes/rtm/trace-gaps.js +64 -0
  54. package/dist/holmes/server/dashboard-launcher.d.ts +20 -0
  55. package/dist/holmes/server/dashboard-launcher.js +24 -1
  56. package/dist/holmes/server/dashboard.js +40 -2
  57. package/package.json +1 -1
  58. package/playbooks/author-slice/PLAYBOOK.md +30 -0
  59. package/playbooks/tdd-slice/PLAYBOOK.md +4 -0
@@ -45,6 +45,7 @@ const root_1 = require("../../project/root");
45
45
  const rtm_builder_1 = require("../../rtm/rtm-builder");
46
46
  const graph_store_1 = require("../../rtm/graph-store");
47
47
  const assoc_arm_1 = require("../../assoc/assoc-arm");
48
+ const trace_gaps_1 = require("../../rtm/trace-gaps");
48
49
  // @implements A-SPEC-478 — the semantic tier runtime stays at the async reindex edge.
49
50
  const tier_1 = require("../../semantic/tier");
50
51
  const vector_cache_1 = require("../../semantic/vector-cache");
@@ -119,7 +120,8 @@ function createGraphOperationsHandlers(context) {
119
120
  // drops outdated/legacy), which is exactly what let this in. Diagnosis (rtm_check) and
120
121
  // matching (issue_localize / maintenance_analyze) keep their own populations — the replay
121
122
  // pins were measured on them.
122
- const specs = (await context.listSpecs()).filter((s) => s.status === 'approved');
123
+ const specsAll = await context.listSpecs();
124
+ const specs = specsAll.filter((s) => s.status === 'approved');
123
125
  // @implements A-SPEC-283
124
126
  // Reuse the persisted graph when its basis still holds. Measured: on the warm path the graph
125
127
  // build is ~81% of the cost and reopening is ~0ms. `scanDigest` is the field that makes this
@@ -181,11 +183,26 @@ function createGraphOperationsHandlers(context) {
181
183
  return { id, summary };
182
184
  });
183
185
  const summariesOmitted = impacted.length - shownSummaries.length;
186
+ // @implements A-SPEC-658 — the specs the graph CANNOT see for these changes: approved,
187
+ // anchored by no production source, and declaring a changed symbol's file. Information
188
+ // beside the closure, never inside it; unresolved symbols yield no files and therefore no
189
+ // guess.
190
+ const changedFiles = a.changed.flatMap((qn) => g.codeNodeIds(qn))
191
+ .map((id) => id.slice(id.lastIndexOf('@') + 1)).filter((f) => f !== '');
192
+ const gaps = changedFiles.length > 0
193
+ ? (0, trace_gaps_1.traceGaps)(changedFiles, (0, trace_gaps_1.unlinkedApproved)(specsAll, scanned), (id) => { try {
194
+ return g.summaryOf(id);
195
+ }
196
+ catch {
197
+ return null;
198
+ } })
199
+ : [];
184
200
  answered = true;
185
201
  return {
186
202
  impacted,
187
203
  impactedSummaries,
188
204
  ...(summariesOmitted > 0 ? { summariesOmitted } : {}),
205
+ ...(gaps.length > 0 ? { traceGaps: gaps } : {}),
189
206
  rankedImpact,
190
207
  reachedByDepth,
191
208
  bounded: stoppedAt.length > 0 ? stoppedAt.slice(0, 20) : undefined,
@@ -1,8 +1,12 @@
1
+ import type { Spec } from '../../spec/spec-parser';
2
+ import type { ScannedFile } from '../../cpg/cpg-scanner';
1
3
  interface MaintenanceEvidenceContext {
2
4
  foreignRootReason(root: string): string | null;
3
5
  projectRootOf(root: string): string;
4
6
  refusal(reason: string): Error;
5
7
  fileDigestOf(root: string, relativePath: string): string | null;
8
+ listSpecs(): Promise<Spec[]>;
9
+ cachedScan(root: string): ScannedFile[];
6
10
  }
7
11
  /** Raw handlers: the public facade attaches basis exactly once. */
8
12
  export declare function createMaintenanceEvidenceHandlers(context: MaintenanceEvidenceContext): {
@@ -41,6 +41,7 @@ const node_child_process_1 = require("node:child_process");
41
41
  const maintenance_evidence_1 = require("../maintenance-evidence");
42
42
  const impact_gate_1 = require("../../guardrail/impact-gate");
43
43
  const root_1 = require("../../project/root");
44
+ const trace_gaps_1 = require("../../rtm/trace-gaps");
44
45
  /** Raw handlers: the public facade attaches basis exactly once. */
45
46
  function createMaintenanceEvidenceHandlers(context) {
46
47
  return {
@@ -77,11 +78,25 @@ function createMaintenanceEvidenceHandlers(context) {
77
78
  throw context.refusal(`이 프로젝트에 저장된 분석이 아닙니다: ${a.digest}`);
78
79
  artifact = JSON.parse(fs.readFileSync(file, 'utf8'));
79
80
  }
81
+ const target = a.target.replace(/\\/g, '/');
82
+ // @implements A-SPEC-658 — approved specs that declare this file but anchor nothing: the graph
83
+ // cannot see them, so the gate must not read their silence as "no impact". A failed lookup
84
+ // drops the input (information lost, verdict unchanged) rather than inventing a gap.
85
+ let gaps;
86
+ try {
87
+ const found = (0, trace_gaps_1.traceGaps)([target], (0, trace_gaps_1.unlinkedApproved)(await context.listSpecs(), context.cachedScan(root)));
88
+ if (found.length > 0)
89
+ gaps = found.map(({ id, file }) => ({ id, file }));
90
+ }
91
+ catch {
92
+ gaps = undefined;
93
+ }
80
94
  return (0, impact_gate_1.evaluateEditEvidence)({
81
- target: a.target.replace(/\\/g, '/'),
95
+ target,
82
96
  artifact,
83
97
  currentHead: head,
84
98
  currentTargetDigest: context.fileDigestOf(root, a.target),
99
+ ...(gaps ? { traceGaps: gaps } : {}),
85
100
  });
86
101
  },
87
102
  // @implements A-SPEC-268
@@ -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,10 @@ 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;
57
+ fttFulfilment?: import("../../rtm/ftt-fulfilment").FttFulfilment | undefined;
49
58
  graphPreview?: {
50
59
  impact?: import("../../rtm/impact-advisory").ImpactAdvisory;
51
60
  density?: Array<import("../../rtm/anchor-density").AnchorDensityFinding>;
@@ -161,7 +170,25 @@ export declare function createOperatorInspectionHandlers(context: OperatorInspec
161
170
  ok: boolean;
162
171
  url: string;
163
172
  running: boolean;
164
- census: import("../../server/dashboard-launcher").DashboardCensus;
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
+ };
165
192
  reason?: undefined;
166
193
  }>;
167
194
  };
@@ -190,7 +190,80 @@ function createOperatorInspectionHandlers(context) {
190
190
  catch {
191
191
  graphPreview = undefined;
192
192
  }
193
- return { ok: true, ...(0, approval_status_1.describeApproval)(cur.spec, context.resolver(all)), ...(graphPreview ? { graphPreview } : {}) };
193
+ // @implements A-SPEC-656 the same promised-but-absent finding sealing will report, delivered
194
+ // BEFORE sealing (REQ-572: delivery time was the gap, not the calculation) and without a ledger
195
+ // write (a query must not pollute the observation denominator). Same pure function as the seal.
196
+ let fttFulfilment;
197
+ try {
198
+ if (cur.spec.type === 'A-SPEC' && a.root) {
199
+ const root = a.root;
200
+ const { fttFulfilment: compute, locateByBasename } = require('../../rtm/ftt-fulfilment');
201
+ const exists = (rel) => {
202
+ const abs = path.resolve(root, rel);
203
+ return abs.startsWith(root + path.sep) && fs.existsSync(abs);
204
+ };
205
+ const found = compute(String(cur.spec.sections?.['Files to Touch'] ?? ''), exists, locateByBasename(root));
206
+ if (found)
207
+ fttFulfilment = found;
208
+ }
209
+ }
210
+ catch {
211
+ fttFulfilment = undefined;
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
+ }
264
+ return { ok: true, ...(0, approval_status_1.describeApproval)(cur.spec, context.resolver(all)), ...(graphPreview ? { graphPreview } : {}),
265
+ ...(fttFulfilment ? { fttFulfilment } : {}), ...(killsAdvisory ? { killsAdvisory } : {}),
266
+ ...(advisoryIds ? { advisoryIds } : {}), ...(advisoryHistory ? { advisoryHistory } : {}) };
194
267
  },
195
268
  /**
196
269
  * @implements A-SPEC-538.3
@@ -270,11 +343,26 @@ function createOperatorInspectionHandlers(context) {
270
343
  return { ok: false, reason: dest.reason };
271
344
  try {
272
345
  const { startDashboardServer } = await Promise.resolve().then(() => __importStar(require('../../server/dashboard')));
273
- const { ensureDashboard, dashboardCensus } = await Promise.resolve().then(() => __importStar(require('../../server/dashboard-launcher')));
346
+ // @implements A-SPEC-655 the census carries the code-link axis beside pipeline coverage.
347
+ const { ensureDashboard, dashboardCensusExtended } = await Promise.resolve().then(() => __importStar(require('../../server/dashboard-launcher')));
274
348
  const launch = await ensureDashboard(dest.root, a.port, (opts) => startDashboardServer(opts));
275
349
  const rtm = await context.fetchJson(`${launch.url}/api/rtm`);
276
350
  const heatmap = await context.fetchJson(`${launch.url}/api/rtm/heatmap`);
277
- return { ok: true, url: launch.url, running: launch.running, census: dashboardCensus(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 } : {}) } };
278
366
  }
279
367
  catch (err) {
280
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,11 @@ 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;
65
+ fttFulfilment?: import("../../rtm/ftt-fulfilment").FttFulfilment | undefined;
60
66
  impactAdvisoryUnavailable?: "empty" | "unreadable" | undefined;
61
67
  impactGraph?: {
62
68
  status: "current" | "stale" | "unverified";
@@ -415,8 +415,94 @@ function createSpecApprovalHandlers(context) {
415
415
  impactGraph = undefined;
416
416
  impactAdvisoryUnavailable = undefined;
417
417
  }
418
+ // @implements A-SPEC-656 — the promised-but-absent side of the declaration, filesystem only,
419
+ // independent of the graph block above (no rtm.sqlite needed). Advisory: the seal is already
420
+ // done. The finding rides the response and lands in its own observation ledger so its
421
+ // false-positive rate can be measured before anyone proposes a gate; a failed read drops the
422
+ // field rather than inventing a finding. Parsing, buckets and the walk live in the pure module.
423
+ let fttFulfilment;
424
+ try {
425
+ if (spec.type === 'A-SPEC' && a.root) {
426
+ const root = a.root;
427
+ const { fttFulfilment: compute, locateByBasename, appendFttFulfilment } = require('../../rtm/ftt-fulfilment');
428
+ const exists = (rel) => {
429
+ const abs = path.resolve(root, rel);
430
+ return abs.startsWith(root + path.sep) && fs.existsSync(abs);
431
+ };
432
+ const found = compute(String(candidate.sections?.['Files to Touch'] ?? ''), exists, locateByBasename(root));
433
+ if (found) {
434
+ fttFulfilment = found;
435
+ appendFttFulfilment(root, {
436
+ aspec: a.id, declared: found.declared, missing: found.missing.map((m) => m.path),
437
+ moved: found.moved.map((m) => m.path), ts: new Date().toISOString(),
438
+ });
439
+ }
440
+ }
441
+ }
442
+ catch {
443
+ fttFulfilment = undefined;
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
+ }
418
502
  return { approved: a.id, digest, ...(impactAdvisory ? { impactAdvisory } : {}), ...(anchorDensity ? { anchorDensity } : {}),
419
- ...(impactGraph ? { impactGraph } : {}), ...(impactAdvisoryUnavailable ? { impactAdvisoryUnavailable } : {}) };
503
+ ...(impactGraph ? { impactGraph } : {}), ...(impactAdvisoryUnavailable ? { impactAdvisoryUnavailable } : {}),
504
+ ...(fttFulfilment ? { fttFulfilment } : {}), ...(killsAdvisory ? { killsAdvisory } : {}),
505
+ ...(advisoryIds ? { advisoryIds } : {}), ...(advisoryHistory ? { advisoryHistory } : {}), ...(dismissUnknown ? { dismissUnknown } : {}) };
420
506
  },
421
507
  /**
422
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;
@@ -410,6 +410,7 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
410
410
  reason: "hub" | "depth";
411
411
  inDegree?: number;
412
412
  }[] | undefined;
413
+ traceGaps?: import("../rtm/trace-gaps").TraceGap[] | undefined;
413
414
  summariesOmitted?: number | undefined;
414
415
  impacted: string[];
415
416
  impactedSummaries: {
@@ -770,6 +771,10 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
770
771
  ok: boolean;
771
772
  reason: string;
772
773
  } | {
774
+ advisoryHistory?: import("../rtm/advisory-outcomes").AdvisoryState[] | undefined;
775
+ advisoryIds?: Partial<Record<string, string>> | undefined;
776
+ killsAdvisory?: import("../review/kills-check").KillsAdvisory | undefined;
777
+ fttFulfilment?: import("../rtm/ftt-fulfilment").FttFulfilment | undefined;
773
778
  graphPreview?: {
774
779
  impact?: import("../rtm/impact-advisory").ImpactAdvisory;
775
780
  density?: Array<import("../rtm/anchor-density").AnchorDensityFinding>;
@@ -822,12 +827,31 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
822
827
  ok: boolean;
823
828
  url: string;
824
829
  running: boolean;
825
- census: import("../server/dashboard-launcher").DashboardCensus;
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
+ };
826
849
  reason?: undefined;
827
850
  }>;
828
851
  spec_approve: (a: {
829
852
  root?: string;
830
853
  id: string;
854
+ dismiss?: string[];
831
855
  }) => Promise<{
832
856
  ok: false;
833
857
  reason: string;
@@ -844,6 +868,11 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
844
868
  reason: string;
845
869
  conflict: import("../spec/version-conflict").ConflictDetail;
846
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;
875
+ fttFulfilment?: import("../rtm/ftt-fulfilment").FttFulfilment | undefined;
847
876
  impactAdvisoryUnavailable?: "empty" | "unreadable" | undefined;
848
877
  impactGraph?: {
849
878
  status: "current" | "stale" | "unverified";
@@ -1025,6 +1054,10 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
1025
1054
  reason?: undefined;
1026
1055
  })[];
1027
1056
  survivors: import("../spec/kills").Mutation[];
1057
+ unapplied: {
1058
+ where: string;
1059
+ reason: string;
1060
+ }[];
1028
1061
  };
1029
1062
  } | {
1030
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
  });
@@ -919,6 +920,10 @@ function makeRawHandlers(store, opts) {
919
920
  projectRootOf,
920
921
  refusal: reason => new HandlerRefusal(reason),
921
922
  fileDigestOf,
923
+ // @implements A-SPEC-658 — the impact gate reads trace gaps from the same store and scan the
924
+ // graph handlers use.
925
+ listSpecs: () => store.list(),
926
+ cachedScan,
922
927
  }),
923
928
  // @implements A-SPEC-642 — placed in the one slot no public-key-order pin covers (the handler suites pin
924
929
  // spec_create..spec_upgrade, spec_retire..spec_unseal, spec_unseal+5, spec_approve+4, spec_next+3,
@@ -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
@@ -204,6 +204,7 @@ exports.TOOL_SCHEMAS = {
204
204
  properties: {
205
205
  root: str('Optional when the server is bound to a file store — the ledger location is derived from the store itself; if supplied it must resolve to the SAME project, otherwise the approval is refused before anything is written.'),
206
206
  id: str('Id of the spec to approve and seal.'),
207
+ dismiss: { type: 'array', items: { type: 'string' }, description: 'A-SPEC-663 — advisory ids (from approval_status.advisoryIds) the author judged not useful; recorded as `dismissed` in the reaction ledger, never blocking. Unknown ids are echoed back in `dismissUnknown`.' },
207
208
  },
208
209
  required: ['id'],
209
210
  },
@@ -0,0 +1,46 @@
1
+ export type CiStatus = 'green' | 'red' | 'clone-failed' | 'checkout-failed' | 'install-failed' | 'build-failed' | 'jest-crashed' | 'vm-unreachable';
2
+ export interface CiRun {
3
+ rev: string;
4
+ os: string;
5
+ arch?: string;
6
+ machine?: string;
7
+ node?: string;
8
+ status: CiStatus | string;
9
+ suites: number;
10
+ tests: number;
11
+ failed: string[];
12
+ durationMs: number;
13
+ at: string;
14
+ note?: string;
15
+ }
16
+ export interface CiVerdict {
17
+ os: string;
18
+ state: 'green' | 'red' | 'failed' | 'not-run';
19
+ rev?: string;
20
+ at?: string;
21
+ behind?: number;
22
+ failed: string[];
23
+ status?: string;
24
+ }
25
+ /**
26
+ * @implements A-SPEC-664 — the ADOPTION predicate: does this workspace use the CI matrix at all?
27
+ *
28
+ * Existence, not contents. A `ci-runs.<host>.jsonl` that exists but is empty means a run was
29
+ * attempted here, so silence about a commit is a signal worth saying out loud. No file at all means
30
+ * nobody ever ran the matrix in this workspace — and the runner script is not in the shipped package
31
+ * (`files` carries bin/, dist/, grammars/, playbooks/, scripts/install.ps1, docs/, CHANGELOG), so
32
+ * advising every consumer to run it would be advising a path they do not have. Measured 2026-09-18
33
+ * on a consumer-shaped tree: the Stop hook printed that advice every turn.
34
+ */
35
+ export declare function hasCiLedger(root: string): boolean;
36
+ export declare function readCiRuns(root: string): CiRun[];
37
+ /** The most recent run for an OS, by `at`. */
38
+ export declare function latestCiRun(runs: CiRun[], os?: string): CiRun | undefined;
39
+ /**
40
+ * What the matrix says about `head`. `behind(rev)` is the injected git distance from that rev to
41
+ * head (commits), or undefined when git cannot answer. No run → `not-run`; an infrastructure
42
+ * status (clone/install/build failed, VM unreachable) → `failed`, which is not red and not green.
43
+ */
44
+ export declare function ciVerdict(runs: CiRun[], os: string, head: string | undefined, behind: (rev: string) => number | undefined): CiVerdict;
45
+ /** One line for the Stop hook's tracked channel or doctor. Never says green without a green row. */
46
+ export declare function ciStatusLine(v: CiVerdict): string;