@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.
- package/CHANGELOG.md +100 -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/governance/constitution.d.ts +11 -0
- package/dist/holmes/governance/constitution.js +15 -1
- package/dist/holmes/guardrail/impact-gate.d.ts +10 -1
- package/dist/holmes/guardrail/impact-gate.js +19 -0
- package/dist/holmes/guardrail/risk-classifier.d.ts +1 -0
- package/dist/holmes/guardrail/risk-classifier.js +26 -3
- package/dist/holmes/hooks/stop.d.ts +16 -0
- package/dist/holmes/hooks/stop.js +116 -1
- package/dist/holmes/mcp/handlers/graph-operations.d.ts +1 -0
- package/dist/holmes/mcp/handlers/graph-operations.js +18 -1
- package/dist/holmes/mcp/handlers/maintenance-evidence.d.ts +4 -0
- package/dist/holmes/mcp/handlers/maintenance-evidence.js +16 -1
- package/dist/holmes/mcp/handlers/operator-inspection.d.ts +28 -1
- package/dist/holmes/mcp/handlers/operator-inspection.js +91 -3
- package/dist/holmes/mcp/handlers/spec-approval.d.ts +6 -0
- package/dist/holmes/mcp/handlers/spec-approval.js +87 -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 +34 -1
- package/dist/holmes/mcp/handlers.js +5 -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/anchor-comment.d.ts +2 -0
- package/dist/holmes/rtm/anchor-comment.js +8 -0
- package/dist/holmes/rtm/file-anchors.d.ts +9 -0
- package/dist/holmes/rtm/file-anchors.js +128 -0
- package/dist/holmes/rtm/ftt-fulfilment.d.ts +42 -0
- package/dist/holmes/rtm/ftt-fulfilment.js +195 -0
- package/dist/holmes/rtm/known-defects.d.ts +26 -0
- package/dist/holmes/rtm/known-defects.js +77 -0
- package/dist/holmes/rtm/link-census.d.ts +61 -0
- package/dist/holmes/rtm/link-census.js +90 -0
- package/dist/holmes/rtm/rtm-graph.js +1 -0
- package/dist/holmes/rtm/taint-benchmark.js +1 -0
- package/dist/holmes/rtm/trace-gaps.d.ts +20 -0
- package/dist/holmes/rtm/trace-gaps.js +64 -0
- package/dist/holmes/server/dashboard-launcher.d.ts +20 -0
- package/dist/holmes/server/dashboard-launcher.js +24 -1
- package/dist/holmes/server/dashboard.js +40 -2
- package/package.json +1 -1
- package/playbooks/author-slice/PLAYBOOK.md +30 -0
- 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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -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:
|
|
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;
|