@sabaiway/agent-workflow-kit 14.4.0 → 14.4.1

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.
@@ -0,0 +1,331 @@
1
+ // review-state-judge.mjs — the review-state decision leaf; its normative contract is review-state.mjs's header.
2
+
3
+ import { join, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { escapeForDisplay, shellQuoteArg } from './repo-lex.mjs';
6
+ import { readDelegationLedger } from './dispatch-store-read.mjs';
7
+ import {
8
+ decideHeldSession, HELD_EXECUTE_WRAPPER, HELD_RECEIPT_BACKEND, judgeLedger,
9
+ } from './held-session.mjs';
10
+ import {
11
+ summarizeReviewReceiptsForTree,
12
+ isShipVerdict,
13
+ resolveEvidencePath,
14
+ readEvidence,
15
+ authoritativeOfKind,
16
+ } from './core-evidence.mjs';
17
+
18
+ export const ACTIVITY = 'plan-execution';
19
+ export const SLOT = 'review';
20
+ const CORE_EVIDENCE_TOOL = shellQuoteArg(join(dirname(fileURLToPath(import.meta.url)), 'core-evidence.mjs'));
21
+ const DISPATCH_TOOL = shellQuoteArg(join(dirname(fileURLToPath(import.meta.url)), 'dispatch.mjs'));
22
+
23
+ // The clean-tree PASS wording, split so the doc-parity registry can bind the forward-looking half
24
+ // to the mode doc — the notice is the contract, not decoration.
25
+ export const CLEAN_TREE_PASS = 'the working tree is clean — nothing to review';
26
+ export const LATENT_ARM_NOTICE = 'this gate arms as soon as the tree is dirty';
27
+
28
+ const UNSAFE_INLINE_SCALARS = /[\u007f-\u009f\u2028\u2029]/gu;
29
+ // A plan FILENAME reaches a one-line gate report. JSON quoting handles the C0 control bytes; the C1
30
+ // range (DEL + U+0080..U+009F, including the U+0085 line break) and the two Unicode line separators
31
+ // survive JSON.stringify and can still break a line, so they are escaped explicitly — every escape a
32
+ // valid JSON \uXXXX, so the token round-trips through JSON.parse. The plan set stays STRING-typed for
33
+ // its consumers (procedures / worktrees / the JSON surface); a name carrying INVALID UTF-8 bytes is a
34
+ // stated residual (queue: PARALLEL-TRACK-REDESIGN — two such names could collapse in the DISPLAY only,
35
+ // never in the gate decision, which keys on the receipt fingerprint, not the name).
36
+ export const quoteReportName = (name) =>
37
+ JSON.stringify(name).replace(
38
+ UNSAFE_INLINE_SCALARS,
39
+ (value) => `\\u${value.codePointAt(0).toString(16).padStart(4, '0')}`,
40
+ );
41
+
42
+ // Per-backend receipt status for the current fingerprint, over the ONE shared attesting-receipt
43
+ // predicate — the LATEST NORMAL (probe-free, marker-valid, current-fingerprint) receipt is
44
+ // selected FIRST and THEN judged, so a later unknown/ungrounded receipt never lets an earlier
45
+ // SHIP survive:
46
+ // current — the latest normal receipt ATTESTS (grounded + recognized verdict);
47
+ // its verdict rides — ONLY ship-class satisfies, a recognized negative
48
+ // is an authoritative veto;
49
+ // ungrounded — the latest normal receipt carries grounded:false;
50
+ // unrecognized-verdict — the latest normal receipt carries a verdict outside the closed
51
+ // vocabulary (an unknown verdict never attests — fail closed);
52
+ // probe — current receipts exist and EVERY one is a well-formed probe (D3);
53
+ // rejected — current receipts exist, none normal, and >=1 marker malformed/absent;
54
+ // stale — receipts exist, none for the current fingerprint (edited after review);
55
+ // missing — no receipt from this backend at all.
56
+ export const backendReceiptStatus = (receipts, backend, fingerprint) => {
57
+ const own = receipts.filter((r) => r.backend === backend);
58
+ const summary = summarizeReviewReceiptsForTree(own, fingerprint);
59
+ const counts = {
60
+ probeExcluded: summary.probeExcluded,
61
+ markerRejected: summary.markerRejected,
62
+ unmarkedRejected: summary.unmarkedRejected,
63
+ postureRejected: summary.postureRejected,
64
+ deliveryRejected: summary.deliveryRejected,
65
+ };
66
+ if (summary.state === 'current') {
67
+ return { state: 'current', verdict: summary.receipt.verdict ?? 'unknown', shipClass: isShipVerdict(summary.receipt.verdict), grounded: true, timestamp: summary.receipt.timestamp ?? null, ...counts };
68
+ }
69
+ if (summary.state === 'ungrounded' || summary.state === 'unrecognized-verdict') {
70
+ return { state: summary.state, verdict: summary.receipt.verdict ?? 'unknown', shipClass: false, grounded: summary.receipt.grounded === true, timestamp: summary.receipt.timestamp ?? null, ...counts };
71
+ }
72
+ if (summary.state === 'probe' || summary.state === 'rejected') {
73
+ return { state: summary.state, verdict: null, shipClass: false, grounded: null, timestamp: null, ...counts };
74
+ }
75
+ return { state: own.length > 0 ? 'stale' : 'missing', verdict: null, shipClass: false, grounded: null, timestamp: null, ...counts };
76
+ };
77
+
78
+ // degradeRecordSet — the D3(b) escape: an EXPLICIT per-backend, per-tree degrade RECORD in the
79
+ // core-evidence store is the ONLY exemption lane. Fail-closed: an unreadable/malformed store
80
+ // DENIES every exemption (surfaced), but never fails a tree whose receipts independently satisfy
81
+ // the gate. A stale-fingerprint record never exempts (the authoritative record per {backend,
82
+ // fingerprint} must attest THIS tree).
83
+ export const degradeRecordSet = ({ cwd, env = process.env, fingerprint }) => {
84
+ const storePath = resolveEvidencePath(cwd, env);
85
+ const read = storePath ? readEvidence(storePath) : { records: [], malformed: 0, malformedReasons: [] };
86
+ const unavailable = (read.malformed ?? 0) > 0 || read.readError != null;
87
+ const records = unavailable ? [] : authoritativeOfKind(read.records, 'degrade');
88
+ const set = unavailable || fingerprint == null
89
+ ? new Set()
90
+ : new Set(records.filter((r) => r.fingerprint === fingerprint).map((r) => r.backend));
91
+ return { set, records, storePath, malformed: read.malformed ?? 0, readError: read.readError ?? null, unavailable };
92
+ };
93
+
94
+ export const selectHeldSessionDegrades = (records) =>
95
+ records.filter((record) => record.kind === 'degrade' && record.backend === HELD_EXECUTE_WRAPPER);
96
+
97
+ export const shouldReadHeldSession = ({ configuredExecute, plans, fingerprint, clean }) =>
98
+ configuredExecute === 'delegated' && plans.length > 0 && fingerprint !== null && clean === false;
99
+
100
+ export const buildHeldSessionState = ({
101
+ cwd,
102
+ env,
103
+ configuredExecute,
104
+ plans,
105
+ fingerprint,
106
+ clean,
107
+ degrades,
108
+ resolveStore,
109
+ readStore,
110
+ audit,
111
+ readHead,
112
+ }) => {
113
+ if (!shouldReadHeldSession({ configuredExecute, plans, fingerprint, clean })) return null;
114
+ const ledger = readDelegationLedger(cwd, env, { resolveStore, readStore, audit, readHead });
115
+ return judgeLedger(ledger, { backend: HELD_RECEIPT_BACKEND, degrades });
116
+ };
117
+
118
+ // Why a backend's current-tree receipts were all rejected — the causes read differently and
119
+ // have different recoveries (fix the file vs refresh the bridge), so they are never collapsed.
120
+ export const rejectionCause = (b) => {
121
+ const parts = [];
122
+ if (b.markerRejected > 0) parts.push(`${b.markerRejected} with a malformed probe marker`);
123
+ if (b.unmarkedRejected > 0) {
124
+ parts.push(`${b.unmarkedRejected} with no probe marker — silence is not a declaration, so the probe status is untrustworthy; re-run the review with a bridge that marks its runs`);
125
+ }
126
+ if ((b.postureRejected ?? 0) > 0) {
127
+ parts.push(`${b.postureRejected} with an absent/invalid run posture (D5) — a pre-posture wrapper minted it; re-run the review on the current bridge`);
128
+ }
129
+ if ((b.deliveryRejected ?? 0) > 0) {
130
+ parts.push(`${b.deliveryRejected} with an absent/invalid delivery declaration (D8b) — the receipt never declared HOW the change set reached the model, so delivery was not proven; re-run the review on the current bridge`);
131
+ }
132
+ return parts.join(' + ');
133
+ };
134
+
135
+ // One failing backend row → its stated recovery. Shared by the council per-backend arm and the
136
+ // reviewed closest-recovery listing.
137
+ const backendFailurePart = (b, state) => {
138
+ if (b.state === 'current') return `${b.backend}: latest recognized verdict is ${JSON.stringify(b.verdict)} — a recognized negative is an authoritative veto; fold and re-review`;
139
+ if (b.state === 'unrecognized-verdict') return `${b.backend}: the latest normal receipt carries an unrecognized verdict (${JSON.stringify(b.verdict)}) — an unknown verdict never attests (fail-closed); re-run the review`;
140
+ if (b.state === 'ungrounded') return `${b.backend}: the latest normal receipt is ungrounded — re-run grounded (--facts)`;
141
+ if (b.state === 'probe') return `${b.backend}: only probe receipts for the current tree (CODEX_PROBE=1 / AGY_PROBE=1 relaxes the quality guards) — a probe review never attests; re-run a real one`;
142
+ if (b.state === 'rejected') return `${b.backend}: current-tree receipts rejected — ${rejectionCause(b)} (fail-closed); inspect ${escapeForDisplay(state.receiptsPath)}`;
143
+ if (b.state === 'stale') return `${b.backend}: receipts exist but none matches the current tree (edited after review) — run a fresh review`;
144
+ return `${b.backend}: no receipt — run its review wrapper, or record an explicit degrade (node ${CORE_EVIDENCE_TOOL} degrade --backend ${b.backend} --reason "...")`;
145
+ };
146
+
147
+ const describesHeldRecovery = (state) => {
148
+ const substitution = state.heldSession.substitution;
149
+ if (substitution === null) return '';
150
+ if (substitution.baseKind === 'checkpoint') {
151
+ return ` — a checkpoint-based thread: no core-evidence degrade lifts it; record its ledger degrade (node ${DISPATCH_TOOL} degrade --wave <wave> --nonce ${shellQuoteArg(substitution.nonce)} --step-class code --rationale "...") then retry it (cap permitting) or open a new thread on the held session, and fold that`;
152
+ }
153
+ const canRetry = substitution.folded === false;
154
+ if (state.evidenceUnavailable === true) {
155
+ const retry = canRetry ? '; or fold the retry of that thread' : '';
156
+ return ` — the evidence store is unavailable, so the degrade lane cannot be judged: repair it (inspect ${escapeForDisplay(state.evidenceStorePath)}) and re-check${retry}`;
157
+ }
158
+ const canDegrade = typeof substitution.postTreeDigest === 'string'
159
+ && state.fingerprint === substitution.postTreeDigest;
160
+ if (!canDegrade && !canRetry) {
161
+ return ' — no recovery lane remains: the tree moved after the substituted return and that thread is folded; re-dispatch the work under the held session';
162
+ }
163
+ const degrade = canDegrade
164
+ ? ` — mint the accepted-replacement record now, before the next edit: node ${CORE_EVIDENCE_TOOL} degrade --backend ${HELD_EXECUTE_WRAPPER} --reason "..."`
165
+ : '';
166
+ const retry = canRetry ? `${canDegrade ? '; or' : ' —'} fold the retry of that thread` : '';
167
+ return `${degrade}${retry}`;
168
+ };
169
+
170
+ // The normative --check decision (the header contract, in order). → { code, reason }.
171
+ // Obligations come from the CONFIGURED recipe; satisfaction is SHIP-CLASS ONLY; a recognized
172
+ // negative on the latest normal receipt VETOES; an explicit current-tree degrade record is the
173
+ // only escape for an unavailable backend under council — and never all backends (>=1 ship-class
174
+ // attestation is required whenever >=1 backend is configured).
175
+ export const decideCheck = (state) => {
176
+ // Store diagnostics ride EVERY check line, early exits (and the unknowable arm) included — a
177
+ // malformed receipt line, an unavailable evidence store, or an unreadable receipts store is
178
+ // never hidden behind any exit.
179
+ const malformedNote = state.malformed > 0 ? ` — ${state.malformed} malformed receipt line(s) ignored; inspect ${escapeForDisplay(state.receiptsPath)}` : '';
180
+ const evidenceNote = state.evidenceUnavailable
181
+ ? ` — evidence store unavailable (${state.evidenceMalformed} malformed line(s)${state.evidenceReadError ? `, read error: ${state.evidenceReadError}` : ''}); the degrade escape is denied (fail-closed) — inspect ${escapeForDisplay(state.evidenceStorePath)}`
182
+ : '';
183
+ const earlyNotes = `${malformedNote}${evidenceNote}${state.receiptsReadError ? ` — receipts store unreadable (${state.receiptsReadError}); inspect ${escapeForDisplay(state.receiptsPath)}` : ''}`;
184
+ // The git LOCATION (hand-built states carry none; the fingerprint decides there) rules below the solo arm: not-a-repository passes, the four other non-work-tree states refuse by name.
185
+ const location = state.location ?? { state: state.fingerprint == null ? 'not-a-repository' : 'work-tree', cause: 'no location was resolved for a hand-built state' };
186
+ if (state.heldSession !== null && state.heldSession !== undefined) {
187
+ const held = decideHeldSession(state.heldSession);
188
+ const heldRecovery = describesHeldRecovery(state);
189
+ if (held.code !== 0) return { ...held, reason: `${held.reason}${heldRecovery}${earlyNotes}`, terminal: true, heldSession: true };
190
+ }
191
+ // Detector failure with NO configured recipe: the computed default is unknowable → fail closed.
192
+ // An EXPLICIT configured recipe needs no detector — its obligations are readiness-independent.
193
+ if (state.obligations.unknowable) {
194
+ return { code: 1, reason: `cannot verify receipts — ${state.detectionWarning} No configured ${ACTIVITY}.${SLOT} recipe: the computed default is unknowable while the detector is down (fail closed).${earlyNotes}` };
195
+ }
196
+ if (state.obligations.recipe === 'solo' && !state.flowArmed && state.flowBrokenReason == null) {
197
+ const why = state.obligations.source === 'config'
198
+ ? `configured ${ACTIVITY}.${SLOT} recipe is solo`
199
+ : `no reviewer backend is ready — the computed ${ACTIVITY}.${SLOT} default is solo`;
200
+ return { code: 0, reason: `${why} — no receipt required${earlyNotes}` };
201
+ }
202
+ // An UNANCHORED root (git dead everywhere): the bare-cwd plan inventory cannot say "no plan".
203
+ if (location.state !== 'work-tree' && location.state !== 'not-a-repository' && state.rootAnchored === false) return { code: 1, reason: `the git location is ${location.state} — ${escapeForDisplay(location.cause)} — and the project root cannot be anchored, so the plan arm cannot decide (fail closed)${earlyNotes}` };
204
+ if (state.plans.length === 0) return { code: 0, reason: `no plan in flight (docs/plans/ holds no active plan) — no receipt required${earlyNotes}` };
205
+ if (location.state !== 'work-tree' && location.state !== 'not-a-repository') return { code: 1, reason: `the git location is ${location.state} — ${escapeForDisplay(location.cause)} — refusing to judge receipts for a tree git cannot name (fail closed)${earlyNotes}` };
206
+ if (location.state === 'not-a-repository') return { code: 0, reason: `not a git repository (git's own answer: ${escapeForDisplay(location.cause)}) — nothing to fingerprint${earlyNotes}` };
207
+ if (state.fingerprint == null || state.clean === null) return { code: 1, reason: `the tree ${state.fingerprint == null ? 'fingerprint' : 'clean check'} is undecidable in a work tree (a git query failed or was killed) — refusing to judge receipts (fail closed)${earlyNotes}` };
208
+ // A clean tree PASSES, but never silently: >=1 plan IS in flight here, and this gate arms the
209
+ // moment the tree turns dirty — naming each file makes the arm discoverable BEFORE it blocks.
210
+ if (state.clean === true) {
211
+ // Each name is byte-preserving and quoted because this reason is one gate-report line.
212
+ const named = state.plans.map((p) => quoteReportName(p)).join(', ');
213
+ return { code: 0, reason: `${CLEAN_TREE_PASS} — ${state.plans.length} plan(s) in flight: ${named} — ${LATENT_ARM_NOTICE}${earlyNotes}` };
214
+ }
215
+ // Tier-1 fail-closed (P3): a PRESENT flow store must read clean before the decision leans on
216
+ // receipts — the flow arms cannot be evaluated over a store of unknown content.
217
+ if (state.flowBrokenReason != null) {
218
+ return { code: 1, reason: `the flow store is unavailable (${state.flowBrokenReason}) — the flow arms fail closed; inspect the store${earlyNotes}` };
219
+ }
220
+ // The no-reviewers-configured floor (#34/#43): an ARMED flow makes the reduced record set an
221
+ // OBLIGATION for the solo class — every in-flight plan covered by an adopted chain AND
222
+ // internally attested at the current tree; a standing current-tree veto still blocks (#48),
223
+ // whatever the recipe consults, so a recipe flip to solo can never bury one.
224
+ if (state.obligations.recipe === 'solo') {
225
+ if (state.receiptsReadError != null) {
226
+ return { code: 1, reason: `the receipts store is unreadable (${state.receiptsReadError}) — "no standing veto" cannot be established, so the internal-only floor fails closed; inspect ${escapeForDisplay(state.receiptsPath)}${earlyNotes}` };
227
+ }
228
+ const standing = state.soloVetoRows.filter((b) => (b.state === 'current' && !b.shipClass && !b.overrideLabel) || b.state === 'unrecognized-verdict');
229
+ if (standing.length > 0) {
230
+ // `veto` marks the AUTHORITATIVE-veto class only (a recognized negative) — the Decision-4
231
+ // --await early exit keys on it; an unrecognized verdict stays a plain fail-closed refusal.
232
+ const authoritative = standing.some((b) => b.state === 'current' && !b.shipClass);
233
+ return { code: 1, reason: `${standing.map((b) => backendFailurePart(b, state)).join('; ')} — a standing veto blocks the internal-only floor (#48)${earlyNotes}`, ...(authoritative ? { veto: true } : {}) };
234
+ }
235
+ const uncovered = state.planCoverage.filter((p) => !p.covered);
236
+ if (uncovered.length > 0) {
237
+ return { code: 1, reason: `internal-only arming refused (#68): ${uncovered.map((p) => `plan ${quoteReportName(p.plan)} — ${p.reason}`).join('; ')} — an uncovered in-flight plan is a refusal, never a relaxation${earlyNotes}` };
238
+ }
239
+ const missing = state.planCoverage.filter((p) => !state.attestedPlanIds.includes(p.planId));
240
+ if (missing.length > 0) {
241
+ // A lens-refused attestation is a NAMED cause, never a silent gap — the refusal rides the
242
+ // floor's failure reason for exactly the plans it starved.
243
+ const lensNotes = (state.lensRefusedAttestations ?? []).filter((x) => missing.some((p) => p.planId === x.planId));
244
+ const lensSuffix = lensNotes.length > 0 ? `; ${lensNotes.map((x) => x.reason).join('; ')}` : '';
245
+ return { code: 1, reason: `internal-only floor: no internal-attestation record at the current tree for plan(s) ${missing.map((p) => quoteReportName(p.plan)).join(', ')} — the reduced record set (#34/#43) requires one per covered plan (minting rides the Plan-4 round machinery)${lensSuffix}${earlyNotes}` };
246
+ }
247
+ const floorLabels = [
248
+ ...state.soloVetoRows.filter((b) => b.deltaLift != null).map((b) => `${b.backend}: receipt lifted to CURRENT through an unbroken bookkeeping-delta chain (${b.deltaLift} link(s), #61)`),
249
+ ...state.soloVetoRows.filter((b) => b.overrideLabel).map((b) => b.overrideLabel),
250
+ 'internal-only (downgraded class)',
251
+ ];
252
+ const liftedTail = floorLabels.length > 1 ? ` — ${floorLabels.slice(0, -1).join('; ')}` : '';
253
+ return {
254
+ code: 0,
255
+ reason: `internal-only floor satisfied (DOWNGRADED class, #28/#43): the solo recipe under an ARMED flow consumes the reduced record set — every in-flight plan (${state.planCoverage.map((p) => quoteReportName(p.plan)).join(', ')}) is covered by an adopted chain and internally attested at the current tree; self-authored review, disclosed${liftedTail}${earlyNotes}`,
256
+ flowLabels: floorLabels,
257
+ };
258
+ }
259
+ const exempt = new Set(state.degradedExempt);
260
+ const satisfied = state.backends.filter((b) => b.state === 'current' && b.shipClass);
261
+ const vetoed = state.backends.filter((b) => b.state === 'current' && !b.shipClass && !b.overrideLabel);
262
+ const overridden = state.backends.filter((b) => b.state === 'current' && !b.shipClass && b.overrideLabel);
263
+ // The marker note is PATH-AWARE (no silent rejections on any exit): it counts every backend's
264
+ // untrusted-marker exclusions EXCEPT those whose printed part already names them — i.e. only a
265
+ // PRINTED `rejected` row (backendFailurePart's rejectionCause) suppresses its own counts; a
266
+ // printed veto/unrecognized/ungrounded row does not, and success paths suppress nothing.
267
+ const notesFor = (printed) => {
268
+ const total = state.backends
269
+ .filter((b) => !(printed.has(b.backend) && b.state === 'rejected'))
270
+ .reduce((n, b) => n + (b.markerRejected ?? 0) + (b.unmarkedRejected ?? 0), 0);
271
+ const markerNote = total > 0
272
+ ? ` — ${total} receipt(s) rejected: an untrustworthy probe marker (malformed, or absent — silence is not a declaration) — fail-closed; inspect ${escapeForDisplay(state.receiptsPath)}`
273
+ : '';
274
+ return `${markerNote}${earlyNotes}`;
275
+ };
276
+ const NONE_PRINTED = new Set();
277
+ // UNCONDITIONAL refusals, checked BEFORE minShip/exemptions: a recognized NEGATIVE (the
278
+ // authoritative veto) and an UNRECOGNIZED verdict (fail closed) — another backend's SHIP never
279
+ // masks them and a degrade record never lifts them (the backend demonstrably ran).
280
+ const unrecognized = state.backends.filter((b) => b.state === 'unrecognized-verdict');
281
+ const unconditional = [...vetoed, ...unrecognized];
282
+ if (unconditional.length > 0) {
283
+ // `veto` marks the AUTHORITATIVE-veto class only (Decision 4): --await ends the wait loudly on
284
+ // it — a landed negative is the dispatched review's ANSWER, never a condition to out-wait.
285
+ return { code: 1, reason: `${unconditional.map((b) => backendFailurePart(b, state)).join('; ')}${notesFor(new Set(unconditional.map((b) => b.backend)))}`, ...(vetoed.length > 0 ? { veto: true } : {}) };
286
+ }
287
+ // The armed-flow PASS labels (#61/#38): the delta-lift and override facts ride every PASS reason
288
+ // and the flowLabels field the commit-guard PASS line consumes. Empty (and absent) unarmed.
289
+ const flowNotes = [
290
+ ...state.backends.filter((b) => b.deltaLift != null)
291
+ .map((b) => `${b.backend}: receipt lifted to CURRENT through an unbroken bookkeeping-delta chain (${b.deltaLift} link(s), #61)`),
292
+ ...overridden.map((b) => b.overrideLabel),
293
+ ];
294
+ const flowSuffix = flowNotes.length > 0 ? ` — ${flowNotes.join('; ')}` : '';
295
+ const pass = (reason) => (state.flowArmed ? { code: 0, reason, flowLabels: flowNotes } : { code: 0, reason });
296
+ const perBackendFailing = state.backends.filter((b) => !(b.state === 'current' && b.shipClass) && !exempt.has(b.backend) && !b.overrideLabel);
297
+ // Never all degraded: >=1 ship-class attestation whenever >=1 backend is configured. An
298
+ // already-exempt backend renders its own honest part — never the "record an explicit degrade"
299
+ // recovery it has already taken.
300
+ if (satisfied.length < state.obligations.minShip) {
301
+ const failing = state.backends.filter((b) => !(b.state === 'current' && b.shipClass) && !b.overrideLabel);
302
+ const allExempt = failing.length > 0 && failing.every((b) => exempt.has(b.backend));
303
+ const head = failing.length === 0
304
+ ? 'every configured backend is veto-overridden for this tree — >=1 non-overridden ship-class attestation is required; run at least one real review'
305
+ : allExempt
306
+ ? `every configured backend is ${overridden.length > 0 ? 'degrade-recorded or veto-overridden' : 'degrade-recorded'} for this tree — never all degraded: >=1 ${overridden.length > 0 ? 'non-degraded, non-overridden' : 'non-degraded'} ship-class attestation is required; run at least one real review`
307
+ : failing
308
+ .map((b) => (exempt.has(b.backend)
309
+ ? `${b.backend}: degrade-recorded for this tree — a degrade never counts toward the >=1 ship-class floor; run a real review on another backend`
310
+ : backendFailurePart(b, state)))
311
+ .join('; ');
312
+ // Only backends that actually rendered their backendFailurePart suppress their counts — an
313
+ // exempt row prints the degrade string (no rejectionCause), so its exclusions stay named.
314
+ return { code: 1, reason: `${head}${notesFor(new Set(failing.filter((b) => !exempt.has(b.backend)).map((b) => b.backend)))}` };
315
+ }
316
+ if (state.obligations.perBackend) {
317
+ // Council: EVERY configured backend must attest ship-class OR carry a current-tree degrade
318
+ // record OR a lifted maintainer-override.
319
+ if (perBackendFailing.length > 0) {
320
+ return { code: 1, reason: `${perBackendFailing.map((b) => backendFailurePart(b, state)).join('; ')}${notesFor(new Set(perBackendFailing.map((b) => b.backend)))}` };
321
+ }
322
+ if (exempt.size === 0 && overridden.length === 0) {
323
+ return pass(`every configured backend attests ship-class for the current tree (${state.requiredBackends.join(' + ')})${flowSuffix}${notesFor(NONE_PRINTED)}`);
324
+ }
325
+ const clauses = [`council satisfied: ship-class attestation(s) from ${satisfied.map((b) => b.backend).join(' + ')}`];
326
+ if (exempt.size > 0) clauses.push(`degrade-recorded for this tree: ${[...exempt].join(', ')}`);
327
+ return pass(`${clauses.join('; ')}${flowSuffix}${notesFor(NONE_PRINTED)}`);
328
+ }
329
+ // Reviewed: >=1 ship-class attestation from any review-capable backend satisfies.
330
+ return pass(`reviewed satisfied: ship-class attestation from ${satisfied.map((b) => b.backend).join(' + ')} for the current tree${flowSuffix}${notesFor(NONE_PRINTED)}`);
331
+ };
@@ -0,0 +1,67 @@
1
+ // review-state-render.mjs - the human report and the mask advisory line.
2
+
3
+ import { join, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { CONFIG_REL } from './orchestration-config.mjs';
6
+ import { escapeForDisplay, shellQuoteArg } from './repo-lex.mjs';
7
+ import { decideHeldSession } from './held-session.mjs';
8
+ import { ACTIVITY, SLOT, quoteReportName, rejectionCause } from './review-state-judge.mjs';
9
+
10
+ // ── the sandbox-masks advisory (D lane, AD-044 Plan 4 Phase 1.5) ────────────────────
11
+
12
+ export const maskAdvisoryLine = (state) =>
13
+ state.maskedUntracked > 0
14
+ ? `notice: ${state.maskedUntracked} never-committable untracked path(s) (device/FIFO/socket) are ignored by the review domain — hide them from git status: node ${shellQuoteArg(join(dirname(fileURLToPath(import.meta.url)), 'sandbox-masks.mjs'))} --cwd ${shellQuoteArg(state.root)} --apply`
15
+ : '';
16
+
17
+ // ── rendering ───────────────────────────────────────────────────────────────────────
18
+
19
+ // The glyph reflects SATISFACTION, not bare state: a `current` row carrying a recognized
20
+ // NEGATIVE is an authoritative veto and renders ✗.
21
+ const STATE_GLYPH = { 'unrecognized-verdict': '✗', ungrounded: '✗', probe: '✗', rejected: '✗', stale: '✗', missing: '✗' };
22
+ const glyphFor = (b) => (b.state === 'current' ? (b.shipClass ? '✓' : '✗') : STATE_GLYPH[b.state]);
23
+
24
+ export const formatHuman = (state, check) => {
25
+ const src = state.obligations.source === 'config' ? `from ${CONFIG_REL}` : 'computed default';
26
+ const lines = [
27
+ `review-state — ${ACTIVITY}.${SLOT} = ${state.obligations.recipe ?? '(unknowable)'} (${src})${state.requiredBackends.length ? ` → ${state.requiredBackends.join(' + ')}${state.obligations.perBackend ? '' : ' (any one, ship-class)'}` : ''}`,
28
+ ];
29
+ if (state.detectionWarning) lines.push(` ⚠ ${state.detectionWarning}`);
30
+ lines.push(` plan in flight: ${state.plans.length ? state.plans.map((name) => quoteReportName(name)).join(', ') : '(none)'}`);
31
+ if (state.fingerprint == null) lines.push(` tree: ${state.location != null && state.location.state !== 'work-tree' ? `git location ${state.location.state} — ${escapeForDisplay(state.location.cause)}` : 'the fingerprint is undecidable (a git query failed or was killed)'}`);
32
+ else if (state.clean === true) lines.push(' tree: clean (nothing to review)');
33
+ else lines.push(` tree fingerprint: ${state.fingerprint}`);
34
+ if (state.heldSession != null) lines.push(` ${decideHeldSession(state.heldSession).line}`);
35
+ lines.push(` receipts: ${state.receiptsPath == null ? '(unresolvable — no git dir)' : escapeForDisplay(state.receiptsPath)} (${state.receiptCount} line(s)${state.malformed ? `, ${state.malformed} malformed — inspect the file` : ''})`);
36
+ if (state.receiptsReadError) lines.push(` ⚠ receipts store unreadable (${state.receiptsReadError}) — inspect ${escapeForDisplay(state.receiptsPath)}`);
37
+ if (state.evidenceUnavailable) lines.push(` ⚠ evidence store unavailable (${state.evidenceMalformed} malformed line(s)${state.evidenceReadError ? `, read error: ${state.evidenceReadError}` : ''}) — the degrade escape is denied (fail-closed)`);
38
+ const exempt = new Set(state.degradedExempt);
39
+ for (const b of state.backends) {
40
+ const exemptTag = exempt.has(b.backend) ? ' — degrade-recorded for the current tree (core-evidence store)' : '';
41
+ const detail =
42
+ b.state === 'current'
43
+ ? `current (verdict: ${JSON.stringify(b.verdict)}${b.shipClass ? ', ship-class' : ' — a recognized negative, an authoritative VETO'}, grounded, ${b.timestamp ?? '?'})`
44
+ : b.state === 'unrecognized-verdict'
45
+ ? `latest normal receipt carries an unrecognized verdict (${JSON.stringify(b.verdict)}) — never attests (fail-closed)`
46
+ : b.state === 'ungrounded'
47
+ ? `ungrounded latest normal receipt (verdict: ${JSON.stringify(b.verdict)}) — a grounded fresh run is required`
48
+ : b.state === 'probe'
49
+ ? 'only probe receipts for the current tree (quality guards relaxed) — a probe review never attests'
50
+ : b.state === 'rejected'
51
+ ? `current-tree receipts rejected — ${rejectionCause(b)} (fail-closed)`
52
+ : b.state === 'stale'
53
+ ? 'stale — no receipt matches the current tree (edited after review)'
54
+ : 'missing — no receipt from this backend';
55
+ const excludedTag = b.probeExcluded || b.markerRejected || b.unmarkedRejected
56
+ ? ` [excluded: ${b.probeExcluded} probe, ${b.markerRejected} malformed-marker, ${b.unmarkedRejected} unmarked]`
57
+ : '';
58
+ const liftTag = b.deltaLift != null ? ` — lifted to CURRENT through ${b.deltaLift} bookkeeping-delta link(s) (#61)` : '';
59
+ const overrideTag = b.overrideLabel ? ` — ${b.overrideLabel}` : '';
60
+ // ⊘ only where the escape actually applies: a PRODUCED receipt outranks the record — a
61
+ // negative/unknown verdict keeps its ✗ (a degrade lifts neither; a maintainer-override does).
62
+ const escapeApplies = (exempt.has(b.backend) && b.state !== 'current' && b.state !== 'unrecognized-verdict') || Boolean(b.overrideLabel);
63
+ lines.push(` ${escapeApplies ? '⊘' : glyphFor(b)} ${b.backend}: ${detail}${liftTag}${overrideTag}${excludedTag}${exemptTag}`);
64
+ }
65
+ lines.push(` check: ${check.code === 0 ? 'PASS' : 'FAIL'} — ${check.reason}`);
66
+ return lines.join('\n');
67
+ };