@sabaiway/agent-workflow-kit 1.38.0 → 1.39.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.
@@ -7,17 +7,23 @@
7
7
  // shared hardened atomic-write core (tools/atomic-write.mjs — exclusive-create tmp + rename, TOCTOU
8
8
  // re-check, symlink STOPs). The ledger lives in the git dir (uncommittable by construction).
9
9
  //
10
- // Two record kinds, one JSONL ledger:
10
+ // Record kinds, one JSONL ledger. Every verb evaluates the current SEGMENT — (activity, loop,
11
+ // base = `git rev-parse HEAD`), BUGFREE-2 / AD-048 D1: round numbering, the caps, and every tooth
12
+ // are per segment; a segment closes only through a gated commit, so a counter reset is earned:
11
13
  // recordRound — one review round: per-backend counts + verdict + degraded, finding-origin tally,
12
- // findings[]. Binds to the canonical tree fingerprint. THE TEETH (Decision 5):
13
- // REFUSES (typed STOP) while decideStop on the existing records is `triage-required`
14
- // (an unclassified surviving blocking finding at/after the cap), and refuses ANY
15
- // round beyond the hard-max ceiling unconditionally. Integrity binding (Decision 7):
14
+ // findings[]. Binds to the canonical tree fingerprint + base. THE TEETH (Decision 5
15
+ // + AD-048): REFUSES (typed STOP) while decideStop on the segment's records is
16
+ // `triage-required`; refuses ANY round beyond the per-segment hard-max ceiling
17
+ // unconditionally; refuses while a blocking finding of the segment's previous
18
+ // round VANISHED unclassified (D6 — no-repro-no-fold; `refuted` is the honest
19
+ // phantom lane); refuses while the changed source surface exceeds the diff cap
20
+ // without a recorded segment size-cap override (D4). Integrity binding (Decision 7):
16
21
  // each NON-degraded backend needs a grounded code receipt for the current tree, so a
17
22
  // round cannot be recorded for a tree no bridge reviewed.
18
- // recordTriage — the classification that BREAKS the deadlock: each surviving blocking finding
19
- // classified fixable-bug / inherent-layer-residual / escalate. No teeth (a triage is
20
- // exactly what lets the next round proceed), no receipt binding (it reviews nothing).
23
+ // recordTriage — the classification that BREAKS the deadlock: each surviving blocking finding of a
24
+ // SEGMENT round classified fixable-bug / inherent-layer-residual / escalate /
25
+ // refuted (v4). No teeth (a triage is exactly what lets the next round proceed),
26
+ // no receipt binding (it reviews nothing).
21
27
  //
22
28
  // HONEST residual (stated, accepted — like review-state's): the ledger attests a review occurred; it
23
29
  // does NOT prove the recorded COUNTS are truthful nor that a self-reported `degraded:true` is real.
@@ -36,16 +42,32 @@ import {
36
42
  REVIEW_CAP,
37
43
  SCHEMA_VERSION,
38
44
  resolveLedgerPath,
45
+ resolveBase,
39
46
  readLedger,
40
- filterLoopRecords,
47
+ filterSegmentRecords,
48
+ collectSizeCapLimit,
49
+ isQualityGreenGateRun,
41
50
  roundSequenceIntact,
42
51
  decideStop,
43
52
  validateRecord,
44
53
  } from './review-ledger.mjs';
54
+ // The NEUTRAL shared changed-surface computation (BUGFREE-2 / D4): the D4 diff-cap and the
55
+ // fold-completeness coverage gate consume ONE computation, so they can never drift. The writer
56
+ // imports the NEUTRAL module, never the runner (the sole-tree-toucher boundary, codex R2 — an
57
+ // import-split test pins that this file never imports fold-completeness-run.mjs).
58
+ import { computeChangedSurface, countCapLines, parsePositiveIntKnob } from './changed-surface.mjs';
45
59
 
46
60
  // The absolute WRITER ceiling (Decision 5): hard-max lives ONLY here — it is NOT a decideStop input.
47
- // Even a fully-classified resolved-residual loop cannot reach a round beyond this.
61
+ // Even a fully-classified resolved-residual loop cannot reach a round beyond this. Since AD-048 it
62
+ // is scoped per SEGMENT (D3 — value unchanged, scope corrected): round numbers restart when a gated
63
+ // commit moves base, so a multiphase plan records fully while round 4 within ONE segment stays
64
+ // refused — that ceiling is the point (the 2026-06-30 six-round incident class).
48
65
  export const HARD_MAX = 3;
66
+ // The D4 diff-size review cap (default 400 changed source lines; AW_REVIEW_DIFF_CAP overrides
67
+ // through the shared fail-closed positive-integer parser). Counted classes are pinned in
68
+ // changed-surface.mjs: assessable + unsupported SOURCE lines count; tests and out-of-domain never
69
+ // do; pure deletions are free.
70
+ export const DEFAULT_DIFF_CAP = 400;
49
71
  const DEFAULT_ACTIVITY = 'plan-execution';
50
72
 
51
73
  // A typed STOP — a deliberate refusal we surface (the teeth / a malformed record / a missing
@@ -90,11 +112,13 @@ export const recordRound = (params, deps = {}) => {
90
112
  if (ledgerPath == null) throw stop('cannot resolve the ledger path — not a git work tree and AW_REVIEW_LEDGER is unset');
91
113
  if (!(Number.isInteger(round) && round >= 1)) throw stop(`round must be an integer >= 1 (got ${round})`);
92
114
  // The hard-max ceiling: refuse ANY round beyond it, independent of triage state (Decision 5).
115
+ // Per SEGMENT since AD-048 (D3): the round number is the segment round number.
93
116
  if (round > HARD_MAX) {
94
- throw stop(`refusing to record round ${round}: beyond the hard-max ceiling of ${HARD_MAX} rounds — the loop must converge or the surviving finding must escalate, never another round`);
117
+ throw stop(`refusing to record round ${round}: beyond the hard-max ceiling of ${HARD_MAX} rounds within one segment — the segment must converge (or its surviving finding escalate) and ship through a gated commit; the counter resets only at the commit boundary, never by declaration`);
95
118
  }
96
119
 
97
120
  const fingerprint = deps.computeFingerprint ? deps.computeFingerprint(cwd) : computeTreeFingerprint(cwd);
121
+ const base = deps.resolveBase ? deps.resolveBase(cwd) : resolveBase(cwd);
98
122
 
99
123
  // The teeth: refuse a new round WHILE decideStop on the existing records is triage-required. Once
100
124
  // the surviving blocking finding is classified (recordTriage), decideStop is no longer
@@ -107,25 +131,79 @@ export const recordRound = (params, deps = {}) => {
107
131
  // the teeth OPEN (codex R1). Refuse to append until the ledger is sound.
108
132
  if (readError) throw stop(`cannot read the existing ledger (${readError}) — refusing to append (fail closed)`);
109
133
  if (malformed > 0) throw stop(`the existing ledger has ${malformed} malformed line(s) — refusing to append until they are fixed (fail closed): ${malformedReasons.join('; ')}`);
110
- const existingLoop = filterLoopRecords(records, { activity, loop });
111
- // Sequence integrity + sequentiality (codex R2+R3): the EXISTING rounds must already be exactly
112
- // 1..n (never trust a hand-corrupted [2]/[1,1]/[2,1] to compute "latest"), AND the incoming round
113
- // must be exactly the next (n+1). A duplicate, decreasing, or gapped round would let a fabricated
114
- // "later" round become the latest that decideStop reads, bypassing the "latest round" teeth.
115
- const priorRounds = existingLoop.filter((r) => r.kind === 'round').map((r) => r.round);
116
- if (!roundSequenceIntact(existingLoop)) {
117
- throw stop(`refusing to append to loop "${loop}": its recorded round sequence is corrupt (${priorRounds.join(',') || 'empty'}, not 1..n) — fix the ledger by hand before recording another round`);
134
+ // EVERY tooth below evaluates the current SEGMENT (activity, loop, base) — D1: records of earlier
135
+ // segments (other bases, or pre-v4 records with no base) are closed history; the field-proven
136
+ // 11-round / 4-base loop records completely while round 4 within ONE segment stays refused.
137
+ const segment = filterSegmentRecords(records, { activity, loop, base });
138
+ // Sequence integrity + sequentiality (codex R2+R3): the EXISTING segment rounds must already be
139
+ // exactly 1..n (never trust a hand-corrupted [2]/[1,1]/[2,1] to compute "latest"), AND the
140
+ // incoming round must be exactly the next (n+1). A duplicate, decreasing, or gapped round would
141
+ // let a fabricated "later" round become the latest that decideStop reads, bypassing the teeth.
142
+ const priorRounds = segment.filter((r) => r.kind === 'round').map((r) => r.round);
143
+ if (!roundSequenceIntact(segment)) {
144
+ throw stop(`refusing to append to loop "${loop}": its recorded round sequence for the current segment is corrupt (${priorRounds.join(',') || 'empty'}, not 1..n) — fix the ledger by hand before recording another round`);
118
145
  }
119
146
  const nextRound = priorRounds.length + 1;
120
147
  if (round !== nextRound) {
121
- throw stop(`refusing to record round ${round}: rounds must be sequential — the next round for loop "${loop}" is ${nextRound} (a duplicate, out-of-order, or gapped round would corrupt the crossover computation)`);
148
+ throw stop(`refusing to record round ${round}: rounds must be sequential within the segment — the next round for loop "${loop}" at the current base is ${nextRound} (a duplicate, out-of-order, or gapped round would corrupt the crossover computation)`);
122
149
  }
123
- const pre = decideStop(existingLoop, { cap: REVIEW_CAP });
150
+ const pre = decideStop(segment, { cap: REVIEW_CAP });
124
151
  if (pre.state === 'triage-required') {
125
152
  throw stop(`refusing to record a new round while triage is required — ${pre.reason}. Classify the surviving blocking finding(s) with the "classify" command first (a fixable-bug classification permits the fix round).`);
126
153
  }
127
154
 
128
- const record = { schema: SCHEMA_VERSION, loop, activity, kind: 'round', round, fingerprint, origins, backends, findings, timestamp: timestamp ?? isoNow() };
155
+ // D6 no-repro-no-fold: no blocking finding of the segment's previous round may VANISH
156
+ // unclassified. Present-again is fine (still live); classified is fine (fixable-bug folded with
157
+ // its red→green testId at the round it was folded — late binding restored; inherent-layer-residual
158
+ // documented; escalate handed over; refuted — the honest phantom lane, grounds mandatory). A
159
+ // silent disappearance is exactly the sycophancy hole this pillar closes. Minors stay exempt.
160
+ const previous = segment.find((r) => r.kind === 'round' && r.round === nextRound - 1);
161
+ if (previous) {
162
+ // "Present" means present AS BLOCKING: a blocker/major re-reported as a minor did not survive —
163
+ // it was softened, and the silent-soften lane is exactly the bypass D6 closes (codex R1).
164
+ const incoming = new Set(
165
+ Array.isArray(findings)
166
+ ? findings.filter((f) => f && (f.severity === 'blocker' || f.severity === 'major')).map((f) => f.findingKey)
167
+ : [],
168
+ );
169
+ // A classification CLEARS the vanish only when it actually resolves the finding's fate:
170
+ // fixable-bug (folded, testId bound), inherent-layer-residual (documented), refuted (grounds
171
+ // cited), or an ACCEPTED escalate — a pending escalate (accepted:false) is still undecided and
172
+ // must not disappear into a clean round (codex R1).
173
+ const clearsVanish = (c) =>
174
+ c.class === 'fixable-bug' || c.class === 'inherent-layer-residual' || c.class === 'refuted' || (c.class === 'escalate' && c.accepted === true);
175
+ const classified = new Set();
176
+ for (const t of segment) {
177
+ if (t.kind === 'triage' && t.round === previous.round) for (const c of t.classifications) if (clearsVanish(c)) classified.add(c.findingKey);
178
+ }
179
+ const vanished = [...new Set(
180
+ previous.findings
181
+ .filter((f) => f.severity === 'blocker' || f.severity === 'major')
182
+ .map((f) => f.findingKey)
183
+ .filter((k) => !incoming.has(k) && !classified.has(k)),
184
+ )];
185
+ if (vanished.length > 0) {
186
+ throw stop(`refusing to record round ${round}: blocking finding(s) of round ${previous.round} vanished without a classification: ${vanished.join(', ')} (D6 — no blocking finding disappears silently). Classify each with the "classify" command first: fixable-bug (bind the red→green testId), inherent-layer-residual, escalate, or refuted (a phantom finding — cite the refuting grounds in note).`);
187
+ }
188
+ }
189
+
190
+ // D4 — the diff-size cap over the ONE shared changed-surface computation (changed-surface.mjs):
191
+ // a round is refused while the changed source surface exceeds the cap, unless the SEGMENT carries
192
+ // a recorded size-cap override sanctioning at least the counted magnitude. Subtractive folds are
193
+ // free (new-side lines only); tests and out-of-domain files never count.
194
+ const cap = parsePositiveIntKnob(env, 'AW_REVIEW_DIFF_CAP', DEFAULT_DIFF_CAP, stop);
195
+ const counted = deps.countChangedLines ? deps.countChangedLines(cwd) : countCapLines(computeChangedSurface(gitRoot(cwd) ?? cwd));
196
+ if (counted > cap) {
197
+ const sanctioned = collectSizeCapLimit(records, { activity, loop, base });
198
+ if (sanctioned === null || counted > sanctioned) {
199
+ throw stop(
200
+ `refusing to record round ${round}: the changed source surface is ${counted} lines — over the ${cap}-line review cap (AW_REVIEW_DIFF_CAP)${sanctioned !== null ? ` and over the segment's recorded size-cap sanction of ${sanctioned}` : ''}. ` +
201
+ `Split the change into reviewable units (commit a converged part first), or record the LOUD waiver: node review-ledger-write.mjs override --json '{"loop":"${loop}","round":${round},"scope":"size-cap","sanctionedLines":${counted},"reason":"<why this surface must review as one unit>"}'`,
202
+ );
203
+ }
204
+ }
205
+
206
+ const record = { schema: SCHEMA_VERSION, loop, activity, kind: 'round', round, base, fingerprint, origins, backends, findings, timestamp: timestamp ?? isoNow() };
129
207
  const v = validateRecord(record);
130
208
  if (!v.ok) throw stop(`refusing to record a malformed round: ${v.reason}`);
131
209
 
@@ -143,6 +221,53 @@ export const recordRound = (params, deps = {}) => {
143
221
  }
144
222
  }
145
223
 
224
+ // D5 — the green-baseline tooth (armed at Step 2.3, AFTER run-gates --record exists — the
225
+ // bootstrap order): a round records only over a tree whose FULL declared non-process gate set
226
+ // was proven green by a recorded gate-run at the CURRENT fingerprint. "Gates ran before review"
227
+ // is now computed, never remembered. A `--only` subset is recorded honestly but never satisfies
228
+ // this (the R1 converged subset-bypass hole); a run whose tree changed under it attests no
229
+ // particular tree (codex R2); process-gate failures never block (the closed carve-out).
230
+ const gateRuns = segment.filter((r) => r.kind === 'gate-run');
231
+ if (!gateRuns.some((g) => g.fingerprint === fingerprint && isQualityGreenGateRun(g))) {
232
+ throw stop(`refusing to record round ${round}: no quality-green gate-run for the current tree in this segment (D5 — gates run before review is computed, not remembered). Run the FULL declared matrix with a recorded receipt first: node agent-workflow-kit/tools/run-gates.mjs --record (a --only subset never satisfies this; a run whose tree changed under it never counts).`);
233
+ }
234
+
235
+ return appendRecord(ledgerPath, record, deps);
236
+ };
237
+
238
+ // ── recordGateRun (BUGFREE-2 / AD-048, D5 — the green-baseline receipt run-gates --record mints) ──
239
+
240
+ // recordGateRun({ cwd, env, activity, declared, results, summary, fingerprintBefore,
241
+ // fingerprintAfter, timestamp }, deps) → { writtenPath, record }. The SOLE ledger entry point for
242
+ // run-gates (`--record` DELEGATES here — the runner never opens the ledger file itself; an
243
+ // import/structure pin holds the boundary). The loop is DERIVED from the single in-flight plan
244
+ // (the recordOverride precedent — a gate-run is minted only inside its live loop, never
245
+ // retro-recorded); the record carries the segment frame and NO round number (per-kind frame, D5).
246
+ // A red run records honestly (telemetry fuel — consecutive red gate-runs are the revert-first
247
+ // visibility); quality-green is judged at read time, never stored.
248
+ export const recordGateRun = (params, deps = {}) => {
249
+ const { cwd = process.cwd(), env = process.env, activity = DEFAULT_ACTIVITY, declared, results, summary, fingerprintBefore, fingerprintAfter, timestamp } = params;
250
+ const ledgerPath = deps.ledgerPath ?? resolveLedgerPath(cwd, env);
251
+ if (ledgerPath == null) throw stop('cannot resolve the ledger path — not a git work tree and AW_REVIEW_LEDGER is unset');
252
+ const plans = deps.plansInFlight ? deps.plansInFlight() : plansInFlight(gitRoot(cwd) ?? cwd);
253
+ if (plans.length !== 1) {
254
+ throw stop(`refusing to record a gate-run: it is minted only inside the SINGLE in-flight loop (in flight: ${plans.length ? plans.join(', ') : 'none'})`);
255
+ }
256
+ const loop = plans[0].replace(/\.md$/, '');
257
+ const base = deps.resolveBase ? deps.resolveBase(cwd) : resolveBase(cwd);
258
+ const record = {
259
+ schema: SCHEMA_VERSION, loop, activity, kind: 'gate-run', base,
260
+ fingerprint: fingerprintBefore, fingerprintAfter, declared, results, summary,
261
+ timestamp: timestamp ?? isoNow(),
262
+ };
263
+ const v = validateRecord(record);
264
+ if (!v.ok) throw stop(`refusing to record a malformed gate-run: ${v.reason}`);
265
+ const { records, malformed, malformedReasons, readError } = readLedger(ledgerPath, deps.readFile);
266
+ if (readError) throw stop(`cannot read the existing ledger (${readError}) — refusing to append (fail closed)`);
267
+ if (malformed > 0) throw stop(`the existing ledger has ${malformed} malformed line(s) — refusing to append until they are fixed (fail closed): ${malformedReasons.join('; ')}`);
268
+ if (!roundSequenceIntact(filterSegmentRecords(records, { activity, loop, base }))) {
269
+ throw stop(`refusing to record a gate-run for loop "${loop}": its recorded round sequence for the current segment is corrupt (not 1..n) — fix the ledger by hand first`);
270
+ }
146
271
  return appendRecord(ledgerPath, record, deps);
147
272
  };
148
273
 
@@ -162,7 +287,7 @@ const gitRoot = (cwd) => {
162
287
  // inside its live loop, never retro-recorded for a finished or foreign one. The fold-completeness
163
288
  // gate matches on loop + payload, never on the (audit-only) fingerprint.
164
289
  export const recordOverride = (params, deps = {}) => {
165
- const { cwd = process.cwd(), env = process.env, loop, activity = DEFAULT_ACTIVITY, round, scope, files, testId, reason, timestamp } = params;
290
+ const { cwd = process.cwd(), env = process.env, loop, activity = DEFAULT_ACTIVITY, round, scope, files, testId, sanctionedLines, reason, timestamp } = params;
166
291
  const ledgerPath = deps.ledgerPath ?? resolveLedgerPath(cwd, env);
167
292
  if (ledgerPath == null) throw stop('cannot resolve the ledger path — not a git work tree and AW_REVIEW_LEDGER is unset');
168
293
  if (!(Number.isInteger(round) && round >= 1)) throw stop(`round must be an integer >= 1 (got ${round})`);
@@ -174,9 +299,11 @@ export const recordOverride = (params, deps = {}) => {
174
299
  throw stop(`refusing to record an override for loop "${loop}": an override is minted only inside its SINGLE in-flight loop (in flight: ${plans.length ? plans.join(', ') : 'none'})`);
175
300
  }
176
301
  const fingerprint = deps.computeFingerprint ? deps.computeFingerprint(cwd) : computeTreeFingerprint(cwd);
302
+ const base = deps.resolveBase ? deps.resolveBase(cwd) : resolveBase(cwd);
177
303
  const record = {
178
- schema: SCHEMA_VERSION, loop, activity, kind: 'override', round, fingerprint, scope,
304
+ schema: SCHEMA_VERSION, loop, activity, kind: 'override', round, base, fingerprint, scope,
179
305
  ...(files !== undefined ? { files } : {}), ...(testId !== undefined ? { testId } : {}),
306
+ ...(sanctionedLines !== undefined ? { sanctionedLines } : {}),
180
307
  reason, timestamp: timestamp ?? isoNow(),
181
308
  };
182
309
  const v = validateRecord(record);
@@ -184,8 +311,8 @@ export const recordOverride = (params, deps = {}) => {
184
311
  const { records, malformed, malformedReasons, readError } = readLedger(ledgerPath, deps.readFile);
185
312
  if (readError) throw stop(`cannot read the existing ledger (${readError}) — refusing to append (fail closed)`);
186
313
  if (malformed > 0) throw stop(`the existing ledger has ${malformed} malformed line(s) — refusing to append until they are fixed (fail closed): ${malformedReasons.join('; ')}`);
187
- if (!roundSequenceIntact(filterLoopRecords(records, { activity, loop }))) {
188
- throw stop(`refusing to record an override for loop "${loop}": its recorded round sequence is corrupt (not 1..n) — fix the ledger by hand first`);
314
+ if (!roundSequenceIntact(filterSegmentRecords(records, { activity, loop, base }))) {
315
+ throw stop(`refusing to record an override for loop "${loop}": its recorded round sequence for the current segment is corrupt (not 1..n) — fix the ledger by hand first`);
189
316
  }
190
317
  return appendRecord(ledgerPath, record, deps);
191
318
  };
@@ -200,27 +327,31 @@ export const recordTriage = (params, deps = {}) => {
200
327
  if (ledgerPath == null) throw stop('cannot resolve the ledger path — not a git work tree and AW_REVIEW_LEDGER is unset');
201
328
  if (!(Number.isInteger(round) && round >= 1)) throw stop(`round must be an integer >= 1 (got ${round})`);
202
329
  const fingerprint = deps.computeFingerprint ? deps.computeFingerprint(cwd) : computeTreeFingerprint(cwd);
330
+ const base = deps.resolveBase ? deps.resolveBase(cwd) : resolveBase(cwd);
203
331
  // Normalize each classification: an absent testId → null, an absent note → '' — an absent optional
204
332
  // field is FILLED, never rejected as malformed (agy R3). Under schema v2 (M2/AD-046) a fixable-bug
205
333
  // normalized to a null testId then FAILS validateRecord below (a typed STOP naming the rule + the
206
- // red-test-first fix) — the test-per-fold binding rides the existing validate path. A non-array is
207
- // left as-is for validateRecord to reject with a typed STOP (never a raw .map TypeError).
334
+ // red-test-first fix) — the test-per-fold binding rides the existing validate path; a `refuted`
335
+ // classification normalized to an empty note fails the same way (D6 the grounds are mandatory).
336
+ // A non-array is left as-is for validateRecord to reject with a typed STOP (never a raw .map TypeError).
208
337
  const normalized = Array.isArray(classifications) ? classifications.map((c) => ({ ...c, testId: c?.testId ?? null, note: c?.note ?? '' })) : classifications;
209
- const record = { schema: SCHEMA_VERSION, loop, activity, kind: 'triage', round, fingerprint, classifications: normalized, timestamp: timestamp ?? isoNow() };
338
+ const record = { schema: SCHEMA_VERSION, loop, activity, kind: 'triage', round, base, fingerprint, classifications: normalized, timestamp: timestamp ?? isoNow() };
210
339
  const v = validateRecord(record);
211
340
  if (!v.ok) throw stop(`refusing to record a malformed triage: ${v.reason}`);
212
341
 
213
- // Bind the triage to a REAL round: the referenced round must exist and every classified findingKey
214
- // must be a surviving blocking finding (blocker or major) of THAT round — a classification for a
215
- // nonexistent/future round, or for a key the round never raised, must not satisfy resolved-residual
216
- // downstream (codex R1). Fail CLOSED on an unreadable / malformed ledger.
342
+ // Bind the triage to a REAL round of the current SEGMENT: the referenced round must exist there
343
+ // and every classified findingKey must be a surviving blocking finding (blocker or major) of THAT
344
+ // round — a classification for a nonexistent/future/other-segment round, or for a key the round
345
+ // never raised, must not satisfy resolved-residual downstream (codex R1; D1 a committed
346
+ // segment's rounds are closed, the cross-segment lane is the D7 red-proof override). Fail CLOSED
347
+ // on an unreadable / malformed ledger.
217
348
  const { records, malformed, malformedReasons, readError } = readLedger(ledgerPath, deps.readFile);
218
349
  if (readError) throw stop(`cannot read the existing ledger (${readError}) — refusing to append (fail closed)`);
219
350
  if (malformed > 0) throw stop(`the existing ledger has ${malformed} malformed line(s) — refusing to append until they are fixed (fail closed): ${malformedReasons.join('; ')}`);
220
- const loopRecords = filterLoopRecords(records, { activity, loop });
221
- if (!roundSequenceIntact(loopRecords)) throw stop(`refusing to classify loop "${loop}": its recorded round sequence is corrupt (not 1..n) — fix the ledger by hand first`);
222
- const targetRound = loopRecords.find((r) => r.kind === 'round' && r.round === round);
223
- if (!targetRound) throw stop(`refusing to classify round ${round} of loop "${loop}": no such recorded round — classify a round that exists`);
351
+ const segment = filterSegmentRecords(records, { activity, loop, base });
352
+ if (!roundSequenceIntact(segment)) throw stop(`refusing to classify loop "${loop}": its recorded round sequence for the current segment is corrupt (not 1..n) — fix the ledger by hand first`);
353
+ const targetRound = segment.find((r) => r.kind === 'round' && r.round === round);
354
+ if (!targetRound) throw stop(`refusing to classify round ${round} of loop "${loop}": no such recorded round in the current segment — classify a round that exists at the current base`);
224
355
  const survivingKeys = new Set(targetRound.findings.filter((f) => f.severity === 'blocker' || f.severity === 'major').map((f) => f.findingKey));
225
356
  for (const c of classifications) {
226
357
  if (!survivingKeys.has(c.findingKey)) throw stop(`refusing to classify "${c.findingKey}": it is not a surviving blocking finding of round ${round} (classify only that round's blockers/majors)`);
@@ -231,29 +362,42 @@ export const recordTriage = (params, deps = {}) => {
231
362
 
232
363
  // ── CLI (record / classify) ──────────────────────────────────────────────────────────────────────
233
364
 
234
- const HELP = `review-ledger-write — the review-round ledger WRITER (agent-workflow family, AD-045 + AD-047).
365
+ const HELP = `review-ledger-write — the review-round ledger WRITER (agent-workflow family, AD-045 + AD-047 + AD-048).
235
366
 
236
367
  Usage:
237
368
  node review-ledger-write.mjs record --json '<round-payload>' [--cwd <dir>]
238
369
  node review-ledger-write.mjs classify --json '<triage-payload>' [--cwd <dir>]
239
370
  node review-ledger-write.mjs override --json '<override-payload>' [--cwd <dir>]
240
371
 
372
+ Every verb operates on the current SEGMENT — (loop, base = git rev-parse HEAD): round numbers,
373
+ caps, and teeth reset only when a gated commit moves base (schema 4; records carry base).
374
+
241
375
  record appends one review round. The JSON payload carries { loop, round, origins, backends,
242
376
  findings } (activity defaults to plan-execution; timestamp defaults to now). REFUSES while
243
- triage is required, beyond the hard-max ceiling of ${HARD_MAX} rounds, or when a non-degraded
244
- backend lacks a grounded code receipt for the current tree.
377
+ triage is required; beyond the hard-max ceiling of ${HARD_MAX} rounds within one segment;
378
+ while a blocking finding of the segment's previous round VANISHED unclassified (D6 —
379
+ classify it first, "refuted" is the honest phantom lane); while the changed source surface
380
+ exceeds the ${DEFAULT_DIFF_CAP}-line diff cap (AW_REVIEW_DIFF_CAP) without a recorded
381
+ segment size-cap override (D4); while the segment lacks a quality-green gate-run at the
382
+ current fingerprint (D5 — run the FULL matrix first: run-gates.mjs --record; a --only
383
+ subset or a tree-changed run never satisfies; red PROCESS gates never block); or when a
384
+ non-degraded backend lacks a grounded code receipt for the current tree.
245
385
  classify appends one triage record. The JSON payload carries { loop, round, classifications } (each
246
386
  { findingKey, class, accepted, testId, note }). A fixable-bug REQUIRES a testId — the
247
387
  red→green test that pins the fold, formatted "<test-file>#<test-name-pattern>" (write it
248
- first); inherent-layer-residual / escalate may omit it. This is what permits the next round.
249
- override appends one override record (schema 3) the LOUD, durable waiver the fold-completeness
250
- gate consumes. The JSON payload carries { loop, round, scope, reason } plus, per scope:
388
+ first); refuted REQUIRES a non-empty note citing the refuting grounds;
389
+ inherent-layer-residual / escalate may omit the testId. This is what permits the next round.
390
+ override appends one override record the LOUD, durable waiver the gates consume. The JSON payload
391
+ carries { loop, round, scope, reason } plus, per scope:
251
392
  scope "oracle-change" → files[] (non-empty repo-relative paths whose tamper flag it lifts);
252
393
  scope "red-proof" → testId (the exact bound testId whose observed-red receipt + custody it
253
- waives — for a red that is GENUINELY unestablishable pre-fold, D7). REFUSES for a loop that
254
- is not the in-flight plan. QUARANTINE (a flaky/timed-out probe) has NO override lane.
394
+ waives — for a red that is GENUINELY unestablishable pre-fold, D7);
395
+ scope "size-cap" sanctionedLines (the exact changed-surface magnitude the waiver
396
+ sanctions, SEGMENT-scoped — it dies at the next commit, D4).
397
+ REFUSES for a loop that is not the in-flight plan. QUARANTINE (a flaky/timed-out probe)
398
+ has NO override lane.
255
399
 
256
- The read-only checker is a SEPARATE tool: node review-ledger.mjs --check / --status / --json.
400
+ The read-only checker is a SEPARATE tool: node review-ledger.mjs --check / --status / --json / --telemetry.
257
401
  Exit codes: 0 written; 1 a typed STOP (teeth / malformed / missing receipt / fs error); 2 usage.`;
258
402
 
259
403
  const parseArgs = (argv) => {