@sabaiway/agent-workflow-kit 2.1.0 → 3.0.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 +45 -0
- package/README.md +6 -5
- package/SKILL.md +12 -8
- package/bin/install.mjs +1 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +8 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review-honesty.test.mjs +150 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review-model-screen.test.mjs +76 -0
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +80 -15
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +95 -4
- package/bridges/antigravity-cli-bridge/capability.json +3 -2
- package/bridges/codex-cli-bridge/SKILL.md +8 -1
- package/bridges/codex-cli-bridge/bin/codex-review-honesty.test.mjs +143 -0
- package/bridges/codex-cli-bridge/bin/codex-review.sh +71 -10
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +93 -10
- package/bridges/codex-cli-bridge/capability.json +3 -2
- package/capability.json +1 -1
- package/migrations/3.0.0-hardened-core-loop.md +44 -0
- package/migrations/README.md +1 -1
- package/package.json +2 -2
- package/references/hooks/gate-approve.mjs +1 -1
- package/references/modes/bootstrap.md +3 -3
- package/references/modes/commit-guard.md +19 -0
- package/references/modes/core-evidence.md +15 -0
- package/references/modes/coverage-check.md +12 -0
- package/references/modes/doc-parity.md +7 -6
- package/references/modes/gates.md +7 -6
- package/references/modes/grounding.md +1 -2
- package/references/modes/hook.md +1 -1
- package/references/modes/review-state.md +3 -3
- package/references/modes/upgrade.md +6 -6
- package/references/modes/velocity.md +2 -2
- package/references/scripts/archive-decisions.mjs +1 -1
- package/references/scripts/install-git-hooks-repo-exec.test.mjs +82 -0
- package/references/scripts/install-git-hooks.mjs +90 -18
- package/references/scripts/install-git-hooks.test.mjs +102 -0
- package/references/scripts/migrate-gates-branches.test.mjs +157 -0
- package/references/scripts/migrate-gates.mjs +395 -0
- package/references/scripts/migrate-gates.test.mjs +284 -0
- package/references/templates/agent_rules.md +2 -2
- package/tools/ack-write.mjs +1 -1
- package/tools/atomic-write.mjs +2 -2
- package/tools/autonomy-config.mjs +1 -1
- package/tools/autonomy-doctor.mjs +1 -1
- package/tools/autonomy-write.mjs +1 -1
- package/tools/bridge-settings-read.mjs +1 -1
- package/tools/bridge-settings.mjs +1 -1
- package/tools/changed-surface.mjs +8 -69
- package/tools/cheap-agents.mjs +2 -2
- package/tools/commands.mjs +17 -10
- package/tools/commit-guard.mjs +167 -0
- package/tools/core-evidence.mjs +914 -0
- package/tools/coverage-check.mjs +260 -0
- package/tools/delegation.mjs +1 -1
- package/tools/detect-backends.mjs +3 -3
- package/tools/doc-parity.mjs +11 -27
- package/tools/engine-source.mjs +1 -1
- package/tools/family-members.mjs +1 -1
- package/tools/family-registry.mjs +1 -1
- package/tools/fs-safe.mjs +1 -1
- package/tools/gate-hook.mjs +1 -1
- package/tools/{seed-gates.mjs → gates-init.mjs} +58 -52
- package/tools/grounding.mjs +6 -83
- package/tools/hide-footprint.mjs +1 -1
- package/tools/inject-methodology.mjs +1 -1
- package/tools/known-footprint.mjs +1 -1
- package/tools/labels.mjs +1 -1
- package/tools/lcov.mjs +6 -10
- package/tools/lens-region.mjs +1 -1
- package/tools/manifest/validate.mjs +19 -0
- package/tools/migrate-adr-store.mjs +2 -2
- package/tools/orchestration-config.mjs +1 -1
- package/tools/orchestration-write.mjs +1 -1
- package/tools/presentation.mjs +1 -1
- package/tools/procedures.mjs +2 -2
- package/tools/recipes.mjs +60 -9
- package/tools/recommendations.mjs +63 -7
- package/tools/release-scan.mjs +1 -1
- package/tools/renderers.mjs +1 -1
- package/tools/review-state.mjs +288 -340
- package/tools/run-gates.mjs +216 -92
- package/tools/sandbox-masks.mjs +1 -1
- package/tools/semver-lite.mjs +1 -1
- package/tools/set-autonomy.mjs +1 -1
- package/tools/set-recipe.mjs +1 -1
- package/tools/setup-backends.mjs +1 -1
- package/tools/surface.mjs +1 -1
- package/tools/uninstall.mjs +1 -1
- package/tools/velocity-profile.mjs +2 -2
- package/tools/view-model.mjs +1 -1
- package/references/modes/fold-completeness.md +0 -30
- package/references/modes/review-ledger.md +0 -34
- package/references/templates/verification-profile.json +0 -10
- package/tools/fold-completeness-run.mjs +0 -1120
- package/tools/fold-completeness.mjs +0 -672
- package/tools/review-ledger-core.mjs +0 -428
- package/tools/review-ledger-write.mjs +0 -647
- package/tools/review-ledger.mjs +0 -630
- package/tools/sarif.mjs +0 -52
- package/tools/verification-profile.mjs +0 -219
package/tools/review-ledger.mjs
DELETED
|
@@ -1,630 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// review-ledger.mjs — the read-only review-round LEDGER checker behind `/agent-workflow-kit
|
|
3
|
-
// review-ledger` (DEBT-REVIEW-CAP / AD-045). It turns the prose review-loop crossover-stop
|
|
4
|
-
// (planning.md §9 "cap ≤2 / crossover-stop / fold-at-altitude"; procedures.md "{round N ·
|
|
5
|
-
// finding-origin tally · per-backend verdict} … computed signal, not a remembered rule") into a
|
|
6
|
-
// COMPUTED signal: the orchestrator records one round per review round to a git-dir JSONL, and this
|
|
7
|
-
// tool computes the stop decision from the recorded rounds + triage classifications, never from a
|
|
8
|
-
// remembered rule. It is the read-only sibling of review-state.mjs (presence vs convergence are
|
|
9
|
-
// distinct axes) — it NEVER imports the writer (review-ledger-write.mjs); an import-split test pins
|
|
10
|
-
// that structural invariant.
|
|
11
|
-
//
|
|
12
|
-
// Normative `--check` exit contract (the single home of this list — SKILL.md / the mode file point
|
|
13
|
-
// here). The gate enforces the plan-EXECUTION (code) loop — the loop that produces the committable
|
|
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):
|
|
18
|
-
// exit 0 when the resolved plan-execution.review recipe is solo (configured, or degraded there —
|
|
19
|
-
// no reviewer ready); when no plan is in flight (the review-state naming convention);
|
|
20
|
-
// when the tree is clean (nothing to review); when the cwd is not a git work tree; and
|
|
21
|
-
// when the in-flight plan-execution SEGMENT is `converged` or `resolved-residual` (its
|
|
22
|
-
// latest round's non-degraded backends carry ATTESTING code receipts for the recorded
|
|
23
|
-
// fingerprint — the shared review-ledger-core predicate: grounded AND probe:false, a
|
|
24
|
-
// probe/unmarked/malformed marker never attests — and a recorded 0/0 is
|
|
25
|
-
// ship-class-consistent with those receipts).
|
|
26
|
-
// exit 1 for any DIRTY in-flight plan-execution segment that is neither `converged` nor
|
|
27
|
-
// `resolved-residual` — `triage-required`, `continue`, OR no round/receipt recorded in
|
|
28
|
-
// the CURRENT segment (a dirty active plan with an empty/stale/other-segment ledger is a
|
|
29
|
-
// FAILURE, not a fail-open pass; when the loop holds only pre-v4 records the reason names
|
|
30
|
-
// the schema upgrade — old records never enter a segment, D7);
|
|
31
|
-
// when MORE THAN ONE plan is in flight (ambiguous loop id); when a recorded ship-class 0/0
|
|
32
|
-
// coexists with a non-ship receipt verdict, or a non-degraded recorded backend lacks a
|
|
33
|
-
// grounded receipt for its fingerprint. Fail-CLOSED (unknown state, never a fail-open pass)
|
|
34
|
-
// on a detector failure, an unreadable (non-ENOENT) ledger, malformed ledger lines the
|
|
35
|
-
// reader dropped, or a corrupt segment round sequence (not 1..n) — the only
|
|
36
|
-
// detector-independent green is an EXPLICIT configured solo.
|
|
37
|
-
//
|
|
38
|
-
// The stop decision: `decideStop(records, { cap, currentFingerprint, requiredBackends })` reads the
|
|
39
|
-
// ordered ledger records of ONE loop (both `round` and `triage` kinds) and returns exactly one state
|
|
40
|
-
// ∈ {converged, resolved-residual, triage-required, continue} under the fixed precedence
|
|
41
|
-
// (converged > resolved-residual > triage-required > continue). It reads MACHINE fields only (counts,
|
|
42
|
-
// class, accepted, fingerprint), never free-text. `hardMax` is NOT an input — it is a writer-only
|
|
43
|
-
// ceiling (Decision 5), enforced in review-ledger-write.mjs.
|
|
44
|
-
//
|
|
45
|
-
// HUMAN residual (accepted, documented — exactly like review-state's): the ledger attests a review
|
|
46
|
-
// occurred and its ship-class is consistent; it does NOT prove the recorded COUNTS are truthful, nor
|
|
47
|
-
// that a self-reported `degraded:true` is real (`git commit --no-verify`, ledger-file editing, and
|
|
48
|
-
// forged counts remain possible). The ledger lives in the git dir (never committable) — an honest
|
|
49
|
-
// self-discipline mechanism against silent process drift, not a security boundary.
|
|
50
|
-
//
|
|
51
|
-
// Read-only: never writes, never commits, never runs a subscription CLI. It DOES spawn read-only
|
|
52
|
-
// `git` queries (via the reused review-state fingerprint reader + a git-dir resolver). Dependency-
|
|
53
|
-
// free, Node >= 18. No side effects on import (the isDirectRun idiom).
|
|
54
|
-
|
|
55
|
-
import { pathToFileURL } from 'node:url';
|
|
56
|
-
import { spawnSync } from 'node:child_process';
|
|
57
|
-
import { detectBackends } from './detect-backends.mjs';
|
|
58
|
-
import { resolveActivityRecipe, planRecipe, DISPLAY_ALIASES } from './recipes.mjs';
|
|
59
|
-
import { CONFIG_REL, fail, loadConfig } from './orchestration-config.mjs';
|
|
60
|
-
// Reuse the review-state readers directly (the family read/write split — review-state is read-only,
|
|
61
|
-
// so importing it keeps this module writer-free): the canonical fingerprint, the tolerant receipt
|
|
62
|
-
// parse, the tree-clean preflight, the plan-in-flight detector, and the receipt-path resolver.
|
|
63
|
-
import {
|
|
64
|
-
computeTreeFingerprint,
|
|
65
|
-
isTreeClean,
|
|
66
|
-
plansInFlight,
|
|
67
|
-
readReceipts,
|
|
68
|
-
resolveReceiptsPath,
|
|
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';
|
|
74
|
-
// The NEUTRAL ledger read/schema core (AD-050): the validated read path — path/base resolvers,
|
|
75
|
-
// schema validator + reader, loop/segment filters — extracted so review-state.mjs can read the
|
|
76
|
-
// ledger for its degraded exemption WITHOUT importing this module back (the cycle). Imported here
|
|
77
|
-
// for internal use AND re-exported below so every existing importer (grounding, fold-completeness
|
|
78
|
-
// [-run], review-ledger-write, doc-parity, the tests) resolves unchanged.
|
|
79
|
-
import {
|
|
80
|
-
LEDGER_BASENAME,
|
|
81
|
-
ORIGINS,
|
|
82
|
-
resolveLedgerPath,
|
|
83
|
-
resolveBase,
|
|
84
|
-
readLedger,
|
|
85
|
-
filterLoopRecords,
|
|
86
|
-
filterSegmentRecords,
|
|
87
|
-
roundSequenceIntact,
|
|
88
|
-
summarizeReviewReceiptsForTree,
|
|
89
|
-
describeMissingReviewAttestation,
|
|
90
|
-
} from './review-ledger-core.mjs';
|
|
91
|
-
export {
|
|
92
|
-
LEDGER_BASENAME,
|
|
93
|
-
ORIGINS,
|
|
94
|
-
resolveLedgerPath,
|
|
95
|
-
resolveBase,
|
|
96
|
-
readLedger,
|
|
97
|
-
filterLoopRecords,
|
|
98
|
-
filterSegmentRecords,
|
|
99
|
-
roundSequenceIntact,
|
|
100
|
-
};
|
|
101
|
-
// Re-export-only (validated in the core, unused internally here): the schema/vocabulary + testId
|
|
102
|
-
// format the writer, doc-parity, and fold-completeness import through this module.
|
|
103
|
-
export { SCHEMA_VERSION, V4_CLASSES, V4_OVERRIDE_SCOPES, isWellFormedTestId, splitTestId, validateRecord } from './review-ledger-core.mjs';
|
|
104
|
-
|
|
105
|
-
const ACTIVITY = 'plan-execution';
|
|
106
|
-
const SLOT = 'review';
|
|
107
|
-
// The triage TRIGGER cap (Decision 5): reaching it with an unclassified surviving blocking finding
|
|
108
|
-
// forces triage. Shared with the writer (which imports it) — the writer-only hard-max ceiling lives
|
|
109
|
-
// there, never here (it is not a decideStop input).
|
|
110
|
-
export const REVIEW_CAP = 2;
|
|
111
|
-
|
|
112
|
-
// ── git-dir resolution (read-only queries) ──────────────────────────────────────────────────────
|
|
113
|
-
|
|
114
|
-
const gitLine = (args, cwd) => {
|
|
115
|
-
const r = spawnSync('git', args, { cwd, windowsHide: true });
|
|
116
|
-
if (r.error || r.status !== 0) return null;
|
|
117
|
-
return r.stdout.toString('utf8').replace(/\r?\n$/, '');
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const gitRoot = (cwd) => gitLine(['rev-parse', '--show-toplevel'], cwd);
|
|
121
|
-
|
|
122
|
-
// ── ship-verdict mapping (the single home; a named test pins it) ────────────────────────────────
|
|
123
|
-
|
|
124
|
-
// isShipVerdict(verdict) — which free-text review verdicts are ship-class. SHIP / SHIP WITH NITS are
|
|
125
|
-
// ship; revise / REWORK / unknown / anything else are not. Case-insensitive, trimmed.
|
|
126
|
-
export const isShipVerdict = (verdict) => {
|
|
127
|
-
const v = String(verdict ?? '').trim().toLowerCase();
|
|
128
|
-
return v === 'ship' || v === 'ship with nits';
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
// isProcessGateCmd(cmd) — the CLOSED, kit-owned process-gate classification (D5): the kit's own
|
|
132
|
-
// process-loop `--check` commands (review-state / review-ledger / fold-completeness) legitimately
|
|
133
|
-
// fail MID-loop, so "quality-green" excludes them — without the carve-out the D5 tooth is
|
|
134
|
-
// unsatisfiable by construction. Closed by the gate's cmd being EXACTLY one `--check` invocation of
|
|
135
|
-
// one of the three checkers (an optionally-quoted path token whose basename is the tool — never a
|
|
136
|
-
// substring: fold-completeness-run.mjs must NOT match). A COMPOUND line (`… && checker --check`)
|
|
137
|
-
// is never process: exempting it would forgive the failing quality half — fail-open against the
|
|
138
|
-
// D5 direction (internal sweep, live-probed). Pinned by its own named test.
|
|
139
|
-
// The tool BASENAME must sit at a path boundary (start-of-token or after a separator): a
|
|
140
|
-
// suffix-named sibling like `my-review-ledger.mjs` is a consumer's own tool, and misclassifying it
|
|
141
|
-
// as process would forgive its red result (codex R2, fold-induced by the R1 exact-form rewrite).
|
|
142
|
-
const PROCESS_GATE_RE = /^node\s+("(?:[^"]*[/\\])?(?:review-state|review-ledger|fold-completeness)\.mjs"|(?:[^\s"]*[/\\])?(?:review-state|review-ledger|fold-completeness)\.mjs)\s+--check$/;
|
|
143
|
-
export const isProcessGateCmd = (cmd) => PROCESS_GATE_RE.test(String(cmd).trim());
|
|
144
|
-
|
|
145
|
-
// isQualityGreenGateRun(record) → boolean (D5). Quality-green = the run covers EVERY declared
|
|
146
|
-
// NON-process gate with a green result (a `--only` subset is recorded honestly but never satisfies
|
|
147
|
-
// this — the R1 converged subset-bypass hole), AND the tree did not change under the run
|
|
148
|
-
// (fingerprint === fingerprintAfter, both non-null — a mutating gate attests no particular tree,
|
|
149
|
-
// codex R2). Process-gate failures never block.
|
|
150
|
-
export const isQualityGreenGateRun = (record) => {
|
|
151
|
-
if (record.kind !== 'gate-run') return false;
|
|
152
|
-
if (record.fingerprint == null || record.fingerprint !== record.fingerprintAfter) return false;
|
|
153
|
-
const green = new Set(record.results.filter((r) => r.ok).map((r) => r.id));
|
|
154
|
-
return record.declared.every((d) => isProcessGateCmd(d.cmd) || green.has(d.id));
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
// collectOverrides(records, { activity, loop }) → { oracleChangeFiles: Set, redProofTestIds: Set }.
|
|
158
|
-
// The UNION of the loop's recorded v3-scope override payloads — loop + payload scoped, never
|
|
159
|
-
// fingerprint-bound (D3: re-affirmation churn on every later edit would train rubber-stamping). The
|
|
160
|
-
// fold-completeness checker consumes THIS (both modules are read-only — the read/write split holds).
|
|
161
|
-
// The v4 `size-cap` scope is deliberately NOT here: it is SEGMENT-scoped and consumed by the writer
|
|
162
|
-
// tooth via collectSizeCapLimit.
|
|
163
|
-
export const collectOverrides = (records, { activity = ACTIVITY, loop } = {}) => {
|
|
164
|
-
const oracleChangeFiles = new Set();
|
|
165
|
-
const redProofTestIds = new Set();
|
|
166
|
-
for (const r of filterLoopRecords(records, { activity, loop })) {
|
|
167
|
-
if (r.kind !== 'override') continue;
|
|
168
|
-
if (r.scope === 'oracle-change') for (const f of r.files) oracleChangeFiles.add(f);
|
|
169
|
-
else if (r.scope === 'red-proof') redProofTestIds.add(r.testId);
|
|
170
|
-
}
|
|
171
|
-
return { oracleChangeFiles, redProofTestIds };
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
// collectSizeCapLimit(records, { activity, loop, base }) → the LARGEST sanctionedLines among the
|
|
175
|
-
// segment's recorded size-cap overrides, or null when none exists (D4). Segment-scoped by
|
|
176
|
-
// construction: a magnitude sanctioned for one base never leaks into the next segment.
|
|
177
|
-
export const collectSizeCapLimit = (records, { activity = ACTIVITY, loop, base } = {}) => {
|
|
178
|
-
let limit = null;
|
|
179
|
-
for (const r of filterSegmentRecords(records, { activity, loop, base })) {
|
|
180
|
-
if (r.kind !== 'override' || r.scope !== 'size-cap') continue;
|
|
181
|
-
if (limit === null || r.sanctionedLines > limit) limit = r.sanctionedLines;
|
|
182
|
-
}
|
|
183
|
-
return limit;
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
// ── the computed crossover-stop (pure; machine fields only) ─────────────────────────────────────
|
|
187
|
-
|
|
188
|
-
const BLOCKING = new Set(['blocker', 'major']);
|
|
189
|
-
const isBlocking = (f) => BLOCKING.has(f.severity);
|
|
190
|
-
|
|
191
|
-
// decideStop(records, { cap, currentFingerprint, requiredBackends }) → { state, reason }.
|
|
192
|
-
// state ∈ {converged, resolved-residual, triage-required, continue}, fixed precedence
|
|
193
|
-
// converged > resolved-residual > triage-required > continue. Machine fields only.
|
|
194
|
-
export const decideStop = (records, { cap = REVIEW_CAP, currentFingerprint = null, requiredBackends = [] } = {}) => {
|
|
195
|
-
const rounds = records.filter((r) => r.kind === 'round');
|
|
196
|
-
const triages = records.filter((r) => r.kind === 'triage');
|
|
197
|
-
if (rounds.length === 0) return { state: 'continue', reason: 'no review round recorded for this loop yet' };
|
|
198
|
-
const latest = rounds[rounds.length - 1];
|
|
199
|
-
|
|
200
|
-
// Classification map, BOUND to the triggering (latest) round: only a triage that targets the latest
|
|
201
|
-
// round classifies its surviving findings. A triage recorded for an earlier round — or a future /
|
|
202
|
-
// nonexistent round — must NOT satisfy resolved-residual by findingKey alone (codex R1): a finding
|
|
203
|
-
// that recurs into a new round is re-triaged for that round. A later triage overrides an earlier one;
|
|
204
|
-
// each carries the triage's own fingerprint (the tree state its resolution was decided against).
|
|
205
|
-
const classOf = new Map();
|
|
206
|
-
for (const t of triages) if (t.round === latest.round) for (const c of t.classifications) classOf.set(c.findingKey, { ...c, triageFingerprint: t.fingerprint });
|
|
207
|
-
const isClassified = (key) => classOf.has(key);
|
|
208
|
-
// `refuted` (v4, AD-048) resolves like an inherent-layer-residual: a documented, grounds-cited
|
|
209
|
-
// resolution of a phantom finding. Without this arm a phantom minted at round HARD_MAX and
|
|
210
|
-
// honestly refuted would WEDGE the segment (the immutable round record keeps the minting
|
|
211
|
-
// backend's counts non-0/0, and no further round exists to vanish it into). Additive beside the
|
|
212
|
-
// frozen truth table — every pre-existing row is untouched (D10).
|
|
213
|
-
const isResolvedClass = (c) => c && (c.class === 'inherent-layer-residual' || c.class === 'refuted' || (c.class === 'escalate' && c.accepted === true));
|
|
214
|
-
|
|
215
|
-
const survivingBlocking = latest.findings.filter(isBlocking);
|
|
216
|
-
|
|
217
|
-
// ── converged ── every requiredBackend present + non-degraded at 0/0 (a degraded requiredBackend
|
|
218
|
-
// is excluded but must be recorded), at least one real non-degraded 0/0 review, current tree.
|
|
219
|
-
const entryFor = (rb) => latest.backends.find((b) => b.backend === rb);
|
|
220
|
-
const allPresent = requiredBackends.every((rb) => entryFor(rb) !== undefined);
|
|
221
|
-
const nonDegradedReq = requiredBackends.map(entryFor).filter((b) => b && !b.degraded);
|
|
222
|
-
const convergedCounts = allPresent && nonDegradedReq.length >= 1 && nonDegradedReq.every((b) => b.blockers === 0 && b.majors === 0);
|
|
223
|
-
if (convergedCounts && currentFingerprint != null && currentFingerprint === latest.fingerprint) {
|
|
224
|
-
return { state: 'converged', reason: `every recipe-named backend reviewed the current tree at 0 blockers + 0 majors (${requiredBackends.join(' + ') || 'none named'})` };
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// A blocking findingKey's presence across ROUND records (recurrence, Decision 5).
|
|
228
|
-
const blockingRoundCount = new Map();
|
|
229
|
-
for (const r of rounds) {
|
|
230
|
-
const keys = new Set(r.findings.filter(isBlocking).map((f) => f.findingKey));
|
|
231
|
-
for (const k of keys) blockingRoundCount.set(k, (blockingRoundCount.get(k) ?? 0) + 1);
|
|
232
|
-
}
|
|
233
|
-
const recurred = (key) => (blockingRoundCount.get(key) ?? 0) >= 2;
|
|
234
|
-
// Recurrence is keyed on the LATEST round's SURVIVING findings ONLY. A finding that was FIXED (no
|
|
235
|
-
// longer surviving) must never force triage-required — else the gate DEADLOCKS: recordTriage rightly
|
|
236
|
-
// refuses to classify a vanished finding, so a historical recurring key that is gone could never be
|
|
237
|
-
// cleared. This is the root simplification (council R3, confirmed by both backends).
|
|
238
|
-
const triggerReached = latest.round >= cap || survivingBlocking.some((f) => recurred(f.findingKey));
|
|
239
|
-
|
|
240
|
-
// ── resolved-residual ── trigger reached AND every recipe-named backend present (the SAME presence
|
|
241
|
-
// discipline as converged — a residual accepted while a recipe-named backend never reviewed is not
|
|
242
|
-
// resolved, codex R4) AND every surviving blocking finding classified inherent-layer-residual (or
|
|
243
|
-
// accepted-escalate) AND the classifying triage's fingerprint equals the current tree.
|
|
244
|
-
if (triggerReached && allPresent && survivingBlocking.length >= 1) {
|
|
245
|
-
const allResolved = survivingBlocking.every((f) => {
|
|
246
|
-
const c = classOf.get(f.findingKey);
|
|
247
|
-
return isResolvedClass(c) && currentFingerprint != null && c.triageFingerprint === currentFingerprint;
|
|
248
|
-
});
|
|
249
|
-
if (allResolved) {
|
|
250
|
-
return { state: 'resolved-residual', reason: `${survivingBlocking.length} surviving blocking finding(s) classified as an accepted residual at the current tree — never folded again` };
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// ── triage-required (writer HARD STOP) ── at/after the cap an UNCLASSIFIED surviving blocking
|
|
255
|
-
// finding, OR a blocking findingKey recurred in >= 2 rounds and is still unclassified (even under
|
|
256
|
-
// the cap). Only the UNCLASSIFIED state blocks — a classified fixable-bug lets the fix round run.
|
|
257
|
-
const unclassifiedSurviving = survivingBlocking.filter((f) => !isClassified(f.findingKey));
|
|
258
|
-
// Keys reference only LIVE (surviving) findings — never a vanished historical key (the deadlock).
|
|
259
|
-
const hasUnclassifiedRecurrence = unclassifiedSurviving.some((f) => recurred(f.findingKey));
|
|
260
|
-
if ((latest.round >= cap && unclassifiedSurviving.length >= 1) || hasUnclassifiedRecurrence) {
|
|
261
|
-
const keys = [...new Set(unclassifiedSurviving.map((f) => f.findingKey))];
|
|
262
|
-
const recurNote = hasUnclassifiedRecurrence ? ` (recurred in ≥2 rounds — classify whether it is an inherent-layer-residual before another fold)` : '';
|
|
263
|
-
return { state: 'triage-required', reason: `classify each surviving blocking finding before another round: ${keys.join(', ')}${recurNote}` };
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// ── continue (explicit catch-all) ──
|
|
267
|
-
let reason;
|
|
268
|
-
if (convergedCounts) reason = 'a clean review exists but the tree was edited after it — re-review the edited tree';
|
|
269
|
-
else if (survivingBlocking.length >= 1) reason = 'surviving blocking findings — fold and re-review, or classify at the cap';
|
|
270
|
-
else reason = 'not yet converged — record the current review round';
|
|
271
|
-
return { state: 'continue', reason };
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
// ── receipt cross-check (integrity binding, Decision 7) ──────────────────────────────────────────
|
|
275
|
-
|
|
276
|
-
// receiptCrossCheck(round, receipts, fingerprint) → { ok, reason }. For each NON-degraded backend of
|
|
277
|
-
// `round`: an ATTESTING receipt must exist for (backend, fingerprint) — a round cannot pass for a
|
|
278
|
-
// tree no bridge really reviewed — AND a recorded ship-class 0/0 must not coexist with a non-ship
|
|
279
|
-
// verdict. A degraded backend minted no receipt (it ran no real review) and is exempt.
|
|
280
|
-
// The attesting predicate is the SHARED one (review-ledger-core), the same review-state reads: this
|
|
281
|
-
// gate used to filter raw fields and take `own[own.length - 1]`, so a probe receipt landing AFTER a
|
|
282
|
-
// real one became the authoritative verdict — a late probe SHIP could bury a real REWORK and let
|
|
283
|
-
// BOTH gates report convergence (D3). The summary's `receipt` is the latest ATTESTING one, never
|
|
284
|
-
// simply the last line.
|
|
285
|
-
export const receiptCrossCheck = (round, receipts, fingerprint) => {
|
|
286
|
-
for (const b of round.backends) {
|
|
287
|
-
if (b.degraded) continue;
|
|
288
|
-
const own = receipts.filter((r) => r.backend === b.backend);
|
|
289
|
-
const summary = summarizeReviewReceiptsForTree(own, fingerprint);
|
|
290
|
-
const failure = describeMissingReviewAttestation(summary);
|
|
291
|
-
if (failure !== null) {
|
|
292
|
-
return { ok: false, reason: `no grounded code receipt for ${b.backend} at the recorded fingerprint can attest this review round — ${failure}` };
|
|
293
|
-
}
|
|
294
|
-
if (b.blockers === 0 && b.majors === 0) {
|
|
295
|
-
const { verdict } = summary.receipt;
|
|
296
|
-
if (!isShipVerdict(verdict)) {
|
|
297
|
-
return { ok: false, reason: `${b.backend} recorded 0 blockers/0 majors but its attesting receipt verdict "${verdict}" is not ship-class` };
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
return { ok: true };
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
// ── the check + report core ─────────────────────────────────────────────────────────────────────
|
|
305
|
-
|
|
306
|
-
// buildLedgerState({ cwd, env, detect }) → everything both renders need. Pure I/O at the edges;
|
|
307
|
-
// every project-relative read anchors at the git work-tree ROOT when one exists (the fingerprint is
|
|
308
|
-
// root-anchored — the same discipline review-state uses).
|
|
309
|
-
export const buildLedgerState = ({ cwd, env = process.env, detect = detectBackends } = {}) => {
|
|
310
|
-
const root = gitRoot(cwd) ?? cwd;
|
|
311
|
-
const { config } = loadConfig(root);
|
|
312
|
-
let detection = [];
|
|
313
|
-
let detectionWarning = null;
|
|
314
|
-
try {
|
|
315
|
-
detection = detect();
|
|
316
|
-
} catch (err) {
|
|
317
|
-
detectionWarning = `backend detection failed (${(err && err.message) || err}) — treating all backends as not ready; the review recipe floors at solo.`;
|
|
318
|
-
}
|
|
319
|
-
const resolved = resolveActivityRecipe({ config: config ?? {}, readiness: detection, activity: ACTIVITY, slot: SLOT });
|
|
320
|
-
const { dispatch } = planRecipe(resolved.recipe, detection);
|
|
321
|
-
const requiredBackends = dispatch.map((d) => DISPLAY_ALIASES[d.backend] ?? d.backend);
|
|
322
|
-
const plans = plansInFlight(root);
|
|
323
|
-
const fingerprint = computeTreeFingerprint(cwd);
|
|
324
|
-
const clean = fingerprint == null ? null : isTreeClean(cwd);
|
|
325
|
-
const base = resolveBase(cwd);
|
|
326
|
-
const ledgerPath = resolveLedgerPath(cwd, env);
|
|
327
|
-
const { records, malformed, malformedReasons, readError } = ledgerPath ? readLedger(ledgerPath) : { records: [], malformed: 0, malformedReasons: [] };
|
|
328
|
-
const receiptsPath = resolveReceiptsPath(cwd, env);
|
|
329
|
-
const { receipts } = receiptsPath ? readReceipts(receiptsPath) : { receipts: [] };
|
|
330
|
-
return { resolved, requiredBackends, plans, fingerprint, clean, base, ledgerPath, records, malformed, malformedReasons, readError, receipts, receiptsPath, detectionWarning };
|
|
331
|
-
};
|
|
332
|
-
|
|
333
|
-
// The human label of a segment base (null = an unborn branch; undefined never occurs in a real
|
|
334
|
-
// state — resolveBase returns string | null).
|
|
335
|
-
const baseLabel = (base) => (base === null ? '(unborn branch)' : String(base).slice(0, 12));
|
|
336
|
-
|
|
337
|
-
// The normative --check decision (the header contract, in order) → { code, reason }.
|
|
338
|
-
export const decideCheck = (state) => {
|
|
339
|
-
// A detector failure is UNKNOWN state, not "no reviewer ready" — fail closed (like review-state).
|
|
340
|
-
// The only detector-independent green is an EXPLICIT configured solo.
|
|
341
|
-
const explicitSolo = state.resolved.recipe === 'solo' && state.resolved.source === 'config' && !state.resolved.degradedFrom;
|
|
342
|
-
if (state.detectionWarning && !explicitSolo) return { code: 1, reason: `cannot verify the ledger — ${state.detectionWarning}` };
|
|
343
|
-
if (state.resolved.recipe === 'solo') {
|
|
344
|
-
const why = state.resolved.degradedFrom
|
|
345
|
-
? `resolved ${ACTIVITY}.${SLOT} recipe degrades to solo here (${state.resolved.reason})`
|
|
346
|
-
: `resolved ${ACTIVITY}.${SLOT} recipe is solo`;
|
|
347
|
-
return { code: 0, reason: `${why} — no ledger required` };
|
|
348
|
-
}
|
|
349
|
-
if (state.plans.length === 0) return { code: 0, reason: 'no plan in flight (docs/plans/ holds no active plan) — no ledger required' };
|
|
350
|
-
// More than one plan in flight → ambiguous loop id: fail CLOSED (Decision 6), never guess.
|
|
351
|
-
if (state.plans.length > 1) return { code: 1, reason: `more than one plan in flight (${state.plans.join(', ')}) — ambiguous loop id; resolve to one active plan` };
|
|
352
|
-
if (state.fingerprint == null) return { code: 0, reason: 'not a git work tree — nothing to fingerprint' };
|
|
353
|
-
if (state.clean === true) return { code: 0, reason: 'the working tree is clean — nothing to review' };
|
|
354
|
-
// Two unknown-state conditions on a dirty active plan → fail CLOSED, never a fail-open pass. A
|
|
355
|
-
// non-ENOENT read error (codex R1); AND malformed lines the tolerant reader dropped, which could
|
|
356
|
-
// silently remove the latest/non-converged round and let a stale converged one PASS (codex R3).
|
|
357
|
-
if (state.readError) return { code: 1, reason: `cannot read the ledger (${state.readError}) — failing closed; inspect ${state.ledgerPath}` };
|
|
358
|
-
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}` };
|
|
359
|
-
|
|
360
|
-
const loop = state.plans[0].replace(/\.md$/, '');
|
|
361
|
-
// SEGMENT scope (D1): the gate judges the current segment — (plan-execution, loop, base = the
|
|
362
|
-
// HEAD the dirty tree sits on). Records of earlier segments (other bases) and pre-v4 records
|
|
363
|
-
// (no base — they can never enter a segment) are history, not the current loop state.
|
|
364
|
-
const filtered = filterSegmentRecords(state.records, { activity: ACTIVITY, loop, base: state.base });
|
|
365
|
-
const rounds = filtered.filter((r) => r.kind === 'round');
|
|
366
|
-
// A dirty active plan whose SEGMENT has no round is a FAILURE, not a fail-open pass (codex R2
|
|
367
|
-
// hole) — same failure direction as AD-045, per-segment remedy. When the loop holds only
|
|
368
|
-
// older-schema records, the reason names the schema upgrade (D7 legacy rule).
|
|
369
|
-
if (rounds.length === 0) {
|
|
370
|
-
const loopAll = filterLoopRecords(state.records, { activity: ACTIVITY, loop });
|
|
371
|
-
const legacy = loopAll.length > 0 && loopAll.every((r) => !(r.schema >= 4))
|
|
372
|
-
? ' (the loop has only pre-v4 records, which never enter a segment — the schema upgrade requires a fresh v4 round)'
|
|
373
|
-
: '';
|
|
374
|
-
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}` };
|
|
375
|
-
}
|
|
376
|
-
// A corrupt round sequence (not 1..n) is unknown state → fail closed rather than trust "latest" (codex R3).
|
|
377
|
-
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}` };
|
|
378
|
-
const decision = decideStop(filtered, { cap: REVIEW_CAP, currentFingerprint: state.fingerprint, requiredBackends: state.requiredBackends });
|
|
379
|
-
if (decision.state === 'converged' || decision.state === 'resolved-residual') {
|
|
380
|
-
// Integrity binding: cross-check the latest round's non-degraded backends against their receipts
|
|
381
|
-
// at the RECORDED fingerprint (where the review happened). For converged that fingerprint equals
|
|
382
|
-
// the current tree by construction; for resolved-residual it is the reviewed round's tree.
|
|
383
|
-
const latest = rounds[rounds.length - 1];
|
|
384
|
-
const cc = receiptCrossCheck(latest, state.receipts, latest.fingerprint);
|
|
385
|
-
if (!cc.ok) return { code: 1, reason: `${decision.state} recorded but ${cc.reason}` };
|
|
386
|
-
return { code: 0, reason: `${decision.state} — ${decision.reason}` };
|
|
387
|
-
}
|
|
388
|
-
return { code: 1, reason: `${decision.state} — ${decision.reason}` };
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
// ── rendering ─────────────────────────────────────────────────────────────────────────────────
|
|
392
|
-
|
|
393
|
-
const roundLine = (r) => {
|
|
394
|
-
const backends = r.backends
|
|
395
|
-
.map((b) => `${b.backend} ${b.degraded ? `degraded(${b.reason})` : `${b.blockers}/${b.majors}/${b.minors} ${b.verdict}`}`)
|
|
396
|
-
.join(', ');
|
|
397
|
-
const origins = ORIGINS.map((k) => `${k}:${r.origins[k]}`).join(' ');
|
|
398
|
-
const findings = r.findings.length ? ` [${r.findings.map((f) => `${f.findingKey}(${f.severity})`).join(', ')}]` : '';
|
|
399
|
-
return ` round ${r.round} (${origins}) — ${backends}${findings}`;
|
|
400
|
-
};
|
|
401
|
-
|
|
402
|
-
const triageLine = (t) =>
|
|
403
|
-
` triage @round ${t.round} — ${t.classifications.map((c) => `${c.findingKey}=${c.class}${c.class === 'escalate' ? `(accepted:${c.accepted})` : ''}`).join(', ')}`;
|
|
404
|
-
|
|
405
|
-
const overridePayload = (o) => {
|
|
406
|
-
if (o.scope === 'oracle-change') return o.files.join(', ');
|
|
407
|
-
if (o.scope === 'size-cap') return `sanctioned ${o.sanctionedLines} lines`;
|
|
408
|
-
return o.testId;
|
|
409
|
-
};
|
|
410
|
-
const overrideLine = (o) => ` override @round ${o.round} [${o.scope}] — ${overridePayload(o)}: ${o.reason}`;
|
|
411
|
-
|
|
412
|
-
const gateRunLine = (g) => {
|
|
413
|
-
const posture = isQualityGreenGateRun(g)
|
|
414
|
-
? 'quality-green'
|
|
415
|
-
: g.fingerprint !== g.fingerprintAfter
|
|
416
|
-
? 'NOT quality-green (the tree changed under the run)'
|
|
417
|
-
: 'NOT quality-green (a subset, a red gate, or an unfingerprinted tree)';
|
|
418
|
-
return ` gate-run — status=${g.summary.status} ${g.summary.passed}/${g.summary.gates} green of ${g.declared.length} declared — ${posture}`;
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
const recordLine = (r) =>
|
|
422
|
-
r.kind === 'round' ? roundLine(r) : r.kind === 'override' ? overrideLine(r) : r.kind === 'gate-run' ? gateRunLine(r) : triageLine(r);
|
|
423
|
-
|
|
424
|
-
// The loop's records grouped by SEGMENT (D1): v4 records group by base in order of first
|
|
425
|
-
// appearance; pre-v4 records (no base) group under a legacy header — readable history that never
|
|
426
|
-
// enters a segment.
|
|
427
|
-
const segmentGroups = (forLoop, currentBase) => {
|
|
428
|
-
const lines = [];
|
|
429
|
-
const seen = new Set();
|
|
430
|
-
const legacy = forLoop.filter((r) => !(r.schema >= 4));
|
|
431
|
-
if (legacy.length > 0) {
|
|
432
|
-
lines.push(' pre-v4 records (no segment — readable history):');
|
|
433
|
-
for (const r of legacy) lines.push(` ${recordLine(r)}`);
|
|
434
|
-
}
|
|
435
|
-
for (const r of forLoop) {
|
|
436
|
-
if (!(r.schema >= 4)) continue;
|
|
437
|
-
const key = JSON.stringify(r.base);
|
|
438
|
-
if (seen.has(key)) continue;
|
|
439
|
-
seen.add(key);
|
|
440
|
-
lines.push(` segment @ base ${baseLabel(r.base)}${r.base === currentBase ? ' (current)' : ''}:`);
|
|
441
|
-
for (const s of forLoop) if (s.schema >= 4 && s.base === r.base) lines.push(` ${recordLine(s)}`);
|
|
442
|
-
}
|
|
443
|
-
return lines;
|
|
444
|
-
};
|
|
445
|
-
|
|
446
|
-
const formatHuman = (state, check) => {
|
|
447
|
-
const lines = [
|
|
448
|
-
`review-ledger — ${ACTIVITY}.${SLOT} = ${state.resolved.recipe} (${state.resolved.source === 'config' ? `from ${CONFIG_REL}` : 'computed default'})${state.requiredBackends.length ? ` → ${state.requiredBackends.join(' + ')}` : ''}`,
|
|
449
|
-
];
|
|
450
|
-
if (state.detectionWarning) lines.push(` ⚠ ${state.detectionWarning}`);
|
|
451
|
-
lines.push(` plan in flight: ${state.plans.length ? state.plans.join(', ') : '(none)'}`);
|
|
452
|
-
if (state.fingerprint == null) lines.push(' tree: not a git work tree');
|
|
453
|
-
else if (state.clean === true) lines.push(' tree: clean (nothing to review)');
|
|
454
|
-
else lines.push(` tree fingerprint: ${state.fingerprint}`);
|
|
455
|
-
if (state.fingerprint != null) lines.push(` segment base: ${baseLabel(state.base)}`);
|
|
456
|
-
lines.push(` ledger: ${state.ledgerPath ?? '(unresolvable — no git dir)'} (${state.records.length} record(s)${state.malformed ? `, ${state.malformed} malformed — inspect the file` : ''})`);
|
|
457
|
-
if (state.plans.length === 1) {
|
|
458
|
-
const loop = state.plans[0].replace(/\.md$/, '');
|
|
459
|
-
lines.push(...segmentGroups(filterLoopRecords(state.records, { activity: ACTIVITY, loop }), state.base));
|
|
460
|
-
}
|
|
461
|
-
lines.push(` check: ${check.code === 0 ? 'PASS' : 'FAIL'} — ${check.reason}`);
|
|
462
|
-
return lines.join('\n');
|
|
463
|
-
};
|
|
464
|
-
|
|
465
|
-
// ── telemetry (D8): read-only counts across ALL loops and BOTH ledgers — no judgment ─────────────
|
|
466
|
-
|
|
467
|
-
// computeTelemetry(reviewRecords, foldRows) → { loops: [...] } — deterministic counts with named
|
|
468
|
-
// fields, pinned by a named test. Interpretation (which gates earn their keep) stays with the
|
|
469
|
-
// maintainer. Fold rows come through the TOLERANT reader (readJsonlRows): fields are guarded, a
|
|
470
|
-
// half-shaped row counts only what it provably carries.
|
|
471
|
-
export const computeTelemetry = (reviewRecords, foldRows) => {
|
|
472
|
-
const loops = new Map();
|
|
473
|
-
const bump = (obj, key) => { obj[key] = (obj[key] ?? 0) + 1; };
|
|
474
|
-
const forLoop = (activity, loop) => {
|
|
475
|
-
const key = JSON.stringify([activity, loop]);
|
|
476
|
-
if (!loops.has(key)) {
|
|
477
|
-
loops.set(key, {
|
|
478
|
-
activity, loop, rounds: 0, segments: new Set(), legacyRecords: 0,
|
|
479
|
-
origins: { 'first-draft': 0, 'fold-induced': 0, mechanics: 0 },
|
|
480
|
-
classifications: {}, backendVerdicts: {}, divergenceRounds: 0, overrides: {},
|
|
481
|
-
gateRuns: 0, qualityGreenGateRuns: 0, redByGateId: {},
|
|
482
|
-
foldRuns: 0, redProbes: 0, quarantinedProbes: 0,
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
return loops.get(key);
|
|
486
|
-
};
|
|
487
|
-
for (const r of reviewRecords) {
|
|
488
|
-
const t = forLoop(r.activity, r.loop);
|
|
489
|
-
if (r.schema >= 4) t.segments.add(JSON.stringify(r.base));
|
|
490
|
-
else t.legacyRecords += 1;
|
|
491
|
-
if (r.kind === 'round') {
|
|
492
|
-
t.rounds += 1;
|
|
493
|
-
for (const k of ORIGINS) t.origins[k] += r.origins[k];
|
|
494
|
-
const nonDegraded = r.backends.filter((b) => !b.degraded);
|
|
495
|
-
for (const b of r.backends) {
|
|
496
|
-
const verdicts = (t.backendVerdicts[b.backend] ??= {});
|
|
497
|
-
bump(verdicts, b.degraded ? 'degraded' : b.verdict);
|
|
498
|
-
}
|
|
499
|
-
// Divergence = a round where the non-degraded backends SPLIT on clean (0 blockers + 0 majors)
|
|
500
|
-
// vs not — the computed crossover signal, counted, never judged.
|
|
501
|
-
const clean = nonDegraded.filter((b) => b.blockers === 0 && b.majors === 0).length;
|
|
502
|
-
if (nonDegraded.length >= 2 && clean > 0 && clean < nonDegraded.length) t.divergenceRounds += 1;
|
|
503
|
-
} else if (r.kind === 'triage') {
|
|
504
|
-
for (const c of r.classifications) bump(t.classifications, c.class);
|
|
505
|
-
} else if (r.kind === 'override') {
|
|
506
|
-
bump(t.overrides, r.scope);
|
|
507
|
-
} else if (r.kind === 'gate-run') {
|
|
508
|
-
t.gateRuns += 1;
|
|
509
|
-
if (isQualityGreenGateRun(r)) t.qualityGreenGateRuns += 1;
|
|
510
|
-
for (const res of r.results) if (!res.ok) bump(t.redByGateId, res.id);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
for (const row of foldRows) {
|
|
514
|
-
if (typeof row.loop !== 'string' || row.loop.length === 0) continue;
|
|
515
|
-
const t = forLoop('plan-execution', row.loop); // the fold ledger is plan-execution-scoped
|
|
516
|
-
if (row.kind === 'red-probe') t.redProbes += 1;
|
|
517
|
-
else if (row.kind === 'run' || (row.schema === 1 && row.kind === undefined)) {
|
|
518
|
-
t.foldRuns += 1;
|
|
519
|
-
if (Array.isArray(row.testIds)) {
|
|
520
|
-
for (const e of row.testIds) {
|
|
521
|
-
const counted = e && [e.runs, e.greens, e.reds, e.timeouts].every(Number.isInteger);
|
|
522
|
-
if (counted && probeVerdict(e) === 'quarantine') t.quarantinedProbes += 1;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
const sorted = [...loops.values()].sort((a, b) => (a.activity + a.loop < b.activity + b.loop ? -1 : 1));
|
|
528
|
-
return { loops: sorted.map((t) => ({ ...t, segments: t.segments.size })) };
|
|
529
|
-
};
|
|
530
|
-
|
|
531
|
-
const countsOf = (obj) => {
|
|
532
|
-
const keys = Object.keys(obj).sort();
|
|
533
|
-
return keys.length === 0 ? '(none)' : keys.map((k) => `${k}:${obj[k]}`).join(' ');
|
|
534
|
-
};
|
|
535
|
-
|
|
536
|
-
export const renderTelemetry = (telemetry, meta) => {
|
|
537
|
-
const lines = [
|
|
538
|
-
`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` : ''}.`,
|
|
539
|
-
];
|
|
540
|
-
if (meta.readError) lines.push(` ⚠ review ledger unreadable (${meta.readError}) — counts exclude it`);
|
|
541
|
-
if (meta.foldReadError) lines.push(` ⚠ fold ledger unreadable (${meta.foldReadError}) — counts exclude it`);
|
|
542
|
-
if (telemetry.loops.length === 0) lines.push(' (no loops recorded)');
|
|
543
|
-
for (const t of telemetry.loops) {
|
|
544
|
-
lines.push(` ${t.activity} / ${t.loop}:`);
|
|
545
|
-
lines.push(` rounds ${t.rounds} across ${t.segments} segment(s)${t.legacyRecords ? ` (+${t.legacyRecords} pre-v4 record(s))` : ''} · divergence rounds ${t.divergenceRounds}`);
|
|
546
|
-
lines.push(` finding origins — ${ORIGINS.map((k) => `${k}:${t.origins[k]}`).join(' ')}`);
|
|
547
|
-
lines.push(` classifications — ${countsOf(t.classifications)}`);
|
|
548
|
-
lines.push(` backend verdicts — ${Object.keys(t.backendVerdicts).sort().map((b) => `${b}{${countsOf(t.backendVerdicts[b])}}`).join(' · ') || '(none)'}`);
|
|
549
|
-
lines.push(` overrides — ${countsOf(t.overrides)}`);
|
|
550
|
-
lines.push(` gate-runs ${t.gateRuns} (quality-green ${t.qualityGreenGateRuns}) · red results by gate — ${countsOf(t.redByGateId)}`);
|
|
551
|
-
lines.push(` fold runs ${t.foldRuns} · observed-red receipts ${t.redProbes} · quarantined probe entries ${t.quarantinedProbes}`);
|
|
552
|
-
}
|
|
553
|
-
return lines.join('\n');
|
|
554
|
-
};
|
|
555
|
-
|
|
556
|
-
const HELP = `review-ledger — read-only review-round LEDGER checker (agent-workflow family, AD-045 + AD-048).
|
|
557
|
-
|
|
558
|
-
Usage:
|
|
559
|
-
node review-ledger.mjs [--check | --status | --json | --telemetry]
|
|
560
|
-
|
|
561
|
-
Reads the review-round ledger the orchestrator records to (<git dir>/${LEDGER_BASENAME};
|
|
562
|
-
AW_REVIEW_LEDGER overrides), resolves the effective ${ACTIVITY}.${SLOT} recipe, recomputes the
|
|
563
|
-
canonical uncommitted-state fingerprint, and computes the crossover-stop decision for the in-flight
|
|
564
|
-
plan-execution loop's current SEGMENT — (activity, loop, base = the HEAD commit the dirty tree sits
|
|
565
|
-
on; schema v4). Round numbering, caps, and the teeth are per segment; a segment closes only by a
|
|
566
|
-
gated commit, so the round-counter reset is earned, never declared.
|
|
567
|
-
|
|
568
|
-
--status (default) → the human report: resolved recipe, plan-in-flight, per-round tally with
|
|
569
|
-
findings, grouped by segment, the decideStop verdict.
|
|
570
|
-
--check → the gate exit code. The normative exit contract lives in the tool header (the single home):
|
|
571
|
-
exit 0 for solo / no plan in flight / a clean tree / not-a-git-tree / a converged or
|
|
572
|
-
resolved-residual in-flight SEGMENT; exit 1 for a dirty non-converged segment, triage-required, a
|
|
573
|
-
segment with no round/receipt recorded, more than one plan in flight, or a receipt inconsistency.
|
|
574
|
-
--json → the structured state + decision.
|
|
575
|
-
--telemetry → read-only counts across ALL loops and BOTH ledgers (D8): rounds/segments per loop,
|
|
576
|
-
finding origins, classification distribution (incl. refuted), per-backend verdicts + divergence
|
|
577
|
-
rounds, override usage by scope, gate-run counts (quality-green / red-by-gate), fold runs,
|
|
578
|
-
observed-red receipts, quarantined probes. Counts only — interpretation stays with you.
|
|
579
|
-
|
|
580
|
-
The writer is a SEPARATE tool (review-ledger-write.mjs record/classify/override/batch) — this read-only
|
|
581
|
-
checker never imports it. Sandbox-safe: runs fully inside an OS sandbox (fs + git reads, no
|
|
582
|
-
network) — the D4 sandbox lane. Human residual: git commit --no-verify, ledger-file editing, and
|
|
583
|
-
forged counts remain possible — a self-discipline mechanism, not a security boundary.
|
|
584
|
-
|
|
585
|
-
Exit codes: 0 pass (or plain report); 1 check failed or config error (loud); 2 usage.`;
|
|
586
|
-
|
|
587
|
-
const KNOWN_ARGS = new Set(['--help', '-h', '--check', '--status', '--json', '--telemetry']);
|
|
588
|
-
|
|
589
|
-
export const main = (argv, ctx = {}) => {
|
|
590
|
-
const cwd = ctx.cwd ?? process.cwd();
|
|
591
|
-
const env = ctx.env ?? process.env;
|
|
592
|
-
const detect = ctx.detect ?? detectBackends;
|
|
593
|
-
try {
|
|
594
|
-
if (argv.includes('--help') || argv.includes('-h')) return { code: 0, stdout: HELP, stderr: '' };
|
|
595
|
-
const unknown = argv.find((a) => !KNOWN_ARGS.has(a));
|
|
596
|
-
if (unknown !== undefined) throw fail(2, `unknown argument: ${unknown}`);
|
|
597
|
-
// --telemetry is a standalone report: combined with --check it would WIN the dispatch and exit
|
|
598
|
-
// 0 without running decideCheck — a silently passing gate cmd (codex R2). Reject mixed modes.
|
|
599
|
-
if (argv.includes('--telemetry') && ['--check', '--status', '--json'].some((a) => argv.includes(a))) {
|
|
600
|
-
throw fail(2, '--telemetry never combines with --check/--status/--json — a mixed-mode invocation would bypass the gate; run them separately');
|
|
601
|
-
}
|
|
602
|
-
if (argv.includes('--telemetry')) {
|
|
603
|
-
const ledgerPath = resolveLedgerPath(cwd, env);
|
|
604
|
-
const { records, malformed, readError } = ledgerPath ? readLedger(ledgerPath) : { records: [], malformed: 0 };
|
|
605
|
-
const foldPath = resolveResultsPath(cwd, env);
|
|
606
|
-
const { rows, badLines, readError: foldReadError } = foldPath ? readJsonlRows(foldPath) : { rows: [], badLines: 0 };
|
|
607
|
-
const telemetry = computeTelemetry(records, rows);
|
|
608
|
-
return { code: 0, stdout: renderTelemetry(telemetry, { records: records.length, malformed, rows: rows.length, badLines, readError, foldReadError }), stderr: '' };
|
|
609
|
-
}
|
|
610
|
-
const state = buildLedgerState({ cwd, env, detect });
|
|
611
|
-
const check = decideCheck(state);
|
|
612
|
-
if (argv.includes('--json')) {
|
|
613
|
-
return { code: argv.includes('--check') ? check.code : 0, stdout: JSON.stringify({ ...state, check }, null, 2), stderr: '' };
|
|
614
|
-
}
|
|
615
|
-
if (argv.includes('--check')) {
|
|
616
|
-
return { code: check.code, stdout: `review-ledger check: ${check.code === 0 ? 'PASS' : 'FAIL'} — ${check.reason}`, stderr: '' };
|
|
617
|
-
}
|
|
618
|
-
return { code: 0, stdout: formatHuman(state, check), stderr: '' };
|
|
619
|
-
} catch (err) {
|
|
620
|
-
return { code: err.exitCode ?? 1, stdout: '', stderr: `review-ledger: ${err.message}` };
|
|
621
|
-
}
|
|
622
|
-
};
|
|
623
|
-
|
|
624
|
-
const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
625
|
-
if (isDirectRun) {
|
|
626
|
-
const r = main(process.argv.slice(2));
|
|
627
|
-
if (r.stdout) process.stdout.write(r.stdout.endsWith('\n') ? r.stdout : `${r.stdout}\n`);
|
|
628
|
-
if (r.stderr) process.stderr.write(r.stderr.endsWith('\n') ? r.stderr : `${r.stderr}\n`);
|
|
629
|
-
process.exitCode = r.code;
|
|
630
|
-
}
|