@holmes-lab/holmes-kit 0.19.0 → 0.19.3
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 +120 -0
- package/dist/.build-id +1 -1
- package/dist/holmes/cli/agents.d.ts +22 -0
- package/dist/holmes/cli/agents.js +76 -1
- package/dist/holmes/cli/approve.js +6 -1
- package/dist/holmes/cli/doctor.d.ts +51 -1
- package/dist/holmes/cli/doctor.js +211 -36
- package/dist/holmes/cli/index.js +7 -1
- package/dist/holmes/cli/init.js +12 -0
- package/dist/holmes/cli/native-deps.d.ts +65 -0
- package/dist/holmes/cli/native-deps.js +131 -0
- package/dist/holmes/cpg/cycle-observation.d.ts +65 -0
- package/dist/holmes/cpg/cycle-observation.js +146 -0
- package/dist/holmes/governance/approval-queue.d.ts +23 -4
- package/dist/holmes/governance/approval-queue.js +44 -6
- package/dist/holmes/hooks/stop.d.ts +15 -0
- package/dist/holmes/hooks/stop.js +46 -3
- package/dist/holmes/mcp/handlers.d.ts +2 -0
- package/dist/holmes/mcp/handlers.js +29 -2
- package/dist/holmes/mcp/maintenance-analyze.d.ts +37 -0
- package/dist/holmes/mcp/maintenance-analyze.js +73 -1
- package/dist/holmes/mcp/maintenance-evidence.d.ts +41 -0
- package/dist/holmes/mcp/maintenance-evidence.js +71 -4
- package/dist/holmes/project/install-scripts-policy.d.ts +76 -0
- package/dist/holmes/project/install-scripts-policy.js +131 -0
- package/dist/holmes/project/npx-bin.d.ts +6 -0
- package/dist/holmes/project/npx-bin.js +10 -0
- package/dist/holmes/review/failed-test-names.d.ts +19 -0
- package/dist/holmes/review/failed-test-names.js +43 -0
- package/dist/holmes/review/run-replay.d.ts +23 -0
- package/dist/holmes/review/run-replay.js +30 -0
- package/dist/holmes/review/test-runner.d.ts +27 -0
- package/dist/holmes/review/test-runner.js +59 -3
- package/docs/install-guide.md +54 -5
- package/package.json +4 -1
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CYCLE_LIST_CAP = void 0;
|
|
37
|
+
exports.buildCycleObservation = buildCycleObservation;
|
|
38
|
+
exports.appendCycleObservation = appendCycleObservation;
|
|
39
|
+
exports.readCycleObservations = readCycleObservations;
|
|
40
|
+
// @implements A-SPEC-578.1
|
|
41
|
+
// The observation ledger the cycle ratchet's promotion criterion was already waiting on.
|
|
42
|
+
//
|
|
43
|
+
// `stop.ts` says promotion to `strict` "waits on the observation ledger answering the false-positive
|
|
44
|
+
// rate, which is the same path impactAdvisory and anchorDensity took". Measured 2026-09-09: those two
|
|
45
|
+
// siblings had been appending to `<name>.<replica>.jsonl` all along, and this one wrote a single line
|
|
46
|
+
// to stderr and nothing to disk — 0 records against their 12 and 7. A criterion waiting on data
|
|
47
|
+
// nobody collects never fires. This is the missing half, and it is deliberately the SIBLINGS' shape
|
|
48
|
+
// rather than a better one: a second convention for the same job is the next drift point.
|
|
49
|
+
//
|
|
50
|
+
// `cycle-detect.ts` stays pure (zero imports) — the same split `rtm/anchor-density.ts` uses against
|
|
51
|
+
// the rule it observes.
|
|
52
|
+
const fs = __importStar(require("node:fs"));
|
|
53
|
+
const path = __importStar(require("node:path"));
|
|
54
|
+
const replica_id_1 = require("../governance/replica-id");
|
|
55
|
+
const cycle_detect_1 = require("./cycle-detect");
|
|
56
|
+
/**
|
|
57
|
+
* How many cycles one record lists before it starts counting instead.
|
|
58
|
+
*
|
|
59
|
+
* A record must not grow with the tree: a repository with a thousand cycles would otherwise write a
|
|
60
|
+
* thousand-entry line every turn, and the ledger this exists to make readable would be the thing
|
|
61
|
+
* that makes it unreadable. What is dropped is COUNTED, never silently cut.
|
|
62
|
+
*/
|
|
63
|
+
exports.CYCLE_LIST_CAP = 50;
|
|
64
|
+
/**
|
|
65
|
+
* Build the record. PURE — the clock is an argument, so a test can pin it and two callers cannot
|
|
66
|
+
* disagree about what "now" was.
|
|
67
|
+
*
|
|
68
|
+
* A CLEAN run produces a record too. That is the whole design: a false-positive RATE is violations
|
|
69
|
+
* over chances, and a ledger that only speaks when something is wrong records the numerator and
|
|
70
|
+
* throws the denominator away.
|
|
71
|
+
*/
|
|
72
|
+
function buildCycleObservation(ev, ts) {
|
|
73
|
+
const listed = ev.current.slice(0, exports.CYCLE_LIST_CAP);
|
|
74
|
+
// The ratchet's own predicate decides what counts as a violation — reimplementing the filter here
|
|
75
|
+
// would be a second rule for one question, which is how the two quietly disagree.
|
|
76
|
+
const violations = (0, cycle_detect_1.cycleRatchetViolations)(listed, ev.allowed).map((v) => v.key);
|
|
77
|
+
return {
|
|
78
|
+
ts,
|
|
79
|
+
mode: ev.mode,
|
|
80
|
+
cycles: listed.map((c) => ({ key: (0, cycle_detect_1.cycleKey)(c.files), files: [...c.files].sort(), runtime: c.runtime, edges: c.edges.length })),
|
|
81
|
+
cyclesOmitted: Math.max(0, ev.current.length - listed.length),
|
|
82
|
+
violations,
|
|
83
|
+
allowed: ev.allowed.length,
|
|
84
|
+
scope: { judged: [...ev.scope.judged], unavailable: [...ev.scope.unavailable] },
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const OBSERVATION_FILE_RE = /^cycle-observations\.([^.]+)\.jsonl$/;
|
|
88
|
+
/**
|
|
89
|
+
* Append one record. Never throws, and never creates `.ax` where governance was not opted into
|
|
90
|
+
* (A-SPEC-191 §25 — the same refusal the approval queue makes).
|
|
91
|
+
*
|
|
92
|
+
* A failure returns `false` and changes nothing else: this is an OBSERVATION, and an observation
|
|
93
|
+
* that could alter a verdict would be a gate wearing a different name.
|
|
94
|
+
*/
|
|
95
|
+
function appendCycleObservation(root, rec) {
|
|
96
|
+
try {
|
|
97
|
+
if (!fs.existsSync(path.join(root, '.ax')))
|
|
98
|
+
return false;
|
|
99
|
+
let replica = 'local';
|
|
100
|
+
try {
|
|
101
|
+
replica = (0, replica_id_1.resolveReplicaId)(root) || 'local';
|
|
102
|
+
}
|
|
103
|
+
catch { /* keep the fallback */ }
|
|
104
|
+
const file = path.join(root, '.ax', 'ledger', `cycle-observations.${replica}.jsonl`);
|
|
105
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
106
|
+
fs.appendFileSync(file, `${JSON.stringify({ ...rec, replica })}\n`);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Every replica's records, merged. A corrupt line is skipped, never fatal. */
|
|
114
|
+
function readCycleObservations(root) {
|
|
115
|
+
const dir = path.join(root, '.ax', 'ledger');
|
|
116
|
+
let names;
|
|
117
|
+
try {
|
|
118
|
+
names = fs.readdirSync(dir).filter((n) => OBSERVATION_FILE_RE.test(n)).sort();
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
const out = [];
|
|
124
|
+
for (const name of names) {
|
|
125
|
+
let text;
|
|
126
|
+
try {
|
|
127
|
+
text = fs.readFileSync(path.join(dir, name), 'utf8');
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
for (const line of text.split('\n')) {
|
|
133
|
+
const s = line.trim();
|
|
134
|
+
if (!s)
|
|
135
|
+
continue;
|
|
136
|
+
try {
|
|
137
|
+
const r = JSON.parse(s);
|
|
138
|
+
if (r && typeof r === 'object' && typeof r.ts === 'string' && Array.isArray(r.cycles) && Array.isArray(r.violations)) {
|
|
139
|
+
out.push(r);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch { /* a corrupt line never breaks the read */ }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
@@ -109,8 +109,9 @@ export declare function approvalRequestId(kind: string, target: string): string;
|
|
|
109
109
|
* waiting.
|
|
110
110
|
*/
|
|
111
111
|
export declare function foldQueue(lines: string[], opts?: {
|
|
112
|
-
now
|
|
113
|
-
ttlMs
|
|
112
|
+
now?: number;
|
|
113
|
+
ttlMs?: number;
|
|
114
|
+
includeAllKinds?: boolean;
|
|
114
115
|
}): QueueState;
|
|
115
116
|
/**
|
|
116
117
|
* Append a request event. Fire-and-forget.
|
|
@@ -126,10 +127,28 @@ export declare function enqueueApprovalRequest(root: string, req: {
|
|
|
126
127
|
why: string;
|
|
127
128
|
reasonBytes?: number;
|
|
128
129
|
}): boolean;
|
|
130
|
+
/**
|
|
131
|
+
* The same act, told in full: what happened and why.
|
|
132
|
+
*
|
|
133
|
+
* @implements A-SPEC-576.1
|
|
134
|
+
* Nine modules read this one, and all of them want the boolean — so the boolean stays and this is
|
|
135
|
+
* the shape underneath it, rather than a breaking change rippling through nine call sites for the
|
|
136
|
+
* benefit of the one caller that wants to distinguish a duplicate from a refusal.
|
|
137
|
+
*/
|
|
138
|
+
export declare function enqueueApprovalRequestDetailed(root: string, req: {
|
|
139
|
+
kind: string;
|
|
140
|
+
target: string;
|
|
141
|
+
why: string;
|
|
142
|
+
reasonBytes?: number;
|
|
143
|
+
}): {
|
|
144
|
+
written: boolean;
|
|
145
|
+
reason: 'written' | 'duplicate' | 'no-project' | 'unwritable';
|
|
146
|
+
};
|
|
129
147
|
/** Read and fold the queue on disk. A missing file is an empty queue, not an error. */
|
|
130
148
|
export declare function readQueue(root: string, opts?: {
|
|
131
|
-
now
|
|
132
|
-
ttlMs
|
|
149
|
+
now?: number;
|
|
150
|
+
ttlMs?: number;
|
|
151
|
+
includeAllKinds?: boolean;
|
|
133
152
|
}): QueueState;
|
|
134
153
|
/**
|
|
135
154
|
* The refusal-message suffix pointing the operator at the review CLI.
|
|
@@ -40,6 +40,7 @@ exports.readRefusals = readRefusals;
|
|
|
40
40
|
exports.approvalRequestId = approvalRequestId;
|
|
41
41
|
exports.foldQueue = foldQueue;
|
|
42
42
|
exports.enqueueApprovalRequest = enqueueApprovalRequest;
|
|
43
|
+
exports.enqueueApprovalRequestDetailed = enqueueApprovalRequestDetailed;
|
|
43
44
|
exports.readQueue = readQueue;
|
|
44
45
|
exports.queueHint = queueHint;
|
|
45
46
|
// @implements A-SPEC-244
|
|
@@ -169,6 +170,16 @@ function foldQueue(lines, opts) {
|
|
|
169
170
|
malformedLines++;
|
|
170
171
|
break;
|
|
171
172
|
}
|
|
173
|
+
// @implements A-SPEC-576.1 — the READER applies the writer's predicate.
|
|
174
|
+
// REQ-563 routed gate refusals away from the inbox and deliberately did NOT rewrite the
|
|
175
|
+
// 2,192 lines already written; append-only is the rule this ledger is worth something for.
|
|
176
|
+
// The two decisions together left the inbox permanently 99.93% noise (measured here:
|
|
177
|
+
// 1,418 `shell` entries around a single real decision) because only the writer changed.
|
|
178
|
+
// The same constant does both jobs, so there is no second list to drift.
|
|
179
|
+
// `decisions` and `holds` are built from the OTHER events and stay complete — the hold
|
|
180
|
+
// question and the denial reason a refused `shell` request needs still reach `queueHint`.
|
|
181
|
+
if (!opts?.includeAllKinds && !exports.DECISION_KINDS.has(String(e.kind ?? '')))
|
|
182
|
+
break;
|
|
172
183
|
const prev = pending.get(id);
|
|
173
184
|
const ts = typeof e.ts === 'string' ? e.ts : '';
|
|
174
185
|
if (prev) {
|
|
@@ -223,7 +234,9 @@ function foldQueue(lines, opts) {
|
|
|
223
234
|
}
|
|
224
235
|
}
|
|
225
236
|
const all = [...pending.values()];
|
|
226
|
-
|
|
237
|
+
const ttl = typeof opts?.ttlMs === 'number' && typeof opts?.now === 'number'
|
|
238
|
+
? { ttlMs: opts.ttlMs, now: opts.now } : null;
|
|
239
|
+
if (ttl === null)
|
|
227
240
|
return { pending: all, expired: [], malformedLines, decisions, holds };
|
|
228
241
|
// @implements A-SPEC-507.1 — strict excess only, and an unparseable lastTs stays ACTIVE: a
|
|
229
242
|
// clockless entry must never be silently hidden by a clock it does not carry.
|
|
@@ -231,7 +244,7 @@ function foldQueue(lines, opts) {
|
|
|
231
244
|
const active = [];
|
|
232
245
|
for (const p of all) {
|
|
233
246
|
const last = Date.parse(p.lastTs);
|
|
234
|
-
(Number.isFinite(last) && last +
|
|
247
|
+
(Number.isFinite(last) && last + ttl.ttlMs < ttl.now ? expired : active).push(p);
|
|
235
248
|
}
|
|
236
249
|
return { pending: active, expired, malformedLines, decisions, holds };
|
|
237
250
|
}
|
|
@@ -244,6 +257,18 @@ function foldQueue(lines, opts) {
|
|
|
244
257
|
* caller uses the boolean only to decide whether to print the review hint.
|
|
245
258
|
*/
|
|
246
259
|
function enqueueApprovalRequest(root, req) {
|
|
260
|
+
return enqueueApprovalRequestDetailed(root, req).written;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* The same act, told in full: what happened and why.
|
|
264
|
+
*
|
|
265
|
+
* @implements A-SPEC-576.1
|
|
266
|
+
* Nine modules read this one, and all of them want the boolean — so the boolean stays and this is
|
|
267
|
+
* the shape underneath it, rather than a breaking change rippling through nine call sites for the
|
|
268
|
+
* benefit of the one caller that wants to distinguish a duplicate from a refusal.
|
|
269
|
+
*/
|
|
270
|
+
function enqueueApprovalRequestDetailed(root, req) {
|
|
271
|
+
let reason = 'unwritable';
|
|
247
272
|
try {
|
|
248
273
|
// @implements A-SPEC-244
|
|
249
274
|
// Only under an EXISTING .ax. The constitution suite (§25a) caught the first cut creating
|
|
@@ -251,7 +276,7 @@ function enqueueApprovalRequest(root, req) {
|
|
|
251
276
|
// marker where governance was never opted into, the exact defect A-SPEC-191 §25 exists to stop.
|
|
252
277
|
// An ungoverned directory gets no queue and no hint; it is not part of the system.
|
|
253
278
|
if (!fs.existsSync(path.join(root, '.ax')))
|
|
254
|
-
return false;
|
|
279
|
+
return { written: false, reason: 'no-project' };
|
|
255
280
|
// @implements A-SPEC-563.1 — kind routing: only decision-seeking kinds enter the tracked inbox;
|
|
256
281
|
// a gate refusal (shell, or any future kind — fail-safe toward a clean inbox) goes to this
|
|
257
282
|
// machine's LOCAL refusal log, raw target and all, where the approve fallback can still find it.
|
|
@@ -301,12 +326,25 @@ function enqueueApprovalRequest(root, req) {
|
|
|
301
326
|
// not a regular file, so a link — dangling or not — is refused. A path that truly does not exist
|
|
302
327
|
// throws ENOENT and is created, which is the ordinary first-write case.
|
|
303
328
|
if (!isPlainFile(file))
|
|
304
|
-
return false;
|
|
329
|
+
return { written: false, reason: 'unwritable' };
|
|
330
|
+
// @implements A-SPEC-576.1 — idempotency, on the INBOX only.
|
|
331
|
+
// Measured on this ledger: 2,203 `requested` lines carry 1,427 distinct ids, and 774 of the 776
|
|
332
|
+
// duplicates are one id — a `rm -rf /` test fixture re-filed 774 times across nine days. An
|
|
333
|
+
// outstanding question does not become more answerable by being asked again.
|
|
334
|
+
// The refusal log is deliberately left alone: it is a LOG, where each refusal is its own event
|
|
335
|
+
// and A-SPEC-564.2 counts them. Deduplicating a log erases the measurement it exists for.
|
|
336
|
+
// "Already asked" means still pending — a request re-filed AFTER a decision is a new question,
|
|
337
|
+
// and folding already drops decided ids from `pending`.
|
|
338
|
+
if (exports.DECISION_KINDS.has(req.kind)
|
|
339
|
+
&& readQueue(root, { includeAllKinds: true }).pending.some((p) => p.id === event.id)) {
|
|
340
|
+
return { written: false, reason: 'duplicate' };
|
|
341
|
+
}
|
|
305
342
|
fs.appendFileSync(file, JSON.stringify(event) + '\n');
|
|
306
|
-
|
|
343
|
+
reason = 'written';
|
|
344
|
+
return { written: true, reason };
|
|
307
345
|
}
|
|
308
346
|
catch {
|
|
309
|
-
return false;
|
|
347
|
+
return { written: false, reason };
|
|
310
348
|
}
|
|
311
349
|
}
|
|
312
350
|
/**
|
|
@@ -174,6 +174,21 @@ export declare function rolledBackLedgers(root: string): string[] | undefined;
|
|
|
174
174
|
* Returns the reason to block with, or null when there is nothing to report.
|
|
175
175
|
*/
|
|
176
176
|
export declare function governanceLostPreflight(specsDir: string, projectRoot?: string): string | null;
|
|
177
|
+
/**
|
|
178
|
+
* One line per ARTICLE, each under its own name.
|
|
179
|
+
*
|
|
180
|
+
* This used to be a single line reading `ART-8 RED-first (track)` for everything in `tracked` —
|
|
181
|
+
* and the cycle ratchet pushes ART-2 findings into that same array, so an import cycle was
|
|
182
|
+
* reported to the operator as a RED-first violation. Two observers sharing one sentence means the
|
|
183
|
+
* sentence is wrong for at least one of them.
|
|
184
|
+
*
|
|
185
|
+
* An article with no label still speaks, under its bare name: a new observer that says nothing is
|
|
186
|
+
* worse than one that says something plain.
|
|
187
|
+
*/
|
|
188
|
+
export declare function trackedLines(tracked?: {
|
|
189
|
+
article: string;
|
|
190
|
+
detail: string;
|
|
191
|
+
}[]): string[];
|
|
177
192
|
export declare function evaluateStop(specs: Spec[], evidence?: StopEvidence): {
|
|
178
193
|
block: boolean;
|
|
179
194
|
reason?: string;
|
|
@@ -42,6 +42,7 @@ exports.escalateReappraisals = escalateReappraisals;
|
|
|
42
42
|
exports.unrecordedApprovals = unrecordedApprovals;
|
|
43
43
|
exports.rolledBackLedgers = rolledBackLedgers;
|
|
44
44
|
exports.governanceLostPreflight = governanceLostPreflight;
|
|
45
|
+
exports.trackedLines = trackedLines;
|
|
45
46
|
exports.evaluateStop = evaluateStop;
|
|
46
47
|
exports.stopDebtAction = stopDebtAction;
|
|
47
48
|
exports.acknowledgeStop = acknowledgeStop;
|
|
@@ -432,6 +433,36 @@ function governanceLostPreflight(specsDir, projectRoot) {
|
|
|
432
433
|
const root = projectRoot ?? path.resolve(specsDir, '..', '..');
|
|
433
434
|
return (0, governance_history_1.hasGovernanceHistory)(root) ? governance_history_1.GOVERNANCE_LOST_HINT : null;
|
|
434
435
|
}
|
|
436
|
+
// @implements A-SPEC-578.1
|
|
437
|
+
/** What each article's track observations are called on the operator's screen. */
|
|
438
|
+
const TRACK_LABELS = {
|
|
439
|
+
'ART-8': 'RED-first',
|
|
440
|
+
'ART-2': 'code-graph cycles',
|
|
441
|
+
};
|
|
442
|
+
/**
|
|
443
|
+
* One line per ARTICLE, each under its own name.
|
|
444
|
+
*
|
|
445
|
+
* This used to be a single line reading `ART-8 RED-first (track)` for everything in `tracked` —
|
|
446
|
+
* and the cycle ratchet pushes ART-2 findings into that same array, so an import cycle was
|
|
447
|
+
* reported to the operator as a RED-first violation. Two observers sharing one sentence means the
|
|
448
|
+
* sentence is wrong for at least one of them.
|
|
449
|
+
*
|
|
450
|
+
* An article with no label still speaks, under its bare name: a new observer that says nothing is
|
|
451
|
+
* worse than one that says something plain.
|
|
452
|
+
*/
|
|
453
|
+
function trackedLines(tracked) {
|
|
454
|
+
if (!tracked || tracked.length === 0)
|
|
455
|
+
return [];
|
|
456
|
+
const byArticle = new Map();
|
|
457
|
+
for (const t of tracked) {
|
|
458
|
+
const key = String(t?.article ?? '');
|
|
459
|
+
byArticle.set(key, [...(byArticle.get(key) ?? []), String(t?.detail ?? '')]);
|
|
460
|
+
}
|
|
461
|
+
return [...byArticle.entries()].map(([article, details]) => {
|
|
462
|
+
const label = TRACK_LABELS[article];
|
|
463
|
+
return `[Holmes-Kit] ${article}${label ? ` ${label}` : ''} (track): ${details.join(' | ')}`;
|
|
464
|
+
});
|
|
465
|
+
}
|
|
435
466
|
function evaluateStop(specs, evidence) {
|
|
436
467
|
// L1: the Stop gate IS the constitution's re-verification point — every turn boundary re-runs the
|
|
437
468
|
// inviolable articles (ART-2 RTM, ART-3 validity incl. 4-quadrant GWT, ART-4 coverage evidence).
|
|
@@ -945,6 +976,18 @@ if (require.main === module) {
|
|
|
945
976
|
unavailable: [...sawImports].filter((e) => !judged.has(e)).sort(),
|
|
946
977
|
},
|
|
947
978
|
};
|
|
979
|
+
// @implements A-SPEC-578.1 — record the observation the promotion criterion waits on.
|
|
980
|
+
// EVERY run, including a clean one: a false-positive rate is violations over chances, and a
|
|
981
|
+
// ledger that only speaks when something is wrong keeps the numerator and drops the
|
|
982
|
+
// denominator. Append failure is swallowed by the outer catch below — an observation that
|
|
983
|
+
// could change a verdict would be a gate wearing another name.
|
|
984
|
+
// Its OWN try: sharing the outer one would let a fault in the observation discard the
|
|
985
|
+
// article's evidence, which is the coupling this comment exists to deny.
|
|
986
|
+
try {
|
|
987
|
+
const { appendCycleObservation, buildCycleObservation } = require('../cpg/cycle-observation');
|
|
988
|
+
appendCycleObservation(root, buildCycleObservation(cycles, new Date().toISOString()));
|
|
989
|
+
}
|
|
990
|
+
catch { /* an observation never touches the verdict */ }
|
|
948
991
|
}
|
|
949
992
|
catch {
|
|
950
993
|
cycles = undefined;
|
|
@@ -1046,9 +1089,9 @@ if (require.main === module) {
|
|
|
1046
1089
|
}
|
|
1047
1090
|
}
|
|
1048
1091
|
catch { /* the reappraisal signal is advisory; a failure never affects the stop verdict */ }
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1092
|
+
// @implements A-SPEC-578.1 — one line per article, each under its own name.
|
|
1093
|
+
for (const line of trackedLines(out.tracked))
|
|
1094
|
+
process.stderr.write(`${line}\n`);
|
|
1052
1095
|
// @implements A-SPEC-247 — before deciding to re-block, ask whether every unresolved debt is
|
|
1053
1096
|
// already queued for the owner. If so, tell the user ONCE and let the turn finish; a single
|
|
1054
1097
|
// non-waiting violation and we block exactly as before.
|
|
@@ -490,6 +490,7 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
|
|
|
490
490
|
survivors: import("../spec/kills").Mutation[];
|
|
491
491
|
};
|
|
492
492
|
} | {
|
|
493
|
+
calibrationClosed?: string[] | undefined;
|
|
493
494
|
baselineRecorded?: string | undefined;
|
|
494
495
|
scopeFallback?: "full" | undefined;
|
|
495
496
|
tier: import("../rtm/test-scope").RegressionTier;
|
|
@@ -518,6 +519,7 @@ declare function makeRawHandlers(store: SpecStore, opts?: ElicitOpts): {
|
|
|
518
519
|
changedFiles?: string[];
|
|
519
520
|
}): Promise<MaintenanceAnalysis & {
|
|
520
521
|
persistedTo?: string;
|
|
522
|
+
bounded?: boolean;
|
|
521
523
|
}>;
|
|
522
524
|
maintenance_outcome(a: {
|
|
523
525
|
root: string;
|
|
@@ -2367,6 +2367,9 @@ function makeRawHandlers(store, opts) {
|
|
|
2367
2367
|
// Record ONLY an actually-executed, GREEN run (review C4/C7): a red suite must not stand as
|
|
2368
2368
|
// coverage evidence at the moment the code is broken, and a no-op run must not overwrite a real
|
|
2369
2369
|
// record with a dishonest `passed: true` over an empty result.
|
|
2370
|
+
// @implements A-SPEC-578.4 — read BEFORE the write below replaces it: the calibration window
|
|
2371
|
+
// is "since the previous evidence run", and this line is the only moment that value exists.
|
|
2372
|
+
const previousEvidenceTs = (0, test_evidence_1.readTestEvidence)(root)?.ts;
|
|
2370
2373
|
const verified = (0, baseline_1.shouldRecordBaseline)(result);
|
|
2371
2374
|
if (verified) {
|
|
2372
2375
|
(0, test_evidence_1.writeTestEvidence)(root, { ts: new Date().toISOString(), head, tier: testScope.tier, passed: true, executedByAspec });
|
|
@@ -2382,6 +2385,19 @@ function makeRawHandlers(store, opts) {
|
|
|
2382
2385
|
// actually executed and passed. A red or skipped run must never become the reference point for
|
|
2383
2386
|
// "since the last verified state" — that would silently narrow every later scope against a
|
|
2384
2387
|
// state nobody verified.
|
|
2388
|
+
// @implements A-SPEC-578.4 — close the calibration loop on real work.
|
|
2389
|
+
// The window is the PREVIOUS evidence run's timestamp, read before this run overwrote it —
|
|
2390
|
+
// an analysis older than that belonged to a previous slice, and attributing today's changes
|
|
2391
|
+
// to it would be contamination rather than measurement. Its own try: an observation that
|
|
2392
|
+
// could change `passed` would be a gate wearing another name.
|
|
2393
|
+
let calibrationClosed;
|
|
2394
|
+
try {
|
|
2395
|
+
const closed = (0, maintenance_evidence_1.closeOpenArtifacts)(path.join(root, maintenance_evidence_1.EVIDENCE_DIR), { files: changedFiles }, new Date().toISOString(), previousEvidenceTs).closed;
|
|
2396
|
+
if (closed.length > 0)
|
|
2397
|
+
calibrationClosed = closed;
|
|
2398
|
+
}
|
|
2399
|
+
catch { /* the loop is observation; it never touches the verdict */ }
|
|
2400
|
+
// @implements A-SPEC-128
|
|
2385
2401
|
let baseline;
|
|
2386
2402
|
if (verified) {
|
|
2387
2403
|
baseline = a.mark ?? DEFAULT_BASELINE;
|
|
@@ -2393,7 +2409,8 @@ function makeRawHandlers(store, opts) {
|
|
|
2393
2409
|
ranFiles: result.ranFiles, executedByAspec, tail: result.tail,
|
|
2394
2410
|
// @implements A-SPEC-130 — the remediation rides in the answer: these are the files to anchor.
|
|
2395
2411
|
unresolvedFiles: testScope.unresolvedFiles,
|
|
2396
|
-
changeSource, ...(scopeFallback ? { scopeFallback } : {}), ...(baseline ? { baselineRecorded: baseline } : {})
|
|
2412
|
+
changeSource, ...(scopeFallback ? { scopeFallback } : {}), ...(baseline ? { baselineRecorded: baseline } : {}),
|
|
2413
|
+
...(calibrationClosed ? { calibrationClosed } : {}) };
|
|
2397
2414
|
},
|
|
2398
2415
|
async issue_localize(a) {
|
|
2399
2416
|
assertSpecStoreReachable('issue_localize', store, a.root); // @implements A-SPEC-419
|
|
@@ -2643,9 +2660,19 @@ function makeRawHandlers(store, opts) {
|
|
|
2643
2660
|
if (digest)
|
|
2644
2661
|
fileDigests[file] = digest;
|
|
2645
2662
|
}
|
|
2663
|
+
// The artifact takes the WHOLE analysis, never the bounded one below: a file has no
|
|
2664
|
+
// context window, and the calibration that scores this prediction must score what the
|
|
2665
|
+
// product actually predicted.
|
|
2646
2666
|
result.persistedTo = (0, maintenance_evidence_1.writeArtifact)(path.join(root, maintenance_evidence_1.EVIDENCE_DIR), (0, maintenance_evidence_1.artifactFrom)(analysis, new Date().toISOString(), fileDigests));
|
|
2647
2667
|
}
|
|
2648
|
-
|
|
2668
|
+
// @implements A-SPEC-578.5 — the response, and only the response, is bounded. Measured
|
|
2669
|
+
// 2026-09-09: the full shape is 187,174 characters (~47k tokens) and the harness refuses
|
|
2670
|
+
// it, which is why AGENTS.md step 3 had never once been obeyed with `persist: true`.
|
|
2671
|
+
// `persistedTo` rides along so the caller knows where the whole thing is.
|
|
2672
|
+
const bounded = (0, maintenance_analyze_1.boundAnalysis)(result);
|
|
2673
|
+
if (JSON.stringify(bounded).length !== JSON.stringify(result).length)
|
|
2674
|
+
bounded.bounded = true;
|
|
2675
|
+
return bounded;
|
|
2649
2676
|
}
|
|
2650
2677
|
finally {
|
|
2651
2678
|
graph.close();
|
|
@@ -444,3 +444,40 @@ export declare function unquoteGitPath(line: string): string;
|
|
|
444
444
|
* reproduce and compare an analysis from the exact same captured inputs.
|
|
445
445
|
*/
|
|
446
446
|
export declare function analyzeMaintenance(input: MaintenanceAnalysisInput): MaintenanceAnalysis;
|
|
447
|
+
/**
|
|
448
|
+
* Items a response lists before it starts counting instead.
|
|
449
|
+
*
|
|
450
|
+
* TEN, because that is the unit this product is measured in: localization emits Top-10 and every
|
|
451
|
+
* recall figure in this repository is recall@10. Aligning the response cap with the emission unit
|
|
452
|
+
* is a reason; picking a number that happens to hit a size target is a knob. Measured on the real
|
|
453
|
+
* 187,174-character response: cap 20 gave 34,811 with `intent` alone at 11,037 (32%), cap 10 fits.
|
|
454
|
+
*/
|
|
455
|
+
export declare const ANALYZE_LIST_CAP = 10;
|
|
456
|
+
/** Characters one listed item keeps. */
|
|
457
|
+
export declare const ANALYZE_TEXT_CAP = 400;
|
|
458
|
+
/**
|
|
459
|
+
* @implements A-SPEC-578.5
|
|
460
|
+
* The RESPONSE-EDGE projection. The artifact on disk keeps everything.
|
|
461
|
+
*
|
|
462
|
+
* Measured 2026-09-09: one `maintenance_analyze` call returns 187,174 characters (~47k tokens) and
|
|
463
|
+
* the harness refuses it outright. AGENTS.md step 3 instructs every agent to make that call before
|
|
464
|
+
* editing source — and across 2,779 spec approvals `persist:true` had never been used once. An
|
|
465
|
+
* instruction nobody can afford to follow is not an instruction.
|
|
466
|
+
*
|
|
467
|
+
* The cap is applied RECURSIVELY, which the first cut got wrong. Capping only the top level took
|
|
468
|
+
* the response from 187,174 to 142,701 and no further, because the weight is in NESTED DUPLICATES:
|
|
469
|
+
* `impacts.test` and `testScope.impactedTestFiles` carry the same 158 paths, and `impacts.contract`
|
|
470
|
+
* and `testScope.impactedSpecs` the same 160 spec ids. A cap that stops at depth one caps the
|
|
471
|
+
* cheapest arrays in the document.
|
|
472
|
+
*
|
|
473
|
+
* NEVER CALLED FROM `analyzeMaintenance`. The design-time advisory named the replay benchmark
|
|
474
|
+
* (`run-replay.ts`, `replay-corpus.ts`, `commit-text.ts`, `temporal-prior.ts`) as readers of this
|
|
475
|
+
* type, and among their anchors sits A-SPEC-402 — "the numbers reported are not what the product
|
|
476
|
+
* does". Bounding inside the analysis would make every pinned benchmark score a truncated
|
|
477
|
+
* pipeline, which is the most expensive instrument failure this repository has recorded. The
|
|
478
|
+
* benchmark keeps seeing exactly what it sees today; only the conversation gets the short form.
|
|
479
|
+
*
|
|
480
|
+
* What is dropped is COUNTED, at whatever depth it was dropped. A response that quietly shrank
|
|
481
|
+
* would be a worse lie than a long one.
|
|
482
|
+
*/
|
|
483
|
+
export declare function boundAnalysis(a: MaintenanceAnalysis): MaintenanceAnalysis;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PRIMARY_TIER_WIDTH = exports.EVIDENCE_DIMENSIONS = exports.MAINTENANCE_ANALYSIS_EXTRACTOR = exports.MAINTENANCE_ANALYSIS_SCHEMA = void 0;
|
|
3
|
+
exports.ANALYZE_TEXT_CAP = exports.ANALYZE_LIST_CAP = exports.PRIMARY_TIER_WIDTH = exports.EVIDENCE_DIMENSIONS = exports.MAINTENANCE_ANALYSIS_EXTRACTOR = exports.MAINTENANCE_ANALYSIS_SCHEMA = void 0;
|
|
4
4
|
exports.unquoteGitPath = unquoteGitPath;
|
|
5
5
|
exports.analyzeMaintenance = analyzeMaintenance;
|
|
6
|
+
exports.boundAnalysis = boundAnalysis;
|
|
6
7
|
// @implements A-SPEC-295
|
|
7
8
|
// @implements A-SPEC-294
|
|
8
9
|
// @implements A-SPEC-293
|
|
@@ -1048,3 +1049,74 @@ function buildAblation(a) {
|
|
|
1048
1049
|
], truth, a.primaryTier, a.impactedTier, a.importedByTier);
|
|
1049
1050
|
return { graphOff, current, enhanced };
|
|
1050
1051
|
}
|
|
1052
|
+
// @implements A-SPEC-578.5
|
|
1053
|
+
/**
|
|
1054
|
+
* Items a response lists before it starts counting instead.
|
|
1055
|
+
*
|
|
1056
|
+
* TEN, because that is the unit this product is measured in: localization emits Top-10 and every
|
|
1057
|
+
* recall figure in this repository is recall@10. Aligning the response cap with the emission unit
|
|
1058
|
+
* is a reason; picking a number that happens to hit a size target is a knob. Measured on the real
|
|
1059
|
+
* 187,174-character response: cap 20 gave 34,811 with `intent` alone at 11,037 (32%), cap 10 fits.
|
|
1060
|
+
*/
|
|
1061
|
+
exports.ANALYZE_LIST_CAP = 10;
|
|
1062
|
+
/** Characters one listed item keeps. */
|
|
1063
|
+
exports.ANALYZE_TEXT_CAP = 400;
|
|
1064
|
+
/** Dropped whole: another tool answers this question, and it is 41,619 characters of the answer. */
|
|
1065
|
+
const DROPPED_FIELDS = ['contextBundle'];
|
|
1066
|
+
/**
|
|
1067
|
+
* @implements A-SPEC-578.5
|
|
1068
|
+
* The RESPONSE-EDGE projection. The artifact on disk keeps everything.
|
|
1069
|
+
*
|
|
1070
|
+
* Measured 2026-09-09: one `maintenance_analyze` call returns 187,174 characters (~47k tokens) and
|
|
1071
|
+
* the harness refuses it outright. AGENTS.md step 3 instructs every agent to make that call before
|
|
1072
|
+
* editing source — and across 2,779 spec approvals `persist:true` had never been used once. An
|
|
1073
|
+
* instruction nobody can afford to follow is not an instruction.
|
|
1074
|
+
*
|
|
1075
|
+
* The cap is applied RECURSIVELY, which the first cut got wrong. Capping only the top level took
|
|
1076
|
+
* the response from 187,174 to 142,701 and no further, because the weight is in NESTED DUPLICATES:
|
|
1077
|
+
* `impacts.test` and `testScope.impactedTestFiles` carry the same 158 paths, and `impacts.contract`
|
|
1078
|
+
* and `testScope.impactedSpecs` the same 160 spec ids. A cap that stops at depth one caps the
|
|
1079
|
+
* cheapest arrays in the document.
|
|
1080
|
+
*
|
|
1081
|
+
* NEVER CALLED FROM `analyzeMaintenance`. The design-time advisory named the replay benchmark
|
|
1082
|
+
* (`run-replay.ts`, `replay-corpus.ts`, `commit-text.ts`, `temporal-prior.ts`) as readers of this
|
|
1083
|
+
* type, and among their anchors sits A-SPEC-402 — "the numbers reported are not what the product
|
|
1084
|
+
* does". Bounding inside the analysis would make every pinned benchmark score a truncated
|
|
1085
|
+
* pipeline, which is the most expensive instrument failure this repository has recorded. The
|
|
1086
|
+
* benchmark keeps seeing exactly what it sees today; only the conversation gets the short form.
|
|
1087
|
+
*
|
|
1088
|
+
* What is dropped is COUNTED, at whatever depth it was dropped. A response that quietly shrank
|
|
1089
|
+
* would be a worse lie than a long one.
|
|
1090
|
+
*/
|
|
1091
|
+
function boundAnalysis(a) {
|
|
1092
|
+
const src = a;
|
|
1093
|
+
const out = boundValue(src, 0);
|
|
1094
|
+
for (const field of DROPPED_FIELDS)
|
|
1095
|
+
delete out[field];
|
|
1096
|
+
return out;
|
|
1097
|
+
}
|
|
1098
|
+
/** Depth bound: deep enough for the shapes measured, shallow enough to terminate on any input. */
|
|
1099
|
+
const BOUND_MAX_DEPTH = 6;
|
|
1100
|
+
function boundValue(value, depth) {
|
|
1101
|
+
if (depth > BOUND_MAX_DEPTH || value === null || typeof value !== 'object')
|
|
1102
|
+
return trimItem(value);
|
|
1103
|
+
if (Array.isArray(value)) {
|
|
1104
|
+
return value.slice(0, exports.ANALYZE_LIST_CAP).map((v) => boundValue(v, depth + 1));
|
|
1105
|
+
}
|
|
1106
|
+
const src = value;
|
|
1107
|
+
const out = {};
|
|
1108
|
+
for (const [k, v] of Object.entries(src)) {
|
|
1109
|
+
out[k] = boundValue(v, depth + 1);
|
|
1110
|
+
// The counter sits BESIDE the array it describes, at the depth it was cut, so a nested trim is
|
|
1111
|
+
// as visible as a top-level one.
|
|
1112
|
+
if (Array.isArray(v) && v.length > exports.ANALYZE_LIST_CAP)
|
|
1113
|
+
out[`${k}Omitted`] = v.length - exports.ANALYZE_LIST_CAP;
|
|
1114
|
+
}
|
|
1115
|
+
return out;
|
|
1116
|
+
}
|
|
1117
|
+
/** Trim one listed item. A long item is CUT, never discarded — the item's existence is information. */
|
|
1118
|
+
function trimItem(item) {
|
|
1119
|
+
if (typeof item !== 'string' || item.length <= exports.ANALYZE_TEXT_CAP)
|
|
1120
|
+
return item;
|
|
1121
|
+
return `${item.slice(0, exports.ANALYZE_TEXT_CAP)}… [+${item.length - exports.ANALYZE_TEXT_CAP}]`;
|
|
1122
|
+
}
|
|
@@ -100,8 +100,49 @@ export interface OutcomeInput {
|
|
|
100
100
|
/**
|
|
101
101
|
* Attach what actually happened to a prediction that was actually made. An unknown digest is
|
|
102
102
|
* refused: inventing the prediction alongside the outcome would let the record score itself.
|
|
103
|
+
*
|
|
104
|
+
* @implements A-SPEC-578.6
|
|
105
|
+
* MERGES into any outcome already there. It used to REPLACE, and the defect was found by following
|
|
106
|
+
* this repository's own instruction: A-SPEC-578.4 made `test_run` attach the 12 files a slice
|
|
107
|
+
* actually changed, AGENTS.md then said "call maintenance_outcome yourself only to add a judged
|
|
108
|
+
* classification" — and that call left `actualFiles: []`. The documented workflow destroyed the
|
|
109
|
+
* data it existed to complete.
|
|
110
|
+
*
|
|
111
|
+
* Omission and erasure are DIFFERENT ACTS: a field left out keeps its value, a field given as an
|
|
112
|
+
* empty array is cleared. Without the second, a wrong record could never be corrected; without the
|
|
113
|
+
* first, completing a record destroys it.
|
|
103
114
|
*/
|
|
104
115
|
export declare function recordOutcome(dir: string, digest: string, outcome: OutcomeInput, recordedAt: string): EvidenceArtifact;
|
|
116
|
+
/**
|
|
117
|
+
* Predictions still waiting to be scored — the loop's missing second half.
|
|
118
|
+
*
|
|
119
|
+
* `since` bounds the window: an analysis recorded before the previous evidence run belonged to a
|
|
120
|
+
* previous slice, and attributing today's changes to it would be contamination rather than
|
|
121
|
+
* measurement. The boundary is EXCLUSIVE (`> since`) because the previous run already saw that
|
|
122
|
+
* instant.
|
|
123
|
+
*/
|
|
124
|
+
export declare function openArtifacts(dir: string, since?: string): EvidenceArtifact[];
|
|
125
|
+
/**
|
|
126
|
+
* @implements A-SPEC-578.4
|
|
127
|
+
* Attach what a slice ACTUALLY touched to the predictions it made.
|
|
128
|
+
*
|
|
129
|
+
* Measured 2026-09-09: `maintenance_calibration` had reported `n:0, artifacts:0` since the loop was
|
|
130
|
+
* built, because `maintenance_analyze({persist:true})` is instructed (AGENTS.md step 3) while
|
|
131
|
+
* `maintenance_outcome` is instructed nowhere at all. The write path was healthy the whole time —
|
|
132
|
+
* one call produced an artifact immediately. What was missing was the call.
|
|
133
|
+
*
|
|
134
|
+
* TWO REFUSALS make this a measurement rather than a self-graded exam:
|
|
135
|
+
* - `actualClassification` is never filled in. That is a judgement, and a loop that supplies its
|
|
136
|
+
* own judgement scores itself; the bins stay `insufficient-data` and only the objective half —
|
|
137
|
+
* file-level false positives and negatives — accrues.
|
|
138
|
+
* - An empty change set closes nothing. Attributing "nothing changed" would turn every prediction
|
|
139
|
+
* into a false positive: a measurement of nothing, recorded as a failure of everything.
|
|
140
|
+
*/
|
|
141
|
+
export declare function closeOpenArtifacts(dir: string, actual: {
|
|
142
|
+
files: string[];
|
|
143
|
+
}, recordedAt: string, since?: string): {
|
|
144
|
+
closed: string[];
|
|
145
|
+
};
|
|
105
146
|
export interface CalibrationBin {
|
|
106
147
|
lower: number;
|
|
107
148
|
upper: number;
|