@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/review-ledger.mjs
CHANGED
|
@@ -11,22 +11,27 @@
|
|
|
11
11
|
//
|
|
12
12
|
// Normative `--check` exit contract (the single home of this list — SKILL.md / the mode file point
|
|
13
13
|
// here). The gate enforces the plan-EXECUTION (code) loop — the loop that produces the committable
|
|
14
|
-
// artifact — filtered to activity==="plan-execution" AND the in-flight plan's
|
|
14
|
+
// artifact — filtered to the current SEGMENT: activity==="plan-execution" AND the in-flight plan's
|
|
15
|
+
// filename stem AND base===`git rev-parse HEAD` (BUGFREE-2 / AD-048, D1 — a segment is the
|
|
16
|
+
// uncommitted change set over one base commit; it closes only by a gated commit, so a round-counter
|
|
17
|
+
// reset is earned, never declared):
|
|
15
18
|
// exit 0 when the resolved plan-execution.review recipe is solo (configured, or degraded there —
|
|
16
19
|
// no reviewer ready); when no plan is in flight (the review-state naming convention);
|
|
17
20
|
// when the tree is clean (nothing to review); when the cwd is not a git work tree; and
|
|
18
|
-
// when the in-flight plan-execution
|
|
21
|
+
// when the in-flight plan-execution SEGMENT is `converged` or `resolved-residual` (its
|
|
19
22
|
// latest round's non-degraded backends carry grounded code receipts for the recorded
|
|
20
23
|
// fingerprint, and a recorded 0/0 is ship-class-consistent with those receipts).
|
|
21
|
-
// exit 1 for any DIRTY in-flight plan-execution
|
|
22
|
-
// `resolved-residual` — `triage-required`, `continue`, OR no round/receipt recorded
|
|
23
|
-
//
|
|
24
|
+
// exit 1 for any DIRTY in-flight plan-execution segment that is neither `converged` nor
|
|
25
|
+
// `resolved-residual` — `triage-required`, `continue`, OR no round/receipt recorded in
|
|
26
|
+
// the CURRENT segment (a dirty active plan with an empty/stale/other-segment ledger is a
|
|
27
|
+
// FAILURE, not a fail-open pass; when the loop holds only pre-v4 records the reason names
|
|
28
|
+
// the schema upgrade — old records never enter a segment, D7);
|
|
24
29
|
// when MORE THAN ONE plan is in flight (ambiguous loop id); when a recorded ship-class 0/0
|
|
25
30
|
// coexists with a non-ship receipt verdict, or a non-degraded recorded backend lacks a
|
|
26
31
|
// grounded receipt for its fingerprint. Fail-CLOSED (unknown state, never a fail-open pass)
|
|
27
32
|
// on a detector failure, an unreadable (non-ENOENT) ledger, malformed ledger lines the
|
|
28
|
-
// reader dropped, or a corrupt round sequence (not 1..n) — the only
|
|
29
|
-
// green is an EXPLICIT configured solo.
|
|
33
|
+
// reader dropped, or a corrupt segment round sequence (not 1..n) — the only
|
|
34
|
+
// detector-independent green is an EXPLICIT configured solo.
|
|
30
35
|
//
|
|
31
36
|
// The stop decision: `decideStop(records, { cap, currentFingerprint, requiredBackends })` reads the
|
|
32
37
|
// ordered ledger records of ONE loop (both `round` and `triage` kinds) and returns exactly one state
|
|
@@ -62,6 +67,10 @@ import {
|
|
|
62
67
|
readReceipts,
|
|
63
68
|
resolveReceiptsPath,
|
|
64
69
|
} from './review-state.mjs';
|
|
70
|
+
// The NEUTRAL shared core (D4/D8): the D8 telemetry reads the FOLD ledger through the neutral
|
|
71
|
+
// module — never through fold-completeness.mjs, which imports THIS module (the import-cycle
|
|
72
|
+
// invariant, codex R2; an import-split test pins it). probeVerdict is the one D4 algebra home.
|
|
73
|
+
import { probeVerdict, resolveResultsPath, readJsonlRows } from './changed-surface.mjs';
|
|
65
74
|
|
|
66
75
|
export const LEDGER_BASENAME = 'agent-workflow-review-ledger.jsonl';
|
|
67
76
|
const ACTIVITY = 'plan-execution';
|
|
@@ -72,14 +81,21 @@ const SLOT = 'review';
|
|
|
72
81
|
export const REVIEW_CAP = 2;
|
|
73
82
|
// SCHEMA_VERSION is what the WRITER emits (M2/AD-046: a fixable-bug triage requires a non-null,
|
|
74
83
|
// well-formed testId — the red→green test that pins the fold; BUGFREE-1/AD-047: v3 adds the
|
|
75
|
-
// `override` record kind — the loud, durable waiver the fold-completeness gate consumes
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
// `override` record kind — the loud, durable waiver the fold-completeness gate consumes;
|
|
85
|
+
// BUGFREE-2/AD-048: v4 adds the SEGMENT — every record carries `base` = the commit the dirty tree
|
|
86
|
+
// sits on, and round numbering / caps / teeth / decideCheck all operate per (activity, loop, base) —
|
|
87
|
+
// plus the kind `gate-run` (the D5 green-baseline receipt run-gates --record mints), the override
|
|
88
|
+
// scope `size-cap` (the D4 diff-cap waiver), and the triage class `refuted` (the D6 honest lane for
|
|
89
|
+
// a phantom finding). The READER tolerates every SUPPORTED_SCHEMAS version under its own
|
|
90
|
+
// per-version rules, so historical/live v1..v3 ledgers never retroactively become malformed
|
|
91
|
+
// (Decision 2 — a malformed line cascades fail-closed refusals in the writer teeth AND the --check
|
|
92
|
+
// gate). v1 records keep the AD-045 rule (testId optional, unenforced); v2+ enforces the
|
|
93
|
+
// test-per-fold binding; ONLY v3+ may carry kind `override`; ONLY v4 may carry `base` / kind
|
|
94
|
+
// `gate-run` / scope `size-cap` / class `refuted` (older records never grow new surface).
|
|
95
|
+
// decideStop never reads testId, overrides, gate-runs, or base (not decideStop inputs — the caller
|
|
96
|
+
// passes ONE segment's records, exactly as it passes one loop's; D10: the truth table is untouched).
|
|
97
|
+
export const SCHEMA_VERSION = 4;
|
|
98
|
+
const SUPPORTED_SCHEMAS = new Set([1, 2, 3, 4]);
|
|
83
99
|
|
|
84
100
|
// The record vocabulary — the single home of every enum the schema validates.
|
|
85
101
|
const ACTIVITIES_SET = new Set(['plan-authoring', 'plan-execution']);
|
|
@@ -87,7 +103,16 @@ const KINDS_SET = new Set(['round', 'triage']);
|
|
|
87
103
|
const SEVERITIES = new Set(['blocker', 'major', 'minor']);
|
|
88
104
|
export const ORIGINS = ['first-draft', 'fold-induced', 'mechanics'];
|
|
89
105
|
const CLASSES = new Set(['fixable-bug', 'inherent-layer-residual', 'escalate']);
|
|
106
|
+
// v4 (BUGFREE-2 / D6): `refuted` — the honest lane for a phantom finding, refuted against code with
|
|
107
|
+
// a MANDATORY non-empty note citing the grounds; never silently dropped, never folded. Exported as
|
|
108
|
+
// the code-side vocabulary source the doc-parity lint (BUGFREE-3 / AD-049) checks the contract docs
|
|
109
|
+
// against — so the mode files can never drift from the schema's own class/scope lexicon.
|
|
110
|
+
export const V4_CLASSES = new Set([...CLASSES, 'refuted']);
|
|
90
111
|
const OVERRIDE_SCOPES = new Set(['oracle-change', 'red-proof']);
|
|
112
|
+
// v4 (BUGFREE-2 / D4): `size-cap` — the recorded waiver for a changed surface beyond the diff cap;
|
|
113
|
+
// exact payload carries the sanctioned magnitude, and it is SEGMENT-scoped (loop + base), unlike
|
|
114
|
+
// the two loop-scoped v3 scopes.
|
|
115
|
+
export const V4_OVERRIDE_SCOPES = new Set([...OVERRIDE_SCOPES, 'size-cap']);
|
|
91
116
|
|
|
92
117
|
// ── git-dir resolution (read-only queries; the ledger lives in the git dir, uncommittable) ──────
|
|
93
118
|
|
|
@@ -107,6 +132,17 @@ export const resolveLedgerPath = (cwd, env = process.env) => {
|
|
|
107
132
|
return gitDir == null ? null : join(gitDir, LEDGER_BASENAME);
|
|
108
133
|
};
|
|
109
134
|
|
|
135
|
+
// ── the segment (BUGFREE-2 / AD-048, D1) ─────────────────────────────────────────────────────────
|
|
136
|
+
// A SEGMENT = (activity, loop, base) where base = the commit the dirty tree sits on. Derived, never
|
|
137
|
+
// declared: `git rev-parse HEAD` is computed identically at write time and check time, matches the
|
|
138
|
+
// review's actual domain (the working-tree diff vs HEAD), and its reset is commit-gated — so
|
|
139
|
+
// resetting the round counter REQUIRES shipping a green, converged unit. An amend/rebase mid-loop
|
|
140
|
+
// orphans the segment's rounds — correct: the reviewed tree no longer exists.
|
|
141
|
+
|
|
142
|
+
// resolveBase(cwd) → the current HEAD commit sha, or null on an unborn branch / outside a git work
|
|
143
|
+
// tree (a caught refusal from git, never a crash — agy R1).
|
|
144
|
+
export const resolveBase = (cwd) => gitLine(['rev-parse', '--verify', '--quiet', 'HEAD'], cwd);
|
|
145
|
+
|
|
110
146
|
// ── ship-verdict mapping (the single home; a named test pins it) ────────────────────────────────
|
|
111
147
|
|
|
112
148
|
// isShipVerdict(verdict) — which free-text review verdicts are ship-class. SHIP / SHIP WITH NITS are
|
|
@@ -188,13 +224,18 @@ const validateRound = (obj) => {
|
|
|
188
224
|
return { ok: true };
|
|
189
225
|
};
|
|
190
226
|
|
|
191
|
-
// validateTriage(obj, schema) → { ok, reason }. `schema` selects the per-version
|
|
192
|
-
//
|
|
227
|
+
// validateTriage(obj, schema) → { ok, reason }. `schema` selects the per-version rules (v1 tolerant
|
|
228
|
+
// testId / v2 the test-per-fold binding / v4 the `refuted` class) — the shared structural checks
|
|
229
|
+
// run in every version.
|
|
193
230
|
const validateTriage = (obj, schema = SCHEMA_VERSION) => {
|
|
194
231
|
if (!Array.isArray(obj.classifications) || obj.classifications.length === 0) return { ok: false, reason: 'triage: classifications must be a non-empty array' };
|
|
232
|
+
const classes = schema >= 4 ? V4_CLASSES : CLASSES;
|
|
195
233
|
for (const c of obj.classifications) {
|
|
196
234
|
if (!isPlainObject(c) || !isNonEmptyString(c.findingKey)) return { ok: false, reason: 'triage: each classification needs a findingKey' };
|
|
197
|
-
if (!
|
|
235
|
+
if (!classes.has(c.class)) return { ok: false, reason: `triage: classification ${c.findingKey} bad class "${c.class}"` };
|
|
236
|
+
// D6 — `refuted` is the honest phantom-finding lane: the grounds are MANDATORY (a non-empty
|
|
237
|
+
// note citing what refutes it against code), never a silent drop.
|
|
238
|
+
if (c.class === 'refuted' && !isNonEmptyString(c.note)) return { ok: false, reason: `triage: classification ${c.findingKey} is refuted but carries no note — cite the grounds that refute it against code (mandatory)` };
|
|
198
239
|
if (typeof c.accepted !== 'boolean') return { ok: false, reason: `triage: classification ${c.findingKey} missing boolean accepted` };
|
|
199
240
|
// Structural (BOTH versions): testId is null/absent or a non-empty string — an ABSENT key is
|
|
200
241
|
// treated as null, never rejected here (agy R3). The writer normalizes it to null when stored.
|
|
@@ -212,16 +253,20 @@ const validateTriage = (obj, schema = SCHEMA_VERSION) => {
|
|
|
212
253
|
return { ok: true };
|
|
213
254
|
};
|
|
214
255
|
|
|
215
|
-
// validateOverride(obj) → { ok, reason }. v3
|
|
256
|
+
// validateOverride(obj, schema) → { ok, reason }. v3+ (BUGFREE-1 / AD-047, D3): scope `oracle-change`
|
|
216
257
|
// carries non-empty repo-relative files[] + reason; scope `red-proof` carries a REQUIRED
|
|
217
|
-
// well-formed testId + reason, no files[].
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
258
|
+
// well-formed testId + reason, no files[]. v4 (BUGFREE-2 / D4) adds scope `size-cap`: a REQUIRED
|
|
259
|
+
// positive-integer sanctionedLines — the exact magnitude the waiver sanctions, segment-scoped.
|
|
260
|
+
// Payloads are EXACT — a stray cross-scope field is a forgery smell, rejected. The fingerprint is
|
|
261
|
+
// recorded for audit only.
|
|
262
|
+
const OVERRIDE_SHARED_KEYS = new Set(['schema', 'loop', 'activity', 'kind', 'round', 'base', 'fingerprint', 'timestamp', 'scope', 'reason']);
|
|
263
|
+
const OVERRIDE_PAYLOAD_KEY = { 'oracle-change': 'files', 'red-proof': 'testId', 'size-cap': 'sanctionedLines' };
|
|
264
|
+
const validateOverride = (obj, schema = SCHEMA_VERSION) => {
|
|
265
|
+
const scopes = schema >= 4 ? V4_OVERRIDE_SCOPES : OVERRIDE_SCOPES;
|
|
266
|
+
if (!scopes.has(obj.scope)) return { ok: false, reason: `override: bad scope "${obj.scope}" (expected ${[...scopes].join(' | ')})` };
|
|
222
267
|
// EXACT per-scope payloads via an allow-list (codex R5): a stray key — a cross-scope field or an
|
|
223
268
|
// arbitrary hand-added one — is a forgery smell, rejected by name (the mutation-shape precedent).
|
|
224
|
-
const payloadKey = obj.scope
|
|
269
|
+
const payloadKey = OVERRIDE_PAYLOAD_KEY[obj.scope];
|
|
225
270
|
for (const k of Object.keys(obj)) {
|
|
226
271
|
if (!OVERRIDE_SHARED_KEYS.has(k) && k !== payloadKey) return { ok: false, reason: `override: unknown key "${k}" (exact per-scope payloads: shared frame + ${payloadKey})` };
|
|
227
272
|
}
|
|
@@ -233,25 +278,120 @@ const validateOverride = (obj) => {
|
|
|
233
278
|
}
|
|
234
279
|
return { ok: true };
|
|
235
280
|
}
|
|
281
|
+
if (obj.scope === 'size-cap') {
|
|
282
|
+
if (!(Number.isInteger(obj.sanctionedLines) && obj.sanctionedLines >= 1)) return { ok: false, reason: 'override: a size-cap override requires sanctionedLines — the exact positive-integer magnitude it sanctions' };
|
|
283
|
+
return { ok: true };
|
|
284
|
+
}
|
|
236
285
|
if (!isWellFormedTestId(obj.testId)) return { ok: false, reason: 'override: a red-proof override requires a well-formed testId "<test-file>#<test-name-pattern>"' };
|
|
237
286
|
return { ok: true };
|
|
238
287
|
};
|
|
239
288
|
|
|
240
|
-
//
|
|
289
|
+
// ── the gate-run record (BUGFREE-2 / D5): the green-baseline receipt run-gates --record mints ────
|
|
290
|
+
|
|
291
|
+
// Per-kind frame rule: a gate-run carries the SEGMENT frame (loop, base, fingerprint before/after,
|
|
292
|
+
// timestamp) and NO round number. The body is machine-composed by recordGateRun, so the key set is
|
|
293
|
+
// EXACT (the override allow-list precedent): declared[] = the FULL declaration at run time (id +
|
|
294
|
+
// cmd — the cmd is what the process-gate classification reads); results[] = what actually ran (a
|
|
295
|
+
// --only subset records exactly that subset, honestly); summary mirrors the runner's machine
|
|
296
|
+
// summary line, consistency-checked against results[] so a forged verdict cannot ride beside
|
|
297
|
+
// honest-looking evidence.
|
|
298
|
+
const GATE_RUN_KEYS = new Set(['schema', 'loop', 'activity', 'kind', 'base', 'fingerprint', 'fingerprintAfter', 'declared', 'results', 'summary', 'timestamp']);
|
|
299
|
+
const SUMMARY_KEYS = ['failed', 'failedIds', 'gates', 'passed', 'status'];
|
|
300
|
+
// Gate ids are kebab-case — the same closed shape run-gates.mjs enforces on the declaration; it
|
|
301
|
+
// also kills the comma-aliasing class in the failedIds compare (internal sweep).
|
|
302
|
+
const GATE_RUN_ID_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
303
|
+
const validateGateRun = (obj) => {
|
|
304
|
+
for (const k of Object.keys(obj)) {
|
|
305
|
+
if (!GATE_RUN_KEYS.has(k)) return { ok: false, reason: `gate-run: unknown key "${k}" (exact machine-composed payload)` };
|
|
306
|
+
}
|
|
307
|
+
if (!(obj.fingerprintAfter === null || isNonEmptyString(obj.fingerprintAfter))) return { ok: false, reason: 'gate-run: fingerprintAfter must be null or a non-empty string (the post-run tree)' };
|
|
308
|
+
if (!Array.isArray(obj.declared) || obj.declared.length === 0) return { ok: false, reason: 'gate-run: declared must be a non-empty array of { id, cmd }' };
|
|
309
|
+
const declaredIds = new Set();
|
|
310
|
+
for (const d of obj.declared) {
|
|
311
|
+
if (!isPlainObject(d) || !isNonEmptyString(d.id) || !isNonEmptyString(d.cmd) || Object.keys(d).length !== 2) return { ok: false, reason: 'gate-run: each declared entry must be exactly { id, cmd } (non-empty strings)' };
|
|
312
|
+
if (!GATE_RUN_ID_RE.test(d.id)) return { ok: false, reason: `gate-run: id "${d.id}" must be kebab-case (the run-gates declaration shape)` };
|
|
313
|
+
if (declaredIds.has(d.id)) return { ok: false, reason: `gate-run: duplicate declared id "${d.id}"` };
|
|
314
|
+
declaredIds.add(d.id);
|
|
315
|
+
}
|
|
316
|
+
if (!Array.isArray(obj.results) || obj.results.length === 0) return { ok: false, reason: 'gate-run: results must be a non-empty array of { id, ok, code }' };
|
|
317
|
+
const resultIds = new Set();
|
|
318
|
+
for (const r of obj.results) {
|
|
319
|
+
if (!isPlainObject(r) || !isNonEmptyString(r.id) || typeof r.ok !== 'boolean' || !Number.isInteger(r.code) || Object.keys(r).length !== 3) return { ok: false, reason: 'gate-run: each result must be exactly { id, ok, code } (string, boolean, integer)' };
|
|
320
|
+
if (!declaredIds.has(r.id)) return { ok: false, reason: `gate-run: result id "${r.id}" is not in declared[]` };
|
|
321
|
+
if (resultIds.has(r.id)) return { ok: false, reason: `gate-run: duplicate result id "${r.id}"` };
|
|
322
|
+
resultIds.add(r.id);
|
|
323
|
+
}
|
|
324
|
+
const s = obj.summary;
|
|
325
|
+
if (!isPlainObject(s)) return { ok: false, reason: 'gate-run: summary must be an object' };
|
|
326
|
+
if (Object.keys(s).sort().join(',') !== SUMMARY_KEYS.join(',')) return { ok: false, reason: `gate-run: summary must carry exactly { ${SUMMARY_KEYS.join(', ')} }` };
|
|
327
|
+
const failing = obj.results.filter((r) => !r.ok);
|
|
328
|
+
// The status IS the verdict word: a valid gate-run always carries results, so the runner's
|
|
329
|
+
// vocabulary collapses to ok|fail here — tie it to the failing count (a forged "ok" beside red
|
|
330
|
+
// results must not validate; internal sweep).
|
|
331
|
+
const expectedStatus = failing.length === 0 ? 'ok' : 'fail';
|
|
332
|
+
if (s.status !== expectedStatus) return { ok: false, reason: `gate-run: summary.status must be "${expectedStatus}" for ${failing.length} failing result(s) (got ${JSON.stringify(s.status)})` };
|
|
333
|
+
if (s.gates !== obj.results.length || s.passed !== obj.results.length - failing.length || s.failed !== failing.length) {
|
|
334
|
+
return { ok: false, reason: `gate-run: summary counts do not match results (${s.gates}/${s.passed}/${s.failed} vs ${obj.results.length} results, ${failing.length} failing)` };
|
|
335
|
+
}
|
|
336
|
+
if (!Array.isArray(s.failedIds) || s.failedIds.length !== failing.length || !s.failedIds.every((id, i) => id === failing[i].id)) return { ok: false, reason: 'gate-run: summary.failedIds must equal the failing result ids, in results order' };
|
|
337
|
+
return { ok: true };
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// isProcessGateCmd(cmd) — the CLOSED, kit-owned process-gate classification (D5): the kit's own
|
|
341
|
+
// process-loop `--check` commands (review-state / review-ledger / fold-completeness) legitimately
|
|
342
|
+
// fail MID-loop, so "quality-green" excludes them — without the carve-out the D5 tooth is
|
|
343
|
+
// unsatisfiable by construction. Closed by the gate's cmd being EXACTLY one `--check` invocation of
|
|
344
|
+
// one of the three checkers (an optionally-quoted path token whose basename is the tool — never a
|
|
345
|
+
// substring: fold-completeness-run.mjs must NOT match). A COMPOUND line (`… && checker --check`)
|
|
346
|
+
// is never process: exempting it would forgive the failing quality half — fail-open against the
|
|
347
|
+
// D5 direction (internal sweep, live-probed). Pinned by its own named test.
|
|
348
|
+
// The tool BASENAME must sit at a path boundary (start-of-token or after a separator): a
|
|
349
|
+
// suffix-named sibling like `my-review-ledger.mjs` is a consumer's own tool, and misclassifying it
|
|
350
|
+
// as process would forgive its red result (codex R2, fold-induced by the R1 exact-form rewrite).
|
|
351
|
+
const PROCESS_GATE_RE = /^node\s+("(?:[^"]*[/\\])?(?:review-state|review-ledger|fold-completeness)\.mjs"|(?:[^\s"]*[/\\])?(?:review-state|review-ledger|fold-completeness)\.mjs)\s+--check$/;
|
|
352
|
+
export const isProcessGateCmd = (cmd) => PROCESS_GATE_RE.test(String(cmd).trim());
|
|
353
|
+
|
|
354
|
+
// isQualityGreenGateRun(record) → boolean (D5). Quality-green = the run covers EVERY declared
|
|
355
|
+
// NON-process gate with a green result (a `--only` subset is recorded honestly but never satisfies
|
|
356
|
+
// this — the R1 converged subset-bypass hole), AND the tree did not change under the run
|
|
357
|
+
// (fingerprint === fingerprintAfter, both non-null — a mutating gate attests no particular tree,
|
|
358
|
+
// codex R2). Process-gate failures never block.
|
|
359
|
+
export const isQualityGreenGateRun = (record) => {
|
|
360
|
+
if (record.kind !== 'gate-run') return false;
|
|
361
|
+
if (record.fingerprint == null || record.fingerprint !== record.fingerprintAfter) return false;
|
|
362
|
+
const green = new Set(record.results.filter((r) => r.ok).map((r) => r.id));
|
|
363
|
+
return record.declared.every((d) => isProcessGateCmd(d.cmd) || green.has(d.id));
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// validateRecord(obj) → { ok, reason }. The shared frame (schema/loop/activity/kind/round/base/
|
|
241
367
|
// fingerprint/timestamp) then the per-kind body. `reason` names the exact failed check so the
|
|
242
368
|
// malformed-line surface and the per-check named tests can assert it. Kind vocabulary is
|
|
243
|
-
// per-version: `override` exists only under schema >= 3
|
|
369
|
+
// per-version: `override` exists only under schema >= 3, `gate-run` only under schema >= 4, and
|
|
370
|
+
// `base` is a v4-only frame field (older records never grow new kinds OR new fields — D2).
|
|
244
371
|
export const validateRecord = (obj) => {
|
|
245
372
|
if (!isPlainObject(obj)) return { ok: false, reason: 'not an object' };
|
|
246
373
|
if (!SUPPORTED_SCHEMAS.has(obj.schema)) return { ok: false, reason: `schema must be one of ${[...SUPPORTED_SCHEMAS].join(', ')}` };
|
|
247
374
|
if (!isNonEmptyString(obj.loop)) return { ok: false, reason: 'missing loop' };
|
|
248
375
|
if (!ACTIVITIES_SET.has(obj.activity)) return { ok: false, reason: `bad activity "${obj.activity}"` };
|
|
249
|
-
if (!KINDS_SET.has(obj.kind) && !(obj.schema >= 3 && obj.kind === 'override')) return { ok: false, reason: `bad kind "${obj.kind}"` };
|
|
250
|
-
|
|
376
|
+
if (!KINDS_SET.has(obj.kind) && !(obj.schema >= 3 && obj.kind === 'override') && !(obj.schema >= 4 && obj.kind === 'gate-run')) return { ok: false, reason: `bad kind "${obj.kind}"` };
|
|
377
|
+
// The v4 SEGMENT frame (D1/D2): a v4 record REQUIRES base (null on an unborn branch, else the
|
|
378
|
+
// HEAD sha); a v1..v3 record must NOT carry it — an old record never grows new surface.
|
|
379
|
+
if (obj.schema >= 4) {
|
|
380
|
+
if (!(obj.base === null || isNonEmptyString(obj.base))) return { ok: false, reason: 'a v4 record requires base — null (unborn branch) or the HEAD commit the dirty tree sits on' };
|
|
381
|
+
} else if (obj.base !== undefined) {
|
|
382
|
+
return { ok: false, reason: `base is a v4 frame field — a schema-${obj.schema} record never carries it` };
|
|
383
|
+
}
|
|
384
|
+
// Per-kind frame (D5): a gate-run carries NO round number; every other kind requires one.
|
|
385
|
+
if (obj.kind === 'gate-run') {
|
|
386
|
+
if (obj.round !== undefined) return { ok: false, reason: 'gate-run: a gate-run carries no round number (it is segment-framed, not round-framed)' };
|
|
387
|
+
} else if (!(Number.isInteger(obj.round) && obj.round >= 1)) {
|
|
388
|
+
return { ok: false, reason: 'round must be an integer >= 1' };
|
|
389
|
+
}
|
|
251
390
|
if (!(obj.fingerprint === null || isNonEmptyString(obj.fingerprint))) return { ok: false, reason: 'fingerprint must be null or a non-empty string' };
|
|
252
391
|
if (!isNonEmptyString(obj.timestamp)) return { ok: false, reason: 'missing timestamp' };
|
|
253
392
|
if (obj.kind === 'round') return validateRound(obj);
|
|
254
|
-
if (obj.kind === 'override') return validateOverride(obj);
|
|
393
|
+
if (obj.kind === 'override') return validateOverride(obj, obj.schema);
|
|
394
|
+
if (obj.kind === 'gate-run') return validateGateRun(obj);
|
|
255
395
|
return validateTriage(obj, obj.schema);
|
|
256
396
|
};
|
|
257
397
|
|
|
@@ -292,21 +432,43 @@ export const readLedger = (path, readFile = readFileSync) => {
|
|
|
292
432
|
export const filterLoopRecords = (records, { activity, loop }) =>
|
|
293
433
|
records.filter((r) => r.activity === activity && r.loop === loop);
|
|
294
434
|
|
|
435
|
+
// filterSegmentRecords(records, { activity, loop, base }) → the records of ONE segment (D1), order
|
|
436
|
+
// preserved. STRICT: only a v4+ record can be a segment member (a v1..v3 record carries no base and
|
|
437
|
+
// never enters one — the D7 legacy rule; the schema guard also keeps a defensive undefined base
|
|
438
|
+
// from matching a pre-v4 record's absent field, codex R1); records at baseA are invisible at baseB;
|
|
439
|
+
// base === null matches only null (an unborn-branch segment).
|
|
440
|
+
export const filterSegmentRecords = (records, { activity, loop, base }) =>
|
|
441
|
+
filterLoopRecords(records, { activity, loop }).filter((r) => r.schema >= 4 && r.base === base);
|
|
442
|
+
|
|
295
443
|
// collectOverrides(records, { activity, loop }) → { oracleChangeFiles: Set, redProofTestIds: Set }.
|
|
296
|
-
// The UNION of the loop's recorded override payloads — loop + payload scoped, never
|
|
297
|
-
// bound (D3: re-affirmation churn on every later edit would train rubber-stamping). The
|
|
444
|
+
// The UNION of the loop's recorded v3-scope override payloads — loop + payload scoped, never
|
|
445
|
+
// fingerprint-bound (D3: re-affirmation churn on every later edit would train rubber-stamping). The
|
|
298
446
|
// fold-completeness checker consumes THIS (both modules are read-only — the read/write split holds).
|
|
447
|
+
// The v4 `size-cap` scope is deliberately NOT here: it is SEGMENT-scoped and consumed by the writer
|
|
448
|
+
// tooth via collectSizeCapLimit.
|
|
299
449
|
export const collectOverrides = (records, { activity = ACTIVITY, loop } = {}) => {
|
|
300
450
|
const oracleChangeFiles = new Set();
|
|
301
451
|
const redProofTestIds = new Set();
|
|
302
452
|
for (const r of filterLoopRecords(records, { activity, loop })) {
|
|
303
453
|
if (r.kind !== 'override') continue;
|
|
304
454
|
if (r.scope === 'oracle-change') for (const f of r.files) oracleChangeFiles.add(f);
|
|
305
|
-
else redProofTestIds.add(r.testId);
|
|
455
|
+
else if (r.scope === 'red-proof') redProofTestIds.add(r.testId);
|
|
306
456
|
}
|
|
307
457
|
return { oracleChangeFiles, redProofTestIds };
|
|
308
458
|
};
|
|
309
459
|
|
|
460
|
+
// collectSizeCapLimit(records, { activity, loop, base }) → the LARGEST sanctionedLines among the
|
|
461
|
+
// segment's recorded size-cap overrides, or null when none exists (D4). Segment-scoped by
|
|
462
|
+
// construction: a magnitude sanctioned for one base never leaks into the next segment.
|
|
463
|
+
export const collectSizeCapLimit = (records, { activity = ACTIVITY, loop, base } = {}) => {
|
|
464
|
+
let limit = null;
|
|
465
|
+
for (const r of filterSegmentRecords(records, { activity, loop, base })) {
|
|
466
|
+
if (r.kind !== 'override' || r.scope !== 'size-cap') continue;
|
|
467
|
+
if (limit === null || r.sanctionedLines > limit) limit = r.sanctionedLines;
|
|
468
|
+
}
|
|
469
|
+
return limit;
|
|
470
|
+
};
|
|
471
|
+
|
|
310
472
|
// roundSequenceIntact(records) → true iff the round records, in file order, number exactly 1,2,…,n
|
|
311
473
|
// (no duplicate, gap, or out-of-order round). Checks the EXISTING sequence, not just the incoming
|
|
312
474
|
// round: a ledger like [2] / [1,1] / [2,1] (reachable only by hand-editing the git-dir file — the
|
|
@@ -338,7 +500,12 @@ export const decideStop = (records, { cap = REVIEW_CAP, currentFingerprint = nul
|
|
|
338
500
|
const classOf = new Map();
|
|
339
501
|
for (const t of triages) if (t.round === latest.round) for (const c of t.classifications) classOf.set(c.findingKey, { ...c, triageFingerprint: t.fingerprint });
|
|
340
502
|
const isClassified = (key) => classOf.has(key);
|
|
341
|
-
|
|
503
|
+
// `refuted` (v4, AD-048) resolves like an inherent-layer-residual: a documented, grounds-cited
|
|
504
|
+
// resolution of a phantom finding. Without this arm a phantom minted at round HARD_MAX and
|
|
505
|
+
// honestly refuted would WEDGE the segment (the immutable round record keeps the minting
|
|
506
|
+
// backend's counts non-0/0, and no further round exists to vanish it into). Additive beside the
|
|
507
|
+
// frozen truth table — every pre-existing row is untouched (D10).
|
|
508
|
+
const isResolvedClass = (c) => c && (c.class === 'inherent-layer-residual' || c.class === 'refuted' || (c.class === 'escalate' && c.accepted === true));
|
|
342
509
|
|
|
343
510
|
const survivingBlocking = latest.findings.filter(isBlocking);
|
|
344
511
|
|
|
@@ -445,13 +612,18 @@ export const buildLedgerState = ({ cwd, env = process.env, detect = detectBacken
|
|
|
445
612
|
const plans = plansInFlight(root);
|
|
446
613
|
const fingerprint = computeTreeFingerprint(cwd);
|
|
447
614
|
const clean = fingerprint == null ? null : isTreeClean(cwd);
|
|
615
|
+
const base = resolveBase(cwd);
|
|
448
616
|
const ledgerPath = resolveLedgerPath(cwd, env);
|
|
449
617
|
const { records, malformed, malformedReasons, readError } = ledgerPath ? readLedger(ledgerPath) : { records: [], malformed: 0, malformedReasons: [] };
|
|
450
618
|
const receiptsPath = resolveReceiptsPath(cwd, env);
|
|
451
619
|
const { receipts } = receiptsPath ? readReceipts(receiptsPath) : { receipts: [] };
|
|
452
|
-
return { resolved, requiredBackends, plans, fingerprint, clean, ledgerPath, records, malformed, malformedReasons, readError, receipts, receiptsPath, detectionWarning };
|
|
620
|
+
return { resolved, requiredBackends, plans, fingerprint, clean, base, ledgerPath, records, malformed, malformedReasons, readError, receipts, receiptsPath, detectionWarning };
|
|
453
621
|
};
|
|
454
622
|
|
|
623
|
+
// The human label of a segment base (null = an unborn branch; undefined never occurs in a real
|
|
624
|
+
// state — resolveBase returns string | null).
|
|
625
|
+
const baseLabel = (base) => (base === null ? '(unborn branch)' : String(base).slice(0, 12));
|
|
626
|
+
|
|
455
627
|
// The normative --check decision (the header contract, in order) → { code, reason }.
|
|
456
628
|
export const decideCheck = (state) => {
|
|
457
629
|
// A detector failure is UNKNOWN state, not "no reviewer ready" — fail closed (like review-state).
|
|
@@ -476,14 +648,23 @@ export const decideCheck = (state) => {
|
|
|
476
648
|
if (state.malformed > 0) return { code: 1, reason: `the ledger has ${state.malformed} malformed line(s) — failing closed (a dropped line could hide a non-converged round); inspect ${state.ledgerPath}` };
|
|
477
649
|
|
|
478
650
|
const loop = state.plans[0].replace(/\.md$/, '');
|
|
479
|
-
|
|
651
|
+
// SEGMENT scope (D1): the gate judges the current segment — (plan-execution, loop, base = the
|
|
652
|
+
// HEAD the dirty tree sits on). Records of earlier segments (other bases) and pre-v4 records
|
|
653
|
+
// (no base — they can never enter a segment) are history, not the current loop state.
|
|
654
|
+
const filtered = filterSegmentRecords(state.records, { activity: ACTIVITY, loop, base: state.base });
|
|
480
655
|
const rounds = filtered.filter((r) => r.kind === 'round');
|
|
481
|
-
// A dirty active plan
|
|
656
|
+
// A dirty active plan whose SEGMENT has no round is a FAILURE, not a fail-open pass (codex R2
|
|
657
|
+
// hole) — same failure direction as AD-045, per-segment remedy. When the loop holds only
|
|
658
|
+
// older-schema records, the reason names the schema upgrade (D7 legacy rule).
|
|
482
659
|
if (rounds.length === 0) {
|
|
483
|
-
|
|
660
|
+
const loopAll = filterLoopRecords(state.records, { activity: ACTIVITY, loop });
|
|
661
|
+
const legacy = loopAll.length > 0 && loopAll.every((r) => !(r.schema >= 4))
|
|
662
|
+
? ' (the loop has only pre-v4 records, which never enter a segment — the schema upgrade requires a fresh v4 round)'
|
|
663
|
+
: '';
|
|
664
|
+
return { code: 1, reason: `dirty plan-execution loop "${loop}" but no review round recorded for the current segment (base ${baseLabel(state.base)}) — record the current round${legacy}` };
|
|
484
665
|
}
|
|
485
666
|
// A corrupt round sequence (not 1..n) is unknown state → fail closed rather than trust "latest" (codex R3).
|
|
486
|
-
if (!roundSequenceIntact(rounds)) return { code: 1, reason: `the round sequence for "${loop}" is corrupt (not 1..n) — failing closed; inspect ${state.ledgerPath}` };
|
|
667
|
+
if (!roundSequenceIntact(rounds)) return { code: 1, reason: `the round sequence for "${loop}" (base ${baseLabel(state.base)}) is corrupt (not 1..n) — failing closed; inspect ${state.ledgerPath}` };
|
|
487
668
|
const decision = decideStop(filtered, { cap: REVIEW_CAP, currentFingerprint: state.fingerprint, requiredBackends: state.requiredBackends });
|
|
488
669
|
if (decision.state === 'converged' || decision.state === 'resolved-residual') {
|
|
489
670
|
// Integrity binding: cross-check the latest round's non-degraded backends against their receipts
|
|
@@ -511,8 +692,46 @@ const roundLine = (r) => {
|
|
|
511
692
|
const triageLine = (t) =>
|
|
512
693
|
` triage @round ${t.round} — ${t.classifications.map((c) => `${c.findingKey}=${c.class}${c.class === 'escalate' ? `(accepted:${c.accepted})` : ''}`).join(', ')}`;
|
|
513
694
|
|
|
514
|
-
const
|
|
515
|
-
|
|
695
|
+
const overridePayload = (o) => {
|
|
696
|
+
if (o.scope === 'oracle-change') return o.files.join(', ');
|
|
697
|
+
if (o.scope === 'size-cap') return `sanctioned ${o.sanctionedLines} lines`;
|
|
698
|
+
return o.testId;
|
|
699
|
+
};
|
|
700
|
+
const overrideLine = (o) => ` override @round ${o.round} [${o.scope}] — ${overridePayload(o)}: ${o.reason}`;
|
|
701
|
+
|
|
702
|
+
const gateRunLine = (g) => {
|
|
703
|
+
const posture = isQualityGreenGateRun(g)
|
|
704
|
+
? 'quality-green'
|
|
705
|
+
: g.fingerprint !== g.fingerprintAfter
|
|
706
|
+
? 'NOT quality-green (the tree changed under the run)'
|
|
707
|
+
: 'NOT quality-green (a subset, a red gate, or an unfingerprinted tree)';
|
|
708
|
+
return ` gate-run — status=${g.summary.status} ${g.summary.passed}/${g.summary.gates} green of ${g.declared.length} declared — ${posture}`;
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
const recordLine = (r) =>
|
|
712
|
+
r.kind === 'round' ? roundLine(r) : r.kind === 'override' ? overrideLine(r) : r.kind === 'gate-run' ? gateRunLine(r) : triageLine(r);
|
|
713
|
+
|
|
714
|
+
// The loop's records grouped by SEGMENT (D1): v4 records group by base in order of first
|
|
715
|
+
// appearance; pre-v4 records (no base) group under a legacy header — readable history that never
|
|
716
|
+
// enters a segment.
|
|
717
|
+
const segmentGroups = (forLoop, currentBase) => {
|
|
718
|
+
const lines = [];
|
|
719
|
+
const seen = new Set();
|
|
720
|
+
const legacy = forLoop.filter((r) => !(r.schema >= 4));
|
|
721
|
+
if (legacy.length > 0) {
|
|
722
|
+
lines.push(' pre-v4 records (no segment — readable history):');
|
|
723
|
+
for (const r of legacy) lines.push(` ${recordLine(r)}`);
|
|
724
|
+
}
|
|
725
|
+
for (const r of forLoop) {
|
|
726
|
+
if (!(r.schema >= 4)) continue;
|
|
727
|
+
const key = JSON.stringify(r.base);
|
|
728
|
+
if (seen.has(key)) continue;
|
|
729
|
+
seen.add(key);
|
|
730
|
+
lines.push(` segment @ base ${baseLabel(r.base)}${r.base === currentBase ? ' (current)' : ''}:`);
|
|
731
|
+
for (const s of forLoop) if (s.schema >= 4 && s.base === r.base) lines.push(` ${recordLine(s)}`);
|
|
732
|
+
}
|
|
733
|
+
return lines;
|
|
734
|
+
};
|
|
516
735
|
|
|
517
736
|
const formatHuman = (state, check) => {
|
|
518
737
|
const lines = [
|
|
@@ -523,41 +742,138 @@ const formatHuman = (state, check) => {
|
|
|
523
742
|
if (state.fingerprint == null) lines.push(' tree: not a git work tree');
|
|
524
743
|
else if (state.clean === true) lines.push(' tree: clean (nothing to review)');
|
|
525
744
|
else lines.push(` tree fingerprint: ${state.fingerprint}`);
|
|
745
|
+
if (state.fingerprint != null) lines.push(` segment base: ${baseLabel(state.base)}`);
|
|
526
746
|
lines.push(` ledger: ${state.ledgerPath ?? '(unresolvable — no git dir)'} (${state.records.length} record(s)${state.malformed ? `, ${state.malformed} malformed — inspect the file` : ''})`);
|
|
527
747
|
if (state.plans.length === 1) {
|
|
528
748
|
const loop = state.plans[0].replace(/\.md$/, '');
|
|
529
|
-
|
|
530
|
-
for (const r of forLoop) lines.push(r.kind === 'round' ? roundLine(r) : r.kind === 'override' ? overrideLine(r) : triageLine(r));
|
|
749
|
+
lines.push(...segmentGroups(filterLoopRecords(state.records, { activity: ACTIVITY, loop }), state.base));
|
|
531
750
|
}
|
|
532
751
|
lines.push(` check: ${check.code === 0 ? 'PASS' : 'FAIL'} — ${check.reason}`);
|
|
533
752
|
return lines.join('\n');
|
|
534
753
|
};
|
|
535
754
|
|
|
536
|
-
|
|
755
|
+
// ── telemetry (D8): read-only counts across ALL loops and BOTH ledgers — no judgment ─────────────
|
|
756
|
+
|
|
757
|
+
// computeTelemetry(reviewRecords, foldRows) → { loops: [...] } — deterministic counts with named
|
|
758
|
+
// fields, pinned by a named test. Interpretation (which gates earn their keep) stays with the
|
|
759
|
+
// maintainer. Fold rows come through the TOLERANT reader (readJsonlRows): fields are guarded, a
|
|
760
|
+
// half-shaped row counts only what it provably carries.
|
|
761
|
+
export const computeTelemetry = (reviewRecords, foldRows) => {
|
|
762
|
+
const loops = new Map();
|
|
763
|
+
const bump = (obj, key) => { obj[key] = (obj[key] ?? 0) + 1; };
|
|
764
|
+
const forLoop = (activity, loop) => {
|
|
765
|
+
const key = JSON.stringify([activity, loop]);
|
|
766
|
+
if (!loops.has(key)) {
|
|
767
|
+
loops.set(key, {
|
|
768
|
+
activity, loop, rounds: 0, segments: new Set(), legacyRecords: 0,
|
|
769
|
+
origins: { 'first-draft': 0, 'fold-induced': 0, mechanics: 0 },
|
|
770
|
+
classifications: {}, backendVerdicts: {}, divergenceRounds: 0, overrides: {},
|
|
771
|
+
gateRuns: 0, qualityGreenGateRuns: 0, redByGateId: {},
|
|
772
|
+
foldRuns: 0, redProbes: 0, quarantinedProbes: 0,
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
return loops.get(key);
|
|
776
|
+
};
|
|
777
|
+
for (const r of reviewRecords) {
|
|
778
|
+
const t = forLoop(r.activity, r.loop);
|
|
779
|
+
if (r.schema >= 4) t.segments.add(JSON.stringify(r.base));
|
|
780
|
+
else t.legacyRecords += 1;
|
|
781
|
+
if (r.kind === 'round') {
|
|
782
|
+
t.rounds += 1;
|
|
783
|
+
for (const k of ORIGINS) t.origins[k] += r.origins[k];
|
|
784
|
+
const nonDegraded = r.backends.filter((b) => !b.degraded);
|
|
785
|
+
for (const b of r.backends) {
|
|
786
|
+
const verdicts = (t.backendVerdicts[b.backend] ??= {});
|
|
787
|
+
bump(verdicts, b.degraded ? 'degraded' : b.verdict);
|
|
788
|
+
}
|
|
789
|
+
// Divergence = a round where the non-degraded backends SPLIT on clean (0 blockers + 0 majors)
|
|
790
|
+
// vs not — the computed crossover signal, counted, never judged.
|
|
791
|
+
const clean = nonDegraded.filter((b) => b.blockers === 0 && b.majors === 0).length;
|
|
792
|
+
if (nonDegraded.length >= 2 && clean > 0 && clean < nonDegraded.length) t.divergenceRounds += 1;
|
|
793
|
+
} else if (r.kind === 'triage') {
|
|
794
|
+
for (const c of r.classifications) bump(t.classifications, c.class);
|
|
795
|
+
} else if (r.kind === 'override') {
|
|
796
|
+
bump(t.overrides, r.scope);
|
|
797
|
+
} else if (r.kind === 'gate-run') {
|
|
798
|
+
t.gateRuns += 1;
|
|
799
|
+
if (isQualityGreenGateRun(r)) t.qualityGreenGateRuns += 1;
|
|
800
|
+
for (const res of r.results) if (!res.ok) bump(t.redByGateId, res.id);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
for (const row of foldRows) {
|
|
804
|
+
if (typeof row.loop !== 'string' || row.loop.length === 0) continue;
|
|
805
|
+
const t = forLoop('plan-execution', row.loop); // the fold ledger is plan-execution-scoped
|
|
806
|
+
if (row.kind === 'red-probe') t.redProbes += 1;
|
|
807
|
+
else if (row.kind === 'run' || (row.schema === 1 && row.kind === undefined)) {
|
|
808
|
+
t.foldRuns += 1;
|
|
809
|
+
if (Array.isArray(row.testIds)) {
|
|
810
|
+
for (const e of row.testIds) {
|
|
811
|
+
const counted = e && [e.runs, e.greens, e.reds, e.timeouts].every(Number.isInteger);
|
|
812
|
+
if (counted && probeVerdict(e) === 'quarantine') t.quarantinedProbes += 1;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
const sorted = [...loops.values()].sort((a, b) => (a.activity + a.loop < b.activity + b.loop ? -1 : 1));
|
|
818
|
+
return { loops: sorted.map((t) => ({ ...t, segments: t.segments.size })) };
|
|
819
|
+
};
|
|
820
|
+
|
|
821
|
+
const countsOf = (obj) => {
|
|
822
|
+
const keys = Object.keys(obj).sort();
|
|
823
|
+
return keys.length === 0 ? '(none)' : keys.map((k) => `${k}:${obj[k]}`).join(' ');
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
export const renderTelemetry = (telemetry, meta) => {
|
|
827
|
+
const lines = [
|
|
828
|
+
`review-ledger telemetry — counts only, no judgment (D8). review ledger: ${meta.records} record(s)${meta.malformed ? `, ${meta.malformed} malformed` : ''}; fold ledger: ${meta.rows} row(s)${meta.badLines ? `, ${meta.badLines} unparseable` : ''}.`,
|
|
829
|
+
];
|
|
830
|
+
if (meta.readError) lines.push(` ⚠ review ledger unreadable (${meta.readError}) — counts exclude it`);
|
|
831
|
+
if (meta.foldReadError) lines.push(` ⚠ fold ledger unreadable (${meta.foldReadError}) — counts exclude it`);
|
|
832
|
+
if (telemetry.loops.length === 0) lines.push(' (no loops recorded)');
|
|
833
|
+
for (const t of telemetry.loops) {
|
|
834
|
+
lines.push(` ${t.activity} / ${t.loop}:`);
|
|
835
|
+
lines.push(` rounds ${t.rounds} across ${t.segments} segment(s)${t.legacyRecords ? ` (+${t.legacyRecords} pre-v4 record(s))` : ''} · divergence rounds ${t.divergenceRounds}`);
|
|
836
|
+
lines.push(` finding origins — ${ORIGINS.map((k) => `${k}:${t.origins[k]}`).join(' ')}`);
|
|
837
|
+
lines.push(` classifications — ${countsOf(t.classifications)}`);
|
|
838
|
+
lines.push(` backend verdicts — ${Object.keys(t.backendVerdicts).sort().map((b) => `${b}{${countsOf(t.backendVerdicts[b])}}`).join(' · ') || '(none)'}`);
|
|
839
|
+
lines.push(` overrides — ${countsOf(t.overrides)}`);
|
|
840
|
+
lines.push(` gate-runs ${t.gateRuns} (quality-green ${t.qualityGreenGateRuns}) · red results by gate — ${countsOf(t.redByGateId)}`);
|
|
841
|
+
lines.push(` fold runs ${t.foldRuns} · observed-red receipts ${t.redProbes} · quarantined probe entries ${t.quarantinedProbes}`);
|
|
842
|
+
}
|
|
843
|
+
return lines.join('\n');
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
const HELP = `review-ledger — read-only review-round LEDGER checker (agent-workflow family, AD-045 + AD-048).
|
|
537
847
|
|
|
538
848
|
Usage:
|
|
539
|
-
node review-ledger.mjs [--check | --status | --json]
|
|
849
|
+
node review-ledger.mjs [--check | --status | --json | --telemetry]
|
|
540
850
|
|
|
541
851
|
Reads the review-round ledger the orchestrator records to (<git dir>/${LEDGER_BASENAME};
|
|
542
852
|
AW_REVIEW_LEDGER overrides), resolves the effective ${ACTIVITY}.${SLOT} recipe, recomputes the
|
|
543
853
|
canonical uncommitted-state fingerprint, and computes the crossover-stop decision for the in-flight
|
|
544
|
-
plan-execution loop (
|
|
854
|
+
plan-execution loop's current SEGMENT — (activity, loop, base = the HEAD commit the dirty tree sits
|
|
855
|
+
on; schema v4). Round numbering, caps, and the teeth are per segment; a segment closes only by a
|
|
856
|
+
gated commit, so the round-counter reset is earned, never declared.
|
|
545
857
|
|
|
546
858
|
--status (default) → the human report: resolved recipe, plan-in-flight, per-round tally with
|
|
547
|
-
findings,
|
|
859
|
+
findings, grouped by segment, the decideStop verdict.
|
|
548
860
|
--check → the gate exit code. The normative exit contract lives in the tool header (the single home):
|
|
549
861
|
exit 0 for solo / no plan in flight / a clean tree / not-a-git-tree / a converged or
|
|
550
|
-
resolved-residual in-flight
|
|
551
|
-
with no round/receipt recorded, more than one plan in flight, or a receipt inconsistency.
|
|
862
|
+
resolved-residual in-flight SEGMENT; exit 1 for a dirty non-converged segment, triage-required, a
|
|
863
|
+
segment with no round/receipt recorded, more than one plan in flight, or a receipt inconsistency.
|
|
552
864
|
--json → the structured state + decision.
|
|
865
|
+
--telemetry → read-only counts across ALL loops and BOTH ledgers (D8): rounds/segments per loop,
|
|
866
|
+
finding origins, classification distribution (incl. refuted), per-backend verdicts + divergence
|
|
867
|
+
rounds, override usage by scope, gate-run counts (quality-green / red-by-gate), fold runs,
|
|
868
|
+
observed-red receipts, quarantined probes. Counts only — interpretation stays with you.
|
|
553
869
|
|
|
554
|
-
The writer is a SEPARATE tool (review-ledger-write.mjs record/classify) — this read-only
|
|
555
|
-
never imports it. Human residual: git commit --no-verify, ledger-file editing, and forged
|
|
556
|
-
remain possible — a self-discipline mechanism, not a security boundary.
|
|
870
|
+
The writer is a SEPARATE tool (review-ledger-write.mjs record/classify/override) — this read-only
|
|
871
|
+
checker never imports it. Human residual: git commit --no-verify, ledger-file editing, and forged
|
|
872
|
+
counts remain possible — a self-discipline mechanism, not a security boundary.
|
|
557
873
|
|
|
558
874
|
Exit codes: 0 pass (or plain report); 1 check failed or config error (loud); 2 usage.`;
|
|
559
875
|
|
|
560
|
-
const KNOWN_ARGS = new Set(['--help', '-h', '--check', '--status', '--json']);
|
|
876
|
+
const KNOWN_ARGS = new Set(['--help', '-h', '--check', '--status', '--json', '--telemetry']);
|
|
561
877
|
|
|
562
878
|
export const main = (argv, ctx = {}) => {
|
|
563
879
|
const cwd = ctx.cwd ?? process.cwd();
|
|
@@ -567,6 +883,19 @@ export const main = (argv, ctx = {}) => {
|
|
|
567
883
|
if (argv.includes('--help') || argv.includes('-h')) return { code: 0, stdout: HELP, stderr: '' };
|
|
568
884
|
const unknown = argv.find((a) => !KNOWN_ARGS.has(a));
|
|
569
885
|
if (unknown !== undefined) throw fail(2, `unknown argument: ${unknown}`);
|
|
886
|
+
// --telemetry is a standalone report: combined with --check it would WIN the dispatch and exit
|
|
887
|
+
// 0 without running decideCheck — a silently passing gate cmd (codex R2). Reject mixed modes.
|
|
888
|
+
if (argv.includes('--telemetry') && ['--check', '--status', '--json'].some((a) => argv.includes(a))) {
|
|
889
|
+
throw fail(2, '--telemetry never combines with --check/--status/--json — a mixed-mode invocation would bypass the gate; run them separately');
|
|
890
|
+
}
|
|
891
|
+
if (argv.includes('--telemetry')) {
|
|
892
|
+
const ledgerPath = resolveLedgerPath(cwd, env);
|
|
893
|
+
const { records, malformed, readError } = ledgerPath ? readLedger(ledgerPath) : { records: [], malformed: 0 };
|
|
894
|
+
const foldPath = resolveResultsPath(cwd, env);
|
|
895
|
+
const { rows, badLines, readError: foldReadError } = foldPath ? readJsonlRows(foldPath) : { rows: [], badLines: 0 };
|
|
896
|
+
const telemetry = computeTelemetry(records, rows);
|
|
897
|
+
return { code: 0, stdout: renderTelemetry(telemetry, { records: records.length, malformed, rows: rows.length, badLines, readError, foldReadError }), stderr: '' };
|
|
898
|
+
}
|
|
570
899
|
const state = buildLedgerState({ cwd, env, detect });
|
|
571
900
|
const check = decideCheck(state);
|
|
572
901
|
if (argv.includes('--json')) {
|