@sabaiway/agent-workflow-kit 1.38.0 → 1.40.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 +108 -0
- package/README.md +6 -5
- package/SKILL.md +7 -3
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/bootstrap.md +1 -1
- package/references/modes/doc-parity.md +18 -0
- package/references/modes/fold-completeness.md +10 -6
- package/references/modes/gates.md +5 -3
- package/references/modes/grounding.md +4 -3
- package/references/modes/review-ledger.md +10 -6
- package/references/modes/review-state.md +1 -0
- package/references/modes/upgrade.md +3 -1
- package/references/modes/velocity.md +1 -1
- package/references/scripts/archive-decisions.mjs +39 -2
- package/references/scripts/archive-decisions.test.mjs +118 -1
- package/references/scripts/check-docs-size.mjs +50 -24
- package/references/scripts/check-docs-size.test.mjs +73 -1
- package/references/templates/agent_rules.md +1 -0
- package/references/templates/verification-profile.json +10 -0
- package/tools/changed-surface.mjs +294 -0
- package/tools/commands.mjs +10 -3
- package/tools/doc-parity.mjs +139 -0
- package/tools/fold-completeness-run.mjs +518 -252
- package/tools/fold-completeness.mjs +184 -66
- package/tools/grounding.mjs +92 -5
- package/tools/lcov.mjs +127 -0
- package/tools/procedures.mjs +1 -1
- package/tools/review-ledger-write.mjs +253 -50
- package/tools/review-ledger.mjs +382 -53
- package/tools/review-state.mjs +69 -3
- package/tools/run-gates.mjs +101 -11
- package/tools/sarif.mjs +52 -0
- package/tools/verification-profile.mjs +219 -0
package/tools/lcov.mjs
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// lcov.mjs — a dependency-free LCOV parser for the fold-completeness runner's coverage.kind:"lcov"
|
|
3
|
+
// branch (BUGFREE-3, AD-049 — the language-independence contract). When a verification profile
|
|
4
|
+
// declares coverage.kind:"lcov", the consumer's OWN suite leaves an LCOV file at the declared path
|
|
5
|
+
// (the diff-cover / c8 --reporter=lcov pattern) and this module reads it into a per-file
|
|
6
|
+
// line→hits map, keyed by the SAME canonical absolute path the V8 coverage map uses — so the
|
|
7
|
+
// runner's ONE uncovered-changed loop consumes either source unchanged (computeUncoveredLines /
|
|
8
|
+
// effectiveCount stay the V8-only path, D10).
|
|
9
|
+
//
|
|
10
|
+
// FAIL-CLOSED posture (the whole reason the fold gate exists): a malformed record can never mark a
|
|
11
|
+
// line COVERED — a DA with a valid line but a non-integer HIT count reads the line UNCOVERED (hits 0,
|
|
12
|
+
// the gate fails), and a DA with no valid line number is skipped (unattributable — there is no line to
|
|
13
|
+
// flag), never a false green. An SF path that resolves OUTSIDE the repo — or does not resolve at all —
|
|
14
|
+
// is never trusted as coverage (a wrong file could otherwise read covered). (STATED residual, inherent
|
|
15
|
+
// to the LCOV data model: a CONFORMANT producer emits DA:N,0 for every uncovered executable line, so a
|
|
16
|
+
// changed executable line with NO DA entry reads non-executable — LCOV carries no separate
|
|
17
|
+
// executability signal; the V8 path, which reads an absent line as uncovered, differs by format.)
|
|
18
|
+
//
|
|
19
|
+
// Dependency-free, Node >= 18. No side effects on import.
|
|
20
|
+
|
|
21
|
+
import { join, resolve, isAbsolute, sep } from 'node:path';
|
|
22
|
+
import { realpathSync } from 'node:fs';
|
|
23
|
+
|
|
24
|
+
// Only these three LCOV records are consulted; every other record (FN/FNDA/BRDA/LF/LH/BRF/…) is
|
|
25
|
+
// ignored. LCOV groups per-file sections between an `SF:<path>` line and `end_of_record`.
|
|
26
|
+
const SF_RE = /^SF:(.*)$/;
|
|
27
|
+
const DA_RE = /^DA:(\d+),(.*)$/; // DA:<line>,<hits-field>… — the LINE must be an int; the hits field is
|
|
28
|
+
// parsed leniently below so a truncated/malformed hit count reads UNCOVERED (fail-closed), not omitted.
|
|
29
|
+
const END_RE = /^end_of_record\s*$/;
|
|
30
|
+
|
|
31
|
+
// parseLcov(text) → Map<sfString, Map<lineNumber, hits>>. Raw SF strings (NOT yet canonicalized).
|
|
32
|
+
// A line's hits is the MAX across duplicate DA entries within a section (some tracers emit per-test
|
|
33
|
+
// sections that also repeat lines). A DA whose line or hit count is not a base-10 integer is skipped
|
|
34
|
+
// (fail-closed: it can never mark a line covered). Lines with no DA entry never appear (a
|
|
35
|
+
// non-executable line — blank/comment — is simply absent, mirroring the V8 blank-line skip).
|
|
36
|
+
export const parseLcov = (text) => {
|
|
37
|
+
const out = new Map();
|
|
38
|
+
let current = null; // the Map<line,hits> for the SF section in progress
|
|
39
|
+
for (const rawLine of String(text).split('\n')) {
|
|
40
|
+
const line = rawLine.replace(/\r$/, '');
|
|
41
|
+
const sf = SF_RE.exec(line);
|
|
42
|
+
if (sf) {
|
|
43
|
+
const path = sf[1].trim();
|
|
44
|
+
if (path.length === 0) {
|
|
45
|
+
current = null;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
current = out.get(path);
|
|
49
|
+
if (!current) {
|
|
50
|
+
current = new Map();
|
|
51
|
+
out.set(path, current);
|
|
52
|
+
}
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (END_RE.test(line)) {
|
|
56
|
+
current = null;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const da = DA_RE.exec(line);
|
|
60
|
+
if (da && current) {
|
|
61
|
+
const n = Number.parseInt(da[1], 10);
|
|
62
|
+
if (!Number.isInteger(n) || n < 1) continue; // no valid line number → unattributable, skip
|
|
63
|
+
const hitsField = da[2].split(',')[0]; // the hit count (a trailing ,checksum is ignored)
|
|
64
|
+
// FAIL-CLOSED, STRICT: the hit count must be ALL digits — a truncated `DA:42,`, a
|
|
65
|
+
// `DA:2,xyz`, a partial `DA:5,1abc`, or a fractional `DA:6,2.5` all read UNCOVERED (hits 0), never
|
|
66
|
+
// parseInt-coerced to a positive "covered" value. A covered sibling still wins (max).
|
|
67
|
+
const effHits = /^\d+$/.test(hitsField) ? Number.parseInt(hitsField, 10) : 0;
|
|
68
|
+
current.set(n, Math.max(current.get(n) ?? 0, effHits));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const defaultCanon = (p) => {
|
|
75
|
+
try {
|
|
76
|
+
return realpathSync(p);
|
|
77
|
+
} catch {
|
|
78
|
+
return p;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Segment-safe containment ('/a' never contains '/ab'); correct at the filesystem root (mirrors
|
|
83
|
+
// fold-completeness-run.mjs containsPath). Defined locally so lcov.mjs never imports the runner (the
|
|
84
|
+
// runner imports THIS module).
|
|
85
|
+
const containsPath = (realRoot, realAbs) =>
|
|
86
|
+
realAbs === realRoot || realAbs.startsWith(realRoot.endsWith(sep) ? realRoot : realRoot + sep);
|
|
87
|
+
|
|
88
|
+
// lcovCoveredMap(text, rootTop, { canon }?) → Map<canonAbsKey, Map<line, hits>>. Each SF is resolved
|
|
89
|
+
// against rootTop (a relative / ./-prefixed / absolute-in-repo SF all normalize to the same key the
|
|
90
|
+
// V8 map uses), canon()-normalized, and KEPT only when it resolves strictly INSIDE the repo root; an
|
|
91
|
+
// outside-tree or unresolvable SF is DROPPED (never trusted as coverage). Duplicate SF sections are
|
|
92
|
+
// merged taking the max hits per line. canon is injectable for hermetic tests (default realpathSync).
|
|
93
|
+
export const lcovCoveredMap = (text, rootTop, deps = {}) => {
|
|
94
|
+
const canon = deps.canon ?? defaultCanon;
|
|
95
|
+
const raw = parseLcov(text);
|
|
96
|
+
const rootCanon = canon(rootTop);
|
|
97
|
+
const out = new Map();
|
|
98
|
+
for (const [sf, lineHits] of raw) {
|
|
99
|
+
const abs = isAbsolute(sf) ? sf : resolve(rootTop, sf);
|
|
100
|
+
const key = canon(abs);
|
|
101
|
+
if (!containsPath(rootCanon, key)) continue; // outside the repo (or unresolved elsewhere) → drop
|
|
102
|
+
let m = out.get(key);
|
|
103
|
+
if (!m) {
|
|
104
|
+
m = new Map();
|
|
105
|
+
out.set(key, m);
|
|
106
|
+
}
|
|
107
|
+
for (const [line, hits] of lineHits) m.set(line, Math.max(m.get(line) ?? 0, hits));
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// uncoveredChangedFromLcov(coveredMap, key, changedLines) → the sorted changed lines that the LCOV
|
|
113
|
+
// records as EXECUTABLE-but-unexecuted (a DA entry with 0 hits). A file ABSENT from the map (no SF)
|
|
114
|
+
// is signalled by a null return → the caller records a file-level RED (line:null), exactly like the
|
|
115
|
+
// V8 "absent from coverage" case. A changed line with NO DA entry in a PRESENT file is non-executable
|
|
116
|
+
// (blank/comment) and never flagged — mirroring the V8 blank-line skip.
|
|
117
|
+
export const uncoveredChangedFromLcov = (coveredMap, key, changedLines) => {
|
|
118
|
+
const lineHits = coveredMap.get(key);
|
|
119
|
+
if (!lineHits) return null; // file absent from coverage → file-level RED (caller decides)
|
|
120
|
+
const uncovered = [];
|
|
121
|
+
for (const n of changedLines) {
|
|
122
|
+
const hits = lineHits.get(n);
|
|
123
|
+
if (hits === 0) uncovered.push(n); // executable, 0 hits → uncovered
|
|
124
|
+
// hits > 0 → covered; hits === undefined → non-executable line, skip
|
|
125
|
+
}
|
|
126
|
+
return [...new Set(uncovered)].sort((a, b) => a - b);
|
|
127
|
+
};
|
package/tools/procedures.mjs
CHANGED
|
@@ -212,7 +212,7 @@ const reviewLoopAdvice = (slots, activity) =>
|
|
|
212
212
|
' • At the cap, classify every surviving blocking finding: fixable-bug (fold ONCE as a red→green test, re-review) / inherent-layer-residual (document + raise to an acceptance criterion) / escalate (the maintainer decides); a minor never forces triage.',
|
|
213
213
|
...(activity === 'plan-execution'
|
|
214
214
|
? [
|
|
215
|
-
' • The computed instrument for THIS loop: record each round + triage via review-ledger (record / classify); read the stop with review-ledger --status (its render replaces the hand-composed tally); gate the commit with review-ledger --check.',
|
|
215
|
+
' • The computed instrument for THIS loop: run the FULL gate matrix with run-gates --record BEFORE recording a round (the green-baseline receipt the writer demands), then record each round + triage via review-ledger (record / classify); rounds, caps, and teeth are per SEGMENT (base = HEAD — the counter resets only at a gated commit); read the stop with review-ledger --status (its render replaces the hand-composed tally); gate the commit with review-ledger --check.',
|
|
216
216
|
]
|
|
217
217
|
: []),
|
|
218
218
|
]
|
|
@@ -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
|
-
//
|
|
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
|
|
14
|
-
//
|
|
15
|
-
//
|
|
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
|
|
20
|
-
// exactly what lets the next round proceed),
|
|
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.
|
|
@@ -31,21 +37,37 @@ import { dirname } from 'node:path';
|
|
|
31
37
|
import { pathToFileURL } from 'node:url';
|
|
32
38
|
import { spawnSync } from 'node:child_process';
|
|
33
39
|
import { writeContainedFileAtomic } from './atomic-write.mjs';
|
|
34
|
-
import { computeTreeFingerprint, plansInFlight, readReceipts, resolveReceiptsPath } from './review-state.mjs';
|
|
40
|
+
import { buildState, computeTreeFingerprint, plansInFlight, readReceipts, resolveReceiptsPath } from './review-state.mjs';
|
|
35
41
|
import {
|
|
36
42
|
REVIEW_CAP,
|
|
37
43
|
SCHEMA_VERSION,
|
|
38
44
|
resolveLedgerPath,
|
|
45
|
+
resolveBase,
|
|
39
46
|
readLedger,
|
|
40
|
-
|
|
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
|
|
@@ -79,6 +101,52 @@ const appendRecord = (ledgerPath, record, deps = {}) => {
|
|
|
79
101
|
return { writtenPath: ledgerPath, record };
|
|
80
102
|
};
|
|
81
103
|
|
|
104
|
+
// ── (g) record --from-receipts: draft the backends[] from the current-fingerprint receipts ────────
|
|
105
|
+
// (BUGFREE-3 / AD-049). The orchestrator hand-maintained each backend's { backend, verdict } beside
|
|
106
|
+
// the counts every round — drift-prone busywork. --from-receipts DRAFTS the backends[] instead: for
|
|
107
|
+
// each recipe-named backend it reads the review-state receipt status (verdict from the fresh grounded
|
|
108
|
+
// code receipt) and computes the counts from the orchestrator's OWN supplied findings; `origins` and
|
|
109
|
+
// `findings` stay explicit input. It NEVER invents a backend — a recipe-named backend with no fresh
|
|
110
|
+
// grounded receipt is a LOUD STOP (run its review, or supply it explicitly as a degraded backend).
|
|
111
|
+
// The assembled backends[] then rides the normal recordRound teeth (validateRound re-checks that
|
|
112
|
+
// findings-by-severity equals the drafted counts — true by construction).
|
|
113
|
+
export const draftBackendsFromReceipts = ({ state, findings = [], explicitBackends = [] }, deps = {}) => {
|
|
114
|
+
const stopFn = deps.stop ?? stop;
|
|
115
|
+
const explicitByName = new Map((Array.isArray(explicitBackends) ? explicitBackends : []).filter((b) => b && typeof b.backend === 'string').map((b) => [b.backend, b]));
|
|
116
|
+
const findingsArr = Array.isArray(findings) ? findings : [];
|
|
117
|
+
const countFor = (name) => {
|
|
118
|
+
const own = findingsArr.filter((f) => f && f.backend === name);
|
|
119
|
+
return {
|
|
120
|
+
blockers: own.filter((f) => f.severity === 'blocker').length,
|
|
121
|
+
majors: own.filter((f) => f.severity === 'major').length,
|
|
122
|
+
minors: own.filter((f) => f.severity === 'minor').length,
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
return state.requiredBackends.map((name) => {
|
|
126
|
+
const explicit = explicitByName.get(name);
|
|
127
|
+
if (explicit) {
|
|
128
|
+
// An explicit row is honored verbatim ONLY for a DEGRADED backend (a bridge the operator knows
|
|
129
|
+
// is down, which minted no receipt). A NON-degraded explicit row would BYPASS the
|
|
130
|
+
// receipt-derived verdict --from-receipts exists to compute (a stale hand-composed row silently
|
|
131
|
+
// winning) — a loud STOP, fail-closed (codex council R1). Drop --from-receipts to compose a
|
|
132
|
+
// non-degraded row by hand, or mark the backend degraded.
|
|
133
|
+
if (explicit.degraded !== true) {
|
|
134
|
+
throw stopFn(
|
|
135
|
+
`refusing --from-receipts: an explicit non-degraded backends[] row for ${name} would bypass the receipt-derived verdict this flag computes — mark it degraded (a bridge that is down), or drop --from-receipts and compose the round by hand`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return explicit;
|
|
139
|
+
}
|
|
140
|
+
const status = state.backends.find((b) => b.backend === name);
|
|
141
|
+
if (!status || status.state !== 'current') {
|
|
142
|
+
throw stopFn(
|
|
143
|
+
`refusing --from-receipts: no fresh grounded code receipt for ${name} (state: ${status?.state ?? 'missing'}) — run its review wrapper (codex-review code / agy-review code --facts @f) first, or supply it explicitly as a degraded backend in the payload's backends[]`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return { backend: name, degraded: false, ...countFor(name), verdict: status.verdict };
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
|
|
82
150
|
// ── recordRound (the teeth + the integrity binding) ─────────────────────────────────────────────
|
|
83
151
|
|
|
84
152
|
// recordRound({ cwd, env, loop, activity, round, origins, backends, findings, timestamp }, deps) →
|
|
@@ -90,11 +158,13 @@ export const recordRound = (params, deps = {}) => {
|
|
|
90
158
|
if (ledgerPath == null) throw stop('cannot resolve the ledger path — not a git work tree and AW_REVIEW_LEDGER is unset');
|
|
91
159
|
if (!(Number.isInteger(round) && round >= 1)) throw stop(`round must be an integer >= 1 (got ${round})`);
|
|
92
160
|
// The hard-max ceiling: refuse ANY round beyond it, independent of triage state (Decision 5).
|
|
161
|
+
// Per SEGMENT since AD-048 (D3): the round number is the segment round number.
|
|
93
162
|
if (round > HARD_MAX) {
|
|
94
|
-
throw stop(`refusing to record round ${round}: beyond the hard-max ceiling of ${HARD_MAX} rounds — the
|
|
163
|
+
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
164
|
}
|
|
96
165
|
|
|
97
166
|
const fingerprint = deps.computeFingerprint ? deps.computeFingerprint(cwd) : computeTreeFingerprint(cwd);
|
|
167
|
+
const base = deps.resolveBase ? deps.resolveBase(cwd) : resolveBase(cwd);
|
|
98
168
|
|
|
99
169
|
// The teeth: refuse a new round WHILE decideStop on the existing records is triage-required. Once
|
|
100
170
|
// the surviving blocking finding is classified (recordTriage), decideStop is no longer
|
|
@@ -107,25 +177,79 @@ export const recordRound = (params, deps = {}) => {
|
|
|
107
177
|
// the teeth OPEN (codex R1). Refuse to append until the ledger is sound.
|
|
108
178
|
if (readError) throw stop(`cannot read the existing ledger (${readError}) — refusing to append (fail closed)`);
|
|
109
179
|
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
|
-
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
180
|
+
// EVERY tooth below evaluates the current SEGMENT (activity, loop, base) — D1: records of earlier
|
|
181
|
+
// segments (other bases, or pre-v4 records with no base) are closed history; the field-proven
|
|
182
|
+
// 11-round / 4-base loop records completely while round 4 within ONE segment stays refused.
|
|
183
|
+
const segment = filterSegmentRecords(records, { activity, loop, base });
|
|
184
|
+
// Sequence integrity + sequentiality (codex R2+R3): the EXISTING segment rounds must already be
|
|
185
|
+
// exactly 1..n (never trust a hand-corrupted [2]/[1,1]/[2,1] to compute "latest"), AND the
|
|
186
|
+
// incoming round must be exactly the next (n+1). A duplicate, decreasing, or gapped round would
|
|
187
|
+
// let a fabricated "later" round become the latest that decideStop reads, bypassing the teeth.
|
|
188
|
+
const priorRounds = segment.filter((r) => r.kind === 'round').map((r) => r.round);
|
|
189
|
+
if (!roundSequenceIntact(segment)) {
|
|
190
|
+
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
191
|
}
|
|
119
192
|
const nextRound = priorRounds.length + 1;
|
|
120
193
|
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)`);
|
|
194
|
+
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
195
|
}
|
|
123
|
-
const pre = decideStop(
|
|
196
|
+
const pre = decideStop(segment, { cap: REVIEW_CAP });
|
|
124
197
|
if (pre.state === 'triage-required') {
|
|
125
198
|
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
199
|
}
|
|
127
200
|
|
|
128
|
-
|
|
201
|
+
// D6 — no-repro-no-fold: no blocking finding of the segment's previous round may VANISH
|
|
202
|
+
// unclassified. Present-again is fine (still live); classified is fine (fixable-bug folded with
|
|
203
|
+
// its red→green testId at the round it was folded — late binding restored; inherent-layer-residual
|
|
204
|
+
// documented; escalate handed over; refuted — the honest phantom lane, grounds mandatory). A
|
|
205
|
+
// silent disappearance is exactly the sycophancy hole this pillar closes. Minors stay exempt.
|
|
206
|
+
const previous = segment.find((r) => r.kind === 'round' && r.round === nextRound - 1);
|
|
207
|
+
if (previous) {
|
|
208
|
+
// "Present" means present AS BLOCKING: a blocker/major re-reported as a minor did not survive —
|
|
209
|
+
// it was softened, and the silent-soften lane is exactly the bypass D6 closes (codex R1).
|
|
210
|
+
const incoming = new Set(
|
|
211
|
+
Array.isArray(findings)
|
|
212
|
+
? findings.filter((f) => f && (f.severity === 'blocker' || f.severity === 'major')).map((f) => f.findingKey)
|
|
213
|
+
: [],
|
|
214
|
+
);
|
|
215
|
+
// A classification CLEARS the vanish only when it actually resolves the finding's fate:
|
|
216
|
+
// fixable-bug (folded, testId bound), inherent-layer-residual (documented), refuted (grounds
|
|
217
|
+
// cited), or an ACCEPTED escalate — a pending escalate (accepted:false) is still undecided and
|
|
218
|
+
// must not disappear into a clean round (codex R1).
|
|
219
|
+
const clearsVanish = (c) =>
|
|
220
|
+
c.class === 'fixable-bug' || c.class === 'inherent-layer-residual' || c.class === 'refuted' || (c.class === 'escalate' && c.accepted === true);
|
|
221
|
+
const classified = new Set();
|
|
222
|
+
for (const t of segment) {
|
|
223
|
+
if (t.kind === 'triage' && t.round === previous.round) for (const c of t.classifications) if (clearsVanish(c)) classified.add(c.findingKey);
|
|
224
|
+
}
|
|
225
|
+
const vanished = [...new Set(
|
|
226
|
+
previous.findings
|
|
227
|
+
.filter((f) => f.severity === 'blocker' || f.severity === 'major')
|
|
228
|
+
.map((f) => f.findingKey)
|
|
229
|
+
.filter((k) => !incoming.has(k) && !classified.has(k)),
|
|
230
|
+
)];
|
|
231
|
+
if (vanished.length > 0) {
|
|
232
|
+
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).`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// D4 — the diff-size cap over the ONE shared changed-surface computation (changed-surface.mjs):
|
|
237
|
+
// a round is refused while the changed source surface exceeds the cap, unless the SEGMENT carries
|
|
238
|
+
// a recorded size-cap override sanctioning at least the counted magnitude. Subtractive folds are
|
|
239
|
+
// free (new-side lines only); tests and out-of-domain files never count.
|
|
240
|
+
const cap = parsePositiveIntKnob(env, 'AW_REVIEW_DIFF_CAP', DEFAULT_DIFF_CAP, stop);
|
|
241
|
+
const counted = deps.countChangedLines ? deps.countChangedLines(cwd) : countCapLines(computeChangedSurface(gitRoot(cwd) ?? cwd));
|
|
242
|
+
if (counted > cap) {
|
|
243
|
+
const sanctioned = collectSizeCapLimit(records, { activity, loop, base });
|
|
244
|
+
if (sanctioned === null || counted > sanctioned) {
|
|
245
|
+
throw stop(
|
|
246
|
+
`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}` : ''}. ` +
|
|
247
|
+
`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>"}'`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const record = { schema: SCHEMA_VERSION, loop, activity, kind: 'round', round, base, fingerprint, origins, backends, findings, timestamp: timestamp ?? isoNow() };
|
|
129
253
|
const v = validateRecord(record);
|
|
130
254
|
if (!v.ok) throw stop(`refusing to record a malformed round: ${v.reason}`);
|
|
131
255
|
|
|
@@ -143,6 +267,53 @@ export const recordRound = (params, deps = {}) => {
|
|
|
143
267
|
}
|
|
144
268
|
}
|
|
145
269
|
|
|
270
|
+
// D5 — the green-baseline tooth (armed at Step 2.3, AFTER run-gates --record exists — the
|
|
271
|
+
// bootstrap order): a round records only over a tree whose FULL declared non-process gate set
|
|
272
|
+
// was proven green by a recorded gate-run at the CURRENT fingerprint. "Gates ran before review"
|
|
273
|
+
// is now computed, never remembered. A `--only` subset is recorded honestly but never satisfies
|
|
274
|
+
// this (the R1 converged subset-bypass hole); a run whose tree changed under it attests no
|
|
275
|
+
// particular tree (codex R2); process-gate failures never block (the closed carve-out).
|
|
276
|
+
const gateRuns = segment.filter((r) => r.kind === 'gate-run');
|
|
277
|
+
if (!gateRuns.some((g) => g.fingerprint === fingerprint && isQualityGreenGateRun(g))) {
|
|
278
|
+
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).`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return appendRecord(ledgerPath, record, deps);
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// ── recordGateRun (BUGFREE-2 / AD-048, D5 — the green-baseline receipt run-gates --record mints) ──
|
|
285
|
+
|
|
286
|
+
// recordGateRun({ cwd, env, activity, declared, results, summary, fingerprintBefore,
|
|
287
|
+
// fingerprintAfter, timestamp }, deps) → { writtenPath, record }. The SOLE ledger entry point for
|
|
288
|
+
// run-gates (`--record` DELEGATES here — the runner never opens the ledger file itself; an
|
|
289
|
+
// import/structure pin holds the boundary). The loop is DERIVED from the single in-flight plan
|
|
290
|
+
// (the recordOverride precedent — a gate-run is minted only inside its live loop, never
|
|
291
|
+
// retro-recorded); the record carries the segment frame and NO round number (per-kind frame, D5).
|
|
292
|
+
// A red run records honestly (telemetry fuel — consecutive red gate-runs are the revert-first
|
|
293
|
+
// visibility); quality-green is judged at read time, never stored.
|
|
294
|
+
export const recordGateRun = (params, deps = {}) => {
|
|
295
|
+
const { cwd = process.cwd(), env = process.env, activity = DEFAULT_ACTIVITY, declared, results, summary, fingerprintBefore, fingerprintAfter, timestamp } = params;
|
|
296
|
+
const ledgerPath = deps.ledgerPath ?? resolveLedgerPath(cwd, env);
|
|
297
|
+
if (ledgerPath == null) throw stop('cannot resolve the ledger path — not a git work tree and AW_REVIEW_LEDGER is unset');
|
|
298
|
+
const plans = deps.plansInFlight ? deps.plansInFlight() : plansInFlight(gitRoot(cwd) ?? cwd);
|
|
299
|
+
if (plans.length !== 1) {
|
|
300
|
+
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'})`);
|
|
301
|
+
}
|
|
302
|
+
const loop = plans[0].replace(/\.md$/, '');
|
|
303
|
+
const base = deps.resolveBase ? deps.resolveBase(cwd) : resolveBase(cwd);
|
|
304
|
+
const record = {
|
|
305
|
+
schema: SCHEMA_VERSION, loop, activity, kind: 'gate-run', base,
|
|
306
|
+
fingerprint: fingerprintBefore, fingerprintAfter, declared, results, summary,
|
|
307
|
+
timestamp: timestamp ?? isoNow(),
|
|
308
|
+
};
|
|
309
|
+
const v = validateRecord(record);
|
|
310
|
+
if (!v.ok) throw stop(`refusing to record a malformed gate-run: ${v.reason}`);
|
|
311
|
+
const { records, malformed, malformedReasons, readError } = readLedger(ledgerPath, deps.readFile);
|
|
312
|
+
if (readError) throw stop(`cannot read the existing ledger (${readError}) — refusing to append (fail closed)`);
|
|
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('; ')}`);
|
|
314
|
+
if (!roundSequenceIntact(filterSegmentRecords(records, { activity, loop, base }))) {
|
|
315
|
+
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`);
|
|
316
|
+
}
|
|
146
317
|
return appendRecord(ledgerPath, record, deps);
|
|
147
318
|
};
|
|
148
319
|
|
|
@@ -162,7 +333,7 @@ const gitRoot = (cwd) => {
|
|
|
162
333
|
// inside its live loop, never retro-recorded for a finished or foreign one. The fold-completeness
|
|
163
334
|
// gate matches on loop + payload, never on the (audit-only) fingerprint.
|
|
164
335
|
export const recordOverride = (params, deps = {}) => {
|
|
165
|
-
const { cwd = process.cwd(), env = process.env, loop, activity = DEFAULT_ACTIVITY, round, scope, files, testId, reason, timestamp } = params;
|
|
336
|
+
const { cwd = process.cwd(), env = process.env, loop, activity = DEFAULT_ACTIVITY, round, scope, files, testId, sanctionedLines, reason, timestamp } = params;
|
|
166
337
|
const ledgerPath = deps.ledgerPath ?? resolveLedgerPath(cwd, env);
|
|
167
338
|
if (ledgerPath == null) throw stop('cannot resolve the ledger path — not a git work tree and AW_REVIEW_LEDGER is unset');
|
|
168
339
|
if (!(Number.isInteger(round) && round >= 1)) throw stop(`round must be an integer >= 1 (got ${round})`);
|
|
@@ -174,9 +345,11 @@ export const recordOverride = (params, deps = {}) => {
|
|
|
174
345
|
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
346
|
}
|
|
176
347
|
const fingerprint = deps.computeFingerprint ? deps.computeFingerprint(cwd) : computeTreeFingerprint(cwd);
|
|
348
|
+
const base = deps.resolveBase ? deps.resolveBase(cwd) : resolveBase(cwd);
|
|
177
349
|
const record = {
|
|
178
|
-
schema: SCHEMA_VERSION, loop, activity, kind: 'override', round, fingerprint, scope,
|
|
350
|
+
schema: SCHEMA_VERSION, loop, activity, kind: 'override', round, base, fingerprint, scope,
|
|
179
351
|
...(files !== undefined ? { files } : {}), ...(testId !== undefined ? { testId } : {}),
|
|
352
|
+
...(sanctionedLines !== undefined ? { sanctionedLines } : {}),
|
|
180
353
|
reason, timestamp: timestamp ?? isoNow(),
|
|
181
354
|
};
|
|
182
355
|
const v = validateRecord(record);
|
|
@@ -184,8 +357,8 @@ export const recordOverride = (params, deps = {}) => {
|
|
|
184
357
|
const { records, malformed, malformedReasons, readError } = readLedger(ledgerPath, deps.readFile);
|
|
185
358
|
if (readError) throw stop(`cannot read the existing ledger (${readError}) — refusing to append (fail closed)`);
|
|
186
359
|
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(
|
|
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`);
|
|
360
|
+
if (!roundSequenceIntact(filterSegmentRecords(records, { activity, loop, base }))) {
|
|
361
|
+
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
362
|
}
|
|
190
363
|
return appendRecord(ledgerPath, record, deps);
|
|
191
364
|
};
|
|
@@ -200,27 +373,31 @@ export const recordTriage = (params, deps = {}) => {
|
|
|
200
373
|
if (ledgerPath == null) throw stop('cannot resolve the ledger path — not a git work tree and AW_REVIEW_LEDGER is unset');
|
|
201
374
|
if (!(Number.isInteger(round) && round >= 1)) throw stop(`round must be an integer >= 1 (got ${round})`);
|
|
202
375
|
const fingerprint = deps.computeFingerprint ? deps.computeFingerprint(cwd) : computeTreeFingerprint(cwd);
|
|
376
|
+
const base = deps.resolveBase ? deps.resolveBase(cwd) : resolveBase(cwd);
|
|
203
377
|
// Normalize each classification: an absent testId → null, an absent note → '' — an absent optional
|
|
204
378
|
// field is FILLED, never rejected as malformed (agy R3). Under schema v2 (M2/AD-046) a fixable-bug
|
|
205
379
|
// 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
|
|
207
|
-
//
|
|
380
|
+
// red-test-first fix) — the test-per-fold binding rides the existing validate path; a `refuted`
|
|
381
|
+
// classification normalized to an empty note fails the same way (D6 — the grounds are mandatory).
|
|
382
|
+
// A non-array is left as-is for validateRecord to reject with a typed STOP (never a raw .map TypeError).
|
|
208
383
|
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() };
|
|
384
|
+
const record = { schema: SCHEMA_VERSION, loop, activity, kind: 'triage', round, base, fingerprint, classifications: normalized, timestamp: timestamp ?? isoNow() };
|
|
210
385
|
const v = validateRecord(record);
|
|
211
386
|
if (!v.ok) throw stop(`refusing to record a malformed triage: ${v.reason}`);
|
|
212
387
|
|
|
213
|
-
// Bind the triage to a REAL round: the referenced round must exist
|
|
214
|
-
// must be a surviving blocking finding (blocker or major) of THAT
|
|
215
|
-
// nonexistent/future round, or for a key the round
|
|
216
|
-
// downstream (codex R1
|
|
388
|
+
// Bind the triage to a REAL round of the current SEGMENT: the referenced round must exist there
|
|
389
|
+
// and every classified findingKey must be a surviving blocking finding (blocker or major) of THAT
|
|
390
|
+
// round — a classification for a nonexistent/future/other-segment round, or for a key the round
|
|
391
|
+
// never raised, must not satisfy resolved-residual downstream (codex R1; D1 — a committed
|
|
392
|
+
// segment's rounds are closed, the cross-segment lane is the D7 red-proof override). Fail CLOSED
|
|
393
|
+
// on an unreadable / malformed ledger.
|
|
217
394
|
const { records, malformed, malformedReasons, readError } = readLedger(ledgerPath, deps.readFile);
|
|
218
395
|
if (readError) throw stop(`cannot read the existing ledger (${readError}) — refusing to append (fail closed)`);
|
|
219
396
|
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
|
|
221
|
-
if (!roundSequenceIntact(
|
|
222
|
-
const targetRound =
|
|
223
|
-
if (!targetRound) throw stop(`refusing to classify round ${round} of loop "${loop}": no such recorded round — classify a round that exists`);
|
|
397
|
+
const segment = filterSegmentRecords(records, { activity, loop, base });
|
|
398
|
+
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`);
|
|
399
|
+
const targetRound = segment.find((r) => r.kind === 'round' && r.round === round);
|
|
400
|
+
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
401
|
const survivingKeys = new Set(targetRound.findings.filter((f) => f.severity === 'blocker' || f.severity === 'major').map((f) => f.findingKey));
|
|
225
402
|
for (const c of classifications) {
|
|
226
403
|
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,33 +408,50 @@ export const recordTriage = (params, deps = {}) => {
|
|
|
231
408
|
|
|
232
409
|
// ── CLI (record / classify) ──────────────────────────────────────────────────────────────────────
|
|
233
410
|
|
|
234
|
-
const HELP = `review-ledger-write — the review-round ledger WRITER (agent-workflow family, AD-045 + AD-047).
|
|
411
|
+
const HELP = `review-ledger-write — the review-round ledger WRITER (agent-workflow family, AD-045 + AD-047 + AD-048).
|
|
235
412
|
|
|
236
413
|
Usage:
|
|
237
|
-
node review-ledger-write.mjs record --json '<round-payload>'
|
|
414
|
+
node review-ledger-write.mjs record --json '<round-payload>' [--from-receipts] [--cwd <dir>]
|
|
238
415
|
node review-ledger-write.mjs classify --json '<triage-payload>' [--cwd <dir>]
|
|
239
416
|
node review-ledger-write.mjs override --json '<override-payload>' [--cwd <dir>]
|
|
240
417
|
|
|
418
|
+
Every verb operates on the current SEGMENT — (loop, base = git rev-parse HEAD): round numbers,
|
|
419
|
+
caps, and teeth reset only when a gated commit moves base (schema 4; records carry base).
|
|
420
|
+
|
|
241
421
|
record appends one review round. The JSON payload carries { loop, round, origins, backends,
|
|
242
|
-
findings } (activity defaults to plan-execution; timestamp defaults to now).
|
|
243
|
-
|
|
244
|
-
|
|
422
|
+
findings } (activity defaults to plan-execution; timestamp defaults to now). With
|
|
423
|
+
--from-receipts the backends[] is DRAFTED from the current-fingerprint grounded code
|
|
424
|
+
receipts (verdict per backend) with counts computed from the supplied findings — origins /
|
|
425
|
+
findings stay explicit; a recipe-named backend with no receipt is a LOUD stop (run its
|
|
426
|
+
review, or supply it explicitly as a degraded backend). REFUSES while
|
|
427
|
+
triage is required; beyond the hard-max ceiling of ${HARD_MAX} rounds within one segment;
|
|
428
|
+
while a blocking finding of the segment's previous round VANISHED unclassified (D6 —
|
|
429
|
+
classify it first, "refuted" is the honest phantom lane); while the changed source surface
|
|
430
|
+
exceeds the ${DEFAULT_DIFF_CAP}-line diff cap (AW_REVIEW_DIFF_CAP) without a recorded
|
|
431
|
+
segment size-cap override (D4); while the segment lacks a quality-green gate-run at the
|
|
432
|
+
current fingerprint (D5 — run the FULL matrix first: run-gates.mjs --record; a --only
|
|
433
|
+
subset or a tree-changed run never satisfies; red PROCESS gates never block); or when a
|
|
434
|
+
non-degraded backend lacks a grounded code receipt for the current tree.
|
|
245
435
|
classify appends one triage record. The JSON payload carries { loop, round, classifications } (each
|
|
246
436
|
{ findingKey, class, accepted, testId, note }). A fixable-bug REQUIRES a testId — the
|
|
247
437
|
red→green test that pins the fold, formatted "<test-file>#<test-name-pattern>" (write it
|
|
248
|
-
first);
|
|
249
|
-
|
|
250
|
-
|
|
438
|
+
first); refuted REQUIRES a non-empty note citing the refuting grounds;
|
|
439
|
+
inherent-layer-residual / escalate may omit the testId. This is what permits the next round.
|
|
440
|
+
override appends one override record — the LOUD, durable waiver the gates consume. The JSON payload
|
|
441
|
+
carries { loop, round, scope, reason } plus, per scope:
|
|
251
442
|
scope "oracle-change" → files[] (non-empty repo-relative paths whose tamper flag it lifts);
|
|
252
443
|
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)
|
|
254
|
-
|
|
444
|
+
waives — for a red that is GENUINELY unestablishable pre-fold, D7);
|
|
445
|
+
scope "size-cap" → sanctionedLines (the exact changed-surface magnitude the waiver
|
|
446
|
+
sanctions, SEGMENT-scoped — it dies at the next commit, D4).
|
|
447
|
+
REFUSES for a loop that is not the in-flight plan. QUARANTINE (a flaky/timed-out probe)
|
|
448
|
+
has NO override lane.
|
|
255
449
|
|
|
256
|
-
The read-only checker is a SEPARATE tool: node review-ledger.mjs --check / --status / --json.
|
|
450
|
+
The read-only checker is a SEPARATE tool: node review-ledger.mjs --check / --status / --json / --telemetry.
|
|
257
451
|
Exit codes: 0 written; 1 a typed STOP (teeth / malformed / missing receipt / fs error); 2 usage.`;
|
|
258
452
|
|
|
259
453
|
const parseArgs = (argv) => {
|
|
260
|
-
const opts = { cwd: undefined, json: undefined };
|
|
454
|
+
const opts = { cwd: undefined, json: undefined, fromReceipts: false };
|
|
261
455
|
for (let i = 0; i < argv.length; i += 1) {
|
|
262
456
|
const a = argv[i];
|
|
263
457
|
if (a === '--cwd') {
|
|
@@ -268,6 +462,8 @@ const parseArgs = (argv) => {
|
|
|
268
462
|
opts.json = argv[i + 1];
|
|
269
463
|
if (opts.json === undefined) throw usageFail('--json needs a JSON payload');
|
|
270
464
|
i += 1;
|
|
465
|
+
} else if (a === '--from-receipts') {
|
|
466
|
+
opts.fromReceipts = true;
|
|
271
467
|
} else {
|
|
272
468
|
throw usageFail(`unknown argument: ${a}`);
|
|
273
469
|
}
|
|
@@ -292,8 +488,15 @@ export const main = (argv, ctx = {}) => {
|
|
|
292
488
|
const sub = argv[0];
|
|
293
489
|
if (sub !== 'record' && sub !== 'classify' && sub !== 'override') throw usageFail(`unknown subcommand "${sub}" (expected: record | classify | override)`);
|
|
294
490
|
const opts = parseArgs(argv.slice(1));
|
|
491
|
+
if (opts.fromReceipts && sub !== 'record') throw usageFail('--from-receipts applies only to `record`');
|
|
295
492
|
const payload = parsePayload(opts.json);
|
|
296
493
|
const cwd = opts.cwd ?? cwd0;
|
|
494
|
+
if (opts.fromReceipts) {
|
|
495
|
+
// Draft backends[] from the current-fingerprint receipts + the supplied findings; origins /
|
|
496
|
+
// findings stay explicit. The drafted array then rides the normal recordRound teeth.
|
|
497
|
+
const state = buildState({ cwd, env });
|
|
498
|
+
payload.backends = draftBackendsFromReceipts({ state, findings: payload.findings, explicitBackends: payload.backends ?? [] });
|
|
499
|
+
}
|
|
297
500
|
const result =
|
|
298
501
|
sub === 'record'
|
|
299
502
|
? recordRound({ cwd, env, ...payload })
|