@sabaiway/agent-workflow-kit 5.5.0 → 5.6.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 +62 -0
- package/SKILL.md +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/gates.md +6 -3
- package/references/modes/procedures.md +2 -0
- package/references/modes/recommendations.md +1 -1
- package/references/modes/velocity.md +1 -0
- package/tools/flow-check-cores.mjs +253 -0
- package/tools/flow-check-git-lane.mjs +56 -0
- package/tools/flow-check-rungs.mjs +330 -0
- package/tools/flow-check.mjs +23 -611
- package/tools/gates-declaration.mjs +13 -1
- package/tools/gates-init.mjs +134 -22
- package/tools/procedures.mjs +64 -5
- package/tools/recommendations.mjs +108 -7
- package/tools/source-size-check.mjs +320 -0
- package/tools/source-size-config.mjs +244 -0
- package/tools/source-size-core.mjs +53 -0
- package/tools/source-size-gate-cmd.mjs +55 -0
- package/tools/source-size-judge.mjs +114 -0
- package/tools/source-size-refusal.mjs +70 -0
- package/tools/source-size-report.mjs +254 -0
- package/tools/source-size-scope.mjs +145 -0
- package/tools/velocity-profile.mjs +24 -3
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
// flow-check-rungs.mjs — the checker's EVIDENCE rungs (#61/#56/#65/#25/#42/#15/#3): pure
|
|
2
|
+
// predicates over flow records, core-evidence records, review receipts and an explicit tree —
|
|
3
|
+
// no store IO, no git. Split out of flow-check.mjs (baseline-practices tranche 1); the composed
|
|
4
|
+
// consumers (review-state, flow-writer) reach them through the flow-check facade.
|
|
5
|
+
//
|
|
6
|
+
// This is also the LOWER of the checker's pure modules, so the vocabulary its refusals share with
|
|
7
|
+
// flow-check-cores.mjs — the shortened digest and the verbatim pasteable flow-writer recovery
|
|
8
|
+
// command — is owned here: cores imports it, never the reverse, and the graph stays acyclic.
|
|
9
|
+
|
|
10
|
+
import { join, dirname } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import {
|
|
13
|
+
CHAIN_KIND, canonicalFlowDigest, authoritativeFlowRecords, flowTreeIdentity,
|
|
14
|
+
} from './flow-record.mjs';
|
|
15
|
+
import { resolveRecordReference } from './flow-store.mjs';
|
|
16
|
+
import { authoritativeOfKind, summarizeReviewReceiptsForTree } from './core-evidence.mjs';
|
|
17
|
+
import { FALLBACK_LENS_ADDITIONAL_ONLY } from './cheap-agents.mjs';
|
|
18
|
+
|
|
19
|
+
export const short = (digest) => `${digest.slice(0, 12)}…`;
|
|
20
|
+
|
|
21
|
+
// The verbatim pasteable recovery lane (Decision 3/8): every refusal that names a mintable record
|
|
22
|
+
// class prints the exact flow-writer command that mints it. The tool path is absolute (pasteable
|
|
23
|
+
// from any cwd) and POSIX single-quoted — raw path/id bytes must never execute on paste.
|
|
24
|
+
const FLOW_WRITER_TOOL = join(dirname(fileURLToPath(import.meta.url)), 'flow-writer.mjs');
|
|
25
|
+
export const shellQuote = (v) => `'${String(v).replaceAll("'", "'\\''")}'`;
|
|
26
|
+
export const writerCommand = (args) => `node ${shellQuote(FLOW_WRITER_TOOL)} ${args}`;
|
|
27
|
+
|
|
28
|
+
const isCanonicalInstant = (v) => typeof v === 'string' && Number.isFinite(Date.parse(v)) && new Date(v).toISOString() === v;
|
|
29
|
+
|
|
30
|
+
const hasOwnAdoption = (records, owner) =>
|
|
31
|
+
records.some((r) => r.kind === CHAIN_KIND && r.purpose === 'adoption' && r.owner === owner);
|
|
32
|
+
|
|
33
|
+
// #61: an unbroken declared-path delta chain lifts a stale receipt; each link consumes the
|
|
34
|
+
// refresh cap of the chain that minted its re-attestation.
|
|
35
|
+
export const classifyDeltaChain = ({ records, fromFingerprint, toFingerprint, declaredPaths, refreshCap }) => {
|
|
36
|
+
if (!Number.isInteger(refreshCap) || refreshCap < 1) {
|
|
37
|
+
return { classification: 'refused', reason: `the refresh cap must arrive as a positive-integer input (#45) — got ${JSON.stringify(refreshCap)} (fail closed)` };
|
|
38
|
+
}
|
|
39
|
+
const declared = Array.isArray(declaredPaths) ? declaredPaths : [];
|
|
40
|
+
const deltas = authoritativeFlowRecords(records).filter((r) => r.kind === 'bookkeeping-delta');
|
|
41
|
+
const links = [];
|
|
42
|
+
const consumers = new Map();
|
|
43
|
+
const visited = new Set([fromFingerprint]);
|
|
44
|
+
let tip = fromFingerprint;
|
|
45
|
+
while (tip !== toFingerprint) {
|
|
46
|
+
const candidates = deltas.filter((d) => d.fingerprintBefore === tip);
|
|
47
|
+
if (candidates.length === 0) {
|
|
48
|
+
return { classification: 'refused', reason: `no bookkeeping-delta continues the chain at ${short(tip)} — a gap never classifies CURRENT (fail closed)` };
|
|
49
|
+
}
|
|
50
|
+
if (candidates.length > 1) {
|
|
51
|
+
// FLOW-DELTA-FORK-NAMES-UNDECLARED: the declaredPaths restriction outranks the fork wording
|
|
52
|
+
// — a mixed pair's actionable fact is the undeclared path, not the fork (both lanes refuse).
|
|
53
|
+
const undeclared = candidates.filter((d) => !declared.includes(d.path));
|
|
54
|
+
if (undeclared.length > 0) {
|
|
55
|
+
return { classification: 'refused', reason: `bookkeeping-delta at ${undeclared.map((d) => d.path).join(', ')}: not a DECLARED bookkeeping path (declared: ${declared.join(', ') || 'none'}) — an undeclared-path delta never enters a classification chain, however valid its custody proof (fail closed)` };
|
|
56
|
+
}
|
|
57
|
+
return { classification: 'refused', reason: `${candidates.length} authoritative deltas fork the chain at ${short(tip)} — a fork never classifies CURRENT (fail closed)` };
|
|
58
|
+
}
|
|
59
|
+
const d = candidates[0];
|
|
60
|
+
if (!declared.includes(d.path)) {
|
|
61
|
+
return { classification: 'refused', reason: `bookkeeping-delta at ${d.path}: not a DECLARED bookkeeping path (declared: ${declared.join(', ') || 'none'}) — an undeclared-path delta never enters a classification chain, however valid its custody proof (fail closed)` };
|
|
62
|
+
}
|
|
63
|
+
const digest = canonicalFlowDigest(d);
|
|
64
|
+
const at = records.indexOf(d);
|
|
65
|
+
const attesting = records.find((s, j) => j > at && s.kind === CHAIN_KIND && s.purpose === 'refresh'
|
|
66
|
+
&& s.refreshedRecord === digest && s.fingerprintBefore === d.fingerprintAfter);
|
|
67
|
+
if (attesting === undefined) {
|
|
68
|
+
return { classification: 'refused', reason: `bookkeeping-delta at ${d.path}: no satisfying re-attestation — a mismatched or missing refresh link never carries the chain (fail closed)` };
|
|
69
|
+
}
|
|
70
|
+
const key = JSON.stringify([attesting.planId, attesting.cycle]);
|
|
71
|
+
consumers.set(key, (consumers.get(key) ?? 0) + 1);
|
|
72
|
+
links.push(d);
|
|
73
|
+
tip = d.fingerprintAfter;
|
|
74
|
+
if (visited.has(tip)) {
|
|
75
|
+
return { classification: 'refused', reason: `the delta chain revisits ${short(tip)} — a cycle never classifies CURRENT (fail closed)` };
|
|
76
|
+
}
|
|
77
|
+
visited.add(tip);
|
|
78
|
+
}
|
|
79
|
+
const attribution = [...consumers.entries()].map(([key, count]) => {
|
|
80
|
+
const [planId, cycle] = JSON.parse(key);
|
|
81
|
+
const refreshes = records.filter((r) => r.kind === CHAIN_KIND && r.purpose === 'refresh' && r.planId === planId && r.cycle === cycle).length;
|
|
82
|
+
return { planId, cycle, links: count, refreshes };
|
|
83
|
+
});
|
|
84
|
+
const exhausted = attribution.find((a) => a.refreshes > refreshCap);
|
|
85
|
+
if (exhausted !== undefined) {
|
|
86
|
+
return { classification: 'escalation', reason: `refresh cap exhausted for chain "${exhausted.planId}" (cycle ${exhausted.cycle}): ${exhausted.refreshes} refreshes exceed the cap ${refreshCap} (#45) — cap exhaustion escalates to a real round, never a silent pass` };
|
|
87
|
+
}
|
|
88
|
+
return { classification: 'current', links, attribution };
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// #56: only the authoritative head of the veto instance lifts, exact-matching the full bound set.
|
|
92
|
+
export const evaluateVetoOverride = ({ records, vetoReceipt, tree }) => {
|
|
93
|
+
const stands = (reason) => ({ lifted: false, reason });
|
|
94
|
+
const vetoDigest = canonicalFlowDigest(vetoReceipt);
|
|
95
|
+
const instance = records.filter((r) => r.kind === 'maintainer-override' && r.vetoReceiptDigest === vetoDigest);
|
|
96
|
+
if (instance.length === 0) {
|
|
97
|
+
if (!records.some((r) => r.kind === CHAIN_KIND && r.purpose === 'adoption')) return stands('the flow is unarmed (no adoption record) — the override arm is inert and the veto stands');
|
|
98
|
+
return stands(`a standing veto (backend "${vetoReceipt.backend}", verdict ${JSON.stringify(vetoReceipt.verdict)}) has no maintainer-override for its instance — degradation never lifts a veto (#48); only a checkpoint-approved override does`);
|
|
99
|
+
}
|
|
100
|
+
const head = instance[instance.length - 1];
|
|
101
|
+
const prefix = records.slice(0, records.indexOf(head));
|
|
102
|
+
if (!prefix.some((r) => r.kind === CHAIN_KIND && r.purpose === 'adoption')) {
|
|
103
|
+
return stands('the flow is unarmed at the override head (no adoption record precedes it) — a forward-referencing override never lifts and the veto stands');
|
|
104
|
+
}
|
|
105
|
+
const mismatch = (field, got, want) => stands(`the override head does not lift: bound-set mismatch on ${field} (override ${JSON.stringify(got)} ≠ ${JSON.stringify(want)}) — the evaluation exact-matches the full #56 bound set`);
|
|
106
|
+
if (head.backend !== vetoReceipt.backend) return mismatch('backend', head.backend, vetoReceipt.backend);
|
|
107
|
+
if (head.verdict !== vetoReceipt.verdict) return mismatch('verdict', head.verdict, vetoReceipt.verdict);
|
|
108
|
+
if (head.base !== tree.base) return mismatch('base', head.base, tree.base);
|
|
109
|
+
if (head.fingerprint !== tree.fingerprint) return mismatch('fingerprint', head.fingerprint, tree.fingerprint);
|
|
110
|
+
const target = resolveRecordReference(prefix, head.chainRecord);
|
|
111
|
+
if (target === undefined || target.kind !== CHAIN_KIND) {
|
|
112
|
+
return stands('the override head does not lift: bound-set mismatch on chainRecord — the digest does not resolve to a chain record PRECEDING the override (mint-time order decides)');
|
|
113
|
+
}
|
|
114
|
+
const chain = prefix.filter((r) => r.kind === CHAIN_KIND && r.planId === target.planId);
|
|
115
|
+
if (chain[0].purpose !== 'adoption') {
|
|
116
|
+
return stands('the override head does not lift: bound-set mismatch on chainRecord — the bound chain is not adopted');
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
lifted: true,
|
|
120
|
+
label: `veto lifted by maintainer-override ${short(canonicalFlowDigest(head))} — backend "${head.backend}" verdict "${head.verdict}" (checkpoint-approved, #38/#56)`,
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// #65: a current-base red passes only through a rerun-cause naming ITS attempt, matched to a
|
|
125
|
+
// later first-of-its-attempt completed retry; base correlation is flow-side and must be unambiguous.
|
|
126
|
+
// Scope (the Phase-4 veteran-store dogfood catch): a red final minted strictly BEFORE this
|
|
127
|
+
// worktree's EARLIEST adoption instant is OUTSIDE the rung — no flow record could carry its tree
|
|
128
|
+
// BY CONSTRUCTION, so demanding the correlation retroactively would brick arming over any
|
|
129
|
+
// pre-flow evidence history. Both instants are RECORDED record fields in the CANONICAL
|
|
130
|
+
// UTC ISO form (#39 — check-time wall-clock never enters, and Date.parse's tolerance for
|
|
131
|
+
// non-canonical spellings never widens the boundary): ANY own-adoption instant that is not
|
|
132
|
+
// canonical disables the exemption ENTIRELY (a silently dropped broken instant would move the
|
|
133
|
+
// boundary to a LATER adoption — fail open), and a red final exempts only on its own canonical
|
|
134
|
+
// strictly-earlier instant. Stated residual: the two stores are deliberately not lock-coupled
|
|
135
|
+
// and their records remain forgeable (each store's own header says so) — a backdated or
|
|
136
|
+
// backwards-clock instant can move a red across this boundary; the cross-store arming-fence
|
|
137
|
+
// hardening is queued (FLOW-ARMING-FENCE-CROSS-STORE), never pretended here.
|
|
138
|
+
//
|
|
139
|
+
// The lane split (FLOW-FINAL-RED-DEADLOCK) — the same reasoning the D10 arm already applies one arm
|
|
140
|
+
// away (see the `consumer` paragraph in computeFlowDecision's own header; a line reference there
|
|
141
|
+
// would rot on the next insertion). The strict rule demands a receipt the gate's OWN run has not
|
|
142
|
+
// written yet: run-gates appends the final receipt only AFTER every gate has run, so an in-matrix
|
|
143
|
+
// flow-check can never see the completed retry that would answer the newest red, and each --final
|
|
144
|
+
// mints red N+1 — no number of rerun-causes converges. On the 'gate' lane a current-base red is
|
|
145
|
+
// therefore ALSO answered by a provable IN-PROGRESS retry: an authoritative rerun-cause naming its
|
|
146
|
+
// attempt AND binding the CURRENT fingerprint, plus a final-start at that fingerprint ordered
|
|
147
|
+
// STRICTLY AFTER that red whose attempt carries no completed final — the shape run-gates creates
|
|
148
|
+
// before any gate runs (run-gates.mjs:636-658), so inside a real final run the conjunction holds by
|
|
149
|
+
// construction while a standalone check on a quiet tree still refuses. The relaxation carries the
|
|
150
|
+
// SAME base correlation the strict rule demands of the retry tree: a fingerprint is not unique to a
|
|
151
|
+
// base (a clean tree hashes identically under every HEAD), so without it a cause minted at another
|
|
152
|
+
// base would answer the gate and leave a green final the commit boundary is guaranteed to reject —
|
|
153
|
+
// the relaxation must only ever admit a state a completed retry could actually clear. It is opt-in
|
|
154
|
+
// by EXACT match: any other consumer — unknown, misspelled, absent — evaluates the strict rule, and
|
|
155
|
+
// a tree whose fingerprint is unresolvable or ambiguously correlated never relaxes. The
|
|
156
|
+
// 'commit-guard' lane is unchanged; the commit boundary still demands a real completed retry.
|
|
157
|
+
// Stated residual: an INTERRUPTED final run leaves exactly that record shape with no live run
|
|
158
|
+
// behind it, so a standalone check reads PASS in that window. It authorizes nothing — commit-guard
|
|
159
|
+
// refuses the dangling start independently (commit-guard.mjs:213-223) and consults the STRICT lane
|
|
160
|
+
// (commit-guard.mjs:253) — and the window closes on the next completed final run. The real fix is a
|
|
161
|
+
// runner-attested capability (a one-time unpublished nonce over stdin or an inherited FD, verified
|
|
162
|
+
// against a one-way commitment recorded in the final-start); it needs its own IPC contract and is
|
|
163
|
+
// QUEUED, never pretended here.
|
|
164
|
+
export const collectUnansweredRedRefusals = ({ flowRecords, coreRecords, currentBase, owner, consumer = 'commit-guard', currentFingerprint = null }) => {
|
|
165
|
+
if (!hasOwnAdoption(flowRecords, owner)) return [];
|
|
166
|
+
const adoptionInstants = flowRecords
|
|
167
|
+
.filter((r) => r.kind === CHAIN_KIND && r.purpose === 'adoption' && r.owner === owner)
|
|
168
|
+
.map((r) => (isCanonicalInstant(r.timestamp) ? Date.parse(r.timestamp) : null));
|
|
169
|
+
const armingInstant = adoptionInstants.length > 0 && adoptionInstants.every((t) => t !== null)
|
|
170
|
+
? Math.min(...adoptionInstants) : null;
|
|
171
|
+
const refusals = [];
|
|
172
|
+
const identities = flowRecords.map(flowTreeIdentity);
|
|
173
|
+
const basesAt = (fp) => [...new Set(identities.filter((t) => t.fingerprint === fp).map((t) => t.base))];
|
|
174
|
+
const rerunCauses = authoritativeFlowRecords(flowRecords).filter((r) => r.kind === 'rerun-cause');
|
|
175
|
+
const finals = coreRecords.map((r, i) => ({ r, i })).filter(({ r }) => r.kind === 'final');
|
|
176
|
+
const firstFinalByAttempt = new Map();
|
|
177
|
+
for (const { r, i } of finals) {
|
|
178
|
+
if (!firstFinalByAttempt.has(r.attempt)) firstFinalByAttempt.set(r.attempt, i);
|
|
179
|
+
}
|
|
180
|
+
const answeredBy = (red, redAt) => rerunCauses.some((c) => c.attempt === red.attempt
|
|
181
|
+
&& finals.some(({ r: g, i: gi }) => gi > redAt
|
|
182
|
+
&& basesAt(g.fingerprintBefore).length === 1 && basesAt(g.fingerprintBefore)[0] === currentBase
|
|
183
|
+
&& firstFinalByAttempt.get(g.attempt) === gi
|
|
184
|
+
&& c.fingerprint === g.fingerprintBefore));
|
|
185
|
+
const completedAttempts = new Set(finals.map(({ r }) => r.attempt));
|
|
186
|
+
const relaxes = consumer === 'gate' && typeof currentFingerprint === 'string' && currentFingerprint !== ''
|
|
187
|
+
&& basesAt(currentFingerprint).length === 1 && basesAt(currentFingerprint)[0] === currentBase;
|
|
188
|
+
const inProgressRetryFor = (red, redAt) => rerunCauses.some((c) => c.attempt === red.attempt
|
|
189
|
+
&& c.fingerprint === currentFingerprint
|
|
190
|
+
&& coreRecords.some((s, si) => si > redAt && s.kind === 'final-start'
|
|
191
|
+
&& s.fingerprint === currentFingerprint && !completedAttempts.has(s.attempt)));
|
|
192
|
+
for (const { r, i } of finals) {
|
|
193
|
+
if (r.status !== 'red') continue;
|
|
194
|
+
if (armingInstant !== null && isCanonicalInstant(r.timestamp) && Date.parse(r.timestamp) < armingInstant) continue;
|
|
195
|
+
const bases = basesAt(r.fingerprintBefore);
|
|
196
|
+
if (bases.length === 0) {
|
|
197
|
+
refusals.push(`a red final (attempt "${r.attempt}") cannot be base-correlated: no flow record carries its tree fingerprint ${short(r.fingerprintBefore)} — the zero-base lane is a fail-closed ambiguity (#65); the rung demands exactly ONE base through the flow store`);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (bases.length > 1) {
|
|
201
|
+
refusals.push(`a red final (attempt "${r.attempt}") resolves ${bases.length} distinct bases through the flow store — a multi-base correlation is a fail-closed ambiguity (#65); one tree fingerprint must carry exactly ONE base`);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (bases[0] !== currentBase) continue;
|
|
205
|
+
if (!answeredBy(r, i) && !(relaxes && inProgressRetryFor(r, i))) {
|
|
206
|
+
refusals.push(`a red final (attempt "${r.attempt}") on the CURRENT base (${bases[0] == null ? 'null' : short(bases[0])}) has no later completed retry cleared by a rerun-cause — an unanswered red never passes an armed flow (#65). recovery (edit the quoted cause, then paste; mint on the RETRY tree): ${writerCommand(`rerun-cause --attempt=${shellQuote(r.attempt)} --cause='<the stated cause>'`)}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return refusals;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// collectDegradeCoverageRefusals (#25/#39): every authoritative core degrade at the current tree
|
|
213
|
+
// must be justified by a flow degrade-justification binding {downMark, degradeDigest, base} to a
|
|
214
|
+
// then-active mark of the same backend. All instants are RECORDED and canonical — wall-clock never
|
|
215
|
+
// enters the decision.
|
|
216
|
+
// #25/#42: every relied-on-backend degrade at the tree needs ONE fully-valid justification.
|
|
217
|
+
export const collectDegradeCoverageRefusals = ({ flowRecords, coreRecords, tree, owner, backends }) => {
|
|
218
|
+
if (!hasOwnAdoption(flowRecords, owner)) return [];
|
|
219
|
+
const refusals = [];
|
|
220
|
+
const justifications = authoritativeFlowRecords(flowRecords).filter((r) => r.kind === 'degrade-justification');
|
|
221
|
+
const justificationFailure = (j, degrade) => {
|
|
222
|
+
if (j.base !== tree.base) {
|
|
223
|
+
return `the degrade-justification for backend "${degrade.backend}" binds base ${j.base == null ? 'null' : short(j.base)}, not the current base — the {downMark, degradeDigest, base} binding is exact (#25)`;
|
|
224
|
+
}
|
|
225
|
+
if (j.fingerprint !== tree.fingerprint) {
|
|
226
|
+
return `the degrade-justification for backend "${degrade.backend}" was minted at another tree (fingerprint ${short(j.fingerprint)}) — the per-{base, fingerprint} binding is exact (#25)`;
|
|
227
|
+
}
|
|
228
|
+
if (!isCanonicalInstant(j.timestamp)) {
|
|
229
|
+
return `the degrade-justification for backend "${degrade.backend}" carries an unparseable instant ${JSON.stringify(j.timestamp)} — the decide layer requires a canonical UTC ISO instant (toISOString round-trip), by name (#39)`;
|
|
230
|
+
}
|
|
231
|
+
const jAt = flowRecords.indexOf(j);
|
|
232
|
+
const mark = resolveRecordReference(flowRecords.slice(0, jAt), j.downMark);
|
|
233
|
+
if (mark === undefined || mark.kind !== 'down-mark' || mark.backend !== degrade.backend) {
|
|
234
|
+
return `the degrade-justification for backend "${degrade.backend}" does not ride a down-mark of that backend (${mark === undefined ? 'the downMark digest resolves to no EARLIER record — mint-time order decides' : `it targets a ${mark.kind} of backend "${mark.backend}"`}) — a mis-bound justification refuses (#25)`;
|
|
235
|
+
}
|
|
236
|
+
const closedBefore = flowRecords.some((c, k) => k < jAt && (c.kind === 'down-mark-up' || c.kind === 'down-mark-clear') && c.target === j.downMark);
|
|
237
|
+
if (closedBefore) {
|
|
238
|
+
return `the degrade-justification for backend "${degrade.backend}" rides a down-mark already closed by up/clear at mint time — a closed mark justifies nothing (#25)`;
|
|
239
|
+
}
|
|
240
|
+
if (!(Date.parse(j.timestamp) >= Date.parse(mark.timestamp) && Date.parse(j.timestamp) < Date.parse(mark.expiresAt))) {
|
|
241
|
+
return `the degrade-justification for backend "${degrade.backend}" was minted outside its down-mark's active window (an expired-at-mint or pre-mark instant) — a then-unexpired mark is required (#25), never wall-clock at check time (#39)`;
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
};
|
|
245
|
+
for (const degrade of authoritativeOfKind(coreRecords, 'degrade')) {
|
|
246
|
+
if (degrade.fingerprint !== tree.fingerprint || !backends.includes(degrade.backend)) continue;
|
|
247
|
+
const digest = canonicalFlowDigest(degrade);
|
|
248
|
+
const candidates = justifications.filter((x) => x.degradeDigest === digest);
|
|
249
|
+
if (candidates.length === 0) {
|
|
250
|
+
refusals.push(`a core degrade (backend "${degrade.backend}") the gate relies on at the current tree has no flow degrade-justification binding it — exact coverage refuses uncovered degrades on an armed flow (#25/#42). recovery (pasteable, needs a then-active down-mark): ${writerCommand(`degrade-justification --backend=${shellQuote(degrade.backend)}`)}`);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const failures = candidates.map((j) => justificationFailure(j, degrade));
|
|
254
|
+
if (!failures.includes(null)) refusals.push(failures[0]);
|
|
255
|
+
}
|
|
256
|
+
return refusals;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// evaluateInternalAttestationLenses (#15/#3, Phase 4.3): an internal-attestation whose lens set
|
|
260
|
+
// CLAIMS a review provider's slot (a lens named like a configured backend) must ride a
|
|
261
|
+
// THEN-ACTIVE down-mark for that backend — substitution is recorded, never silent; the refusal
|
|
262
|
+
// quotes the fallback lens's additional-only contract from its one home. "Then-active" is decided
|
|
263
|
+
// in the RAW prefix strictly before the attestation (mint-time order, the #25 discipline): the
|
|
264
|
+
// mark is unclosed there and its TTL window contains the attestation's instant — an unparseable
|
|
265
|
+
// attestation instant refuses by name, never passes.
|
|
266
|
+
export const evaluateInternalAttestationLenses = ({ record, records, providerBackends }) => {
|
|
267
|
+
const at = records.indexOf(record);
|
|
268
|
+
if (at === -1) {
|
|
269
|
+
return { ok: false, reason: 'the internal-attestation record does not belong to the supplied record list — prefix scoping is undecidable (fail closed)' };
|
|
270
|
+
}
|
|
271
|
+
const prefix = records.slice(0, at);
|
|
272
|
+
for (const lens of record.lenses) {
|
|
273
|
+
if (!providerBackends.includes(lens)) continue;
|
|
274
|
+
let active = null;
|
|
275
|
+
for (const r of prefix) {
|
|
276
|
+
if (r.kind === 'down-mark' && r.backend === lens) active = r;
|
|
277
|
+
else if ((r.kind === 'down-mark-up' || r.kind === 'down-mark-clear') && r.backend === lens) active = null;
|
|
278
|
+
}
|
|
279
|
+
const failure = active === null
|
|
280
|
+
? `no down-mark for backend "${lens}" is open at the attestation's position`
|
|
281
|
+
: !isCanonicalInstant(record.timestamp)
|
|
282
|
+
? `the attestation instant ${JSON.stringify(record.timestamp)} is not a canonical UTC ISO instant, so then-activity is undecidable`
|
|
283
|
+
: !(Date.parse(record.timestamp) >= Date.parse(active.timestamp) && Date.parse(record.timestamp) < Date.parse(active.expiresAt))
|
|
284
|
+
? `the down-mark for backend "${lens}" is outside its active window at the attestation's instant`
|
|
285
|
+
: null;
|
|
286
|
+
if (failure !== null) {
|
|
287
|
+
return { ok: false, reason: `the internal-attestation's lens set claims backend "${lens}"'s slot without a then-active down-mark (${failure}) — ${FALLBACK_LENS_ADDITIONAL_ONLY} (fail closed)` };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return { ok: true };
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// The ONE relied-on receipt selector (#42/#61): the latest normal receipt at the CURRENT tree,
|
|
294
|
+
// else — through an unbroken declared-path bookkeeping-delta chain — the backend's LAST receipt
|
|
295
|
+
// judged at its own tree, carrying the lift metadata the PASS labels consume.
|
|
296
|
+
export const selectReliedOnReceipt = ({ receipts, backend, tree, records, declaredPaths, refreshCap }) => {
|
|
297
|
+
const own = receipts.filter((r) => r.backend === backend);
|
|
298
|
+
const current = summarizeReviewReceiptsForTree(own, tree.fingerprint);
|
|
299
|
+
if (current.state === 'current') return { receipt: current.receipt, lifted: 0 };
|
|
300
|
+
const last = own[own.length - 1];
|
|
301
|
+
const candidateFp = typeof last?.fingerprint === 'string' ? last.fingerprint : null;
|
|
302
|
+
if (candidateFp == null || candidateFp === tree.fingerprint) return { receipt: null, lifted: 0 };
|
|
303
|
+
const atCandidate = summarizeReviewReceiptsForTree(own, candidateFp);
|
|
304
|
+
if (atCandidate.state !== 'current') return { receipt: null, lifted: 0 };
|
|
305
|
+
const chain = classifyDeltaChain({ records, fromFingerprint: candidateFp, toFingerprint: tree.fingerprint, declaredPaths, refreshCap });
|
|
306
|
+
if (chain.classification !== 'current') return { receipt: null, lifted: 0 };
|
|
307
|
+
return { receipt: atCandidate.receipt, lifted: chain.links.length };
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// #42: each relied-on backend's selected receipt must ride an OWN round's dispatch ledger; with
|
|
311
|
+
// lift inputs supplied the selection spans the delta lift, so a LIFTED receipt demands its
|
|
312
|
+
// binding at ITS OWN fingerprint; the entry's dispatchBase must equal the round's recorded base.
|
|
313
|
+
export const collectReceiptCoverageRefusals = ({ flowRecords, receipts, tree, owner, backends, declaredPaths = null, refreshCap = null }) => {
|
|
314
|
+
if (!hasOwnAdoption(flowRecords, owner)) return [];
|
|
315
|
+
const refusals = [];
|
|
316
|
+
const rounds = authoritativeFlowRecords(flowRecords).filter((r) => r.kind === CHAIN_KIND && r.purpose === 'round' && r.owner === owner);
|
|
317
|
+
for (const backend of [...new Set(backends)]) {
|
|
318
|
+
const relied = declaredPaths != null
|
|
319
|
+
? selectReliedOnReceipt({ receipts, backend, tree, records: flowRecords, declaredPaths, refreshCap }).receipt
|
|
320
|
+
: summarizeReviewReceiptsForTree(receipts.filter((r) => r.backend === backend), tree.fingerprint).receipt;
|
|
321
|
+
if (relied == null) continue;
|
|
322
|
+
const digest = canonicalFlowDigest(relied);
|
|
323
|
+
const covered = rounds.some((round) => round.fingerprint === relied.fingerprint
|
|
324
|
+
&& round.dispatches.some((e) => e.receiptDigest === digest && e.backend === relied.backend && e.dispatchBase === round.base));
|
|
325
|
+
if (!covered) {
|
|
326
|
+
refusals.push(`the review receipt the decision relies on (backend "${backend}", verdict ${JSON.stringify(relied.verdict)}) is bound by NO round dispatch-ledger entry of this worktree's chains — an unbound receipt never satisfies an armed flow (#42); land the {receiptDigest, backend, dispatchBase} entry through a round revision`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return refusals;
|
|
330
|
+
};
|