@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.
- package/CHANGELOG.md +52 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/agents.js +1 -0
- package/dist/holmes/cli/doctor.d.ts +1 -0
- package/dist/holmes/cli/doctor.js +42 -0
- package/dist/holmes/cli/index.js +1 -0
- package/dist/holmes/cli/init.js +1 -0
- package/dist/holmes/cpg/language-parser-walk.js +1 -0
- package/dist/holmes/hooks/stop.d.ts +7 -0
- package/dist/holmes/hooks/stop.js +54 -1
- package/dist/holmes/mcp/handlers/operator-inspection.d.ts +27 -1
- package/dist/holmes/mcp/handlers/operator-inspection.js +68 -2
- package/dist/holmes/mcp/handlers/spec-approval.d.ts +5 -0
- package/dist/holmes/mcp/handlers/spec-approval.js +59 -1
- package/dist/holmes/mcp/handlers/test-execution.d.ts +4 -0
- package/dist/holmes/mcp/handlers/test-execution.js +6 -2
- package/dist/holmes/mcp/handlers.d.ts +31 -1
- package/dist/holmes/mcp/handlers.js +1 -0
- package/dist/holmes/mcp/maintenance-analyze.js +1 -0
- package/dist/holmes/mcp/tool-schemas.js +1 -0
- package/dist/holmes/project/ci-runs.d.ts +46 -0
- package/dist/holmes/project/ci-runs.js +137 -0
- package/dist/holmes/project/install-scripts-policy.js +1 -0
- package/dist/holmes/review/evaluation-metrics.js +1 -0
- package/dist/holmes/review/kills-check.d.ts +40 -0
- package/dist/holmes/review/kills-check.js +147 -0
- package/dist/holmes/review/manual-baseline.js +1 -0
- package/dist/holmes/rtm/advisory-outcomes.d.ts +137 -0
- package/dist/holmes/rtm/advisory-outcomes.js +314 -0
- package/dist/holmes/rtm/rtm-graph.js +1 -0
- package/dist/holmes/rtm/taint-benchmark.js +1 -0
- package/package.json +1 -1
- 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
|
-
|
|
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}`;
|
package/dist/holmes/cli/index.js
CHANGED
|
@@ -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
|
package/dist/holmes/cli/init.js
CHANGED
|
@@ -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"));
|
|
@@ -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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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:
|
|
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
|