@sabaiway/agent-workflow-kit 14.4.1 → 14.4.3

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.
@@ -34,1138 +34,30 @@
34
34
  // security boundary); coverage/green verification of red-proof records lives in the final-run
35
35
  // checker, not here. Dependency-free. No side effects on import.
36
36
 
37
- import { readFileSync, lstatSync, realpathSync, readlinkSync, openSync, readSync, closeSync } from 'node:fs';
38
- import { join, dirname, normalize, sep, basename } from 'node:path';
39
- import { spawnSync } from 'node:child_process';
40
- import { createHash } from 'node:crypto';
41
- import { writeContainedFileAtomic } from './atomic-write.mjs';
42
- import { parsePositiveIntKnob, probeVerdict } from './changed-surface.mjs';
43
37
  import { isDirectRun } from './direct-run.mjs';
44
- import { readRegularFileNoFollow } from './fs-read-nofollow.mjs';
45
- import { lexicalRepoRelative } from './repo-lex.mjs';
46
- import { GIT_MAX_BUFFER, resolveGitLocation, withGitPath } from './git-env.mjs';
47
- // The coverage vocabulary leaf: run-gates RECORDS the token this validator checks, and run-gates
48
- // imports THIS module (the sole-writer boundary), so their shared home sits below both.
49
- import { COVERAGE, FINAL_COVERAGE_STATES } from './coverage-state.mjs';
50
-
51
- export const CORE_EVIDENCE_STOP = 'CORE_EVIDENCE_STOP';
52
- const stop = (message) => Object.assign(new Error(`[agent-workflow-kit] ${message}`), { name: 'CoreEvidenceStop', code: CORE_EVIDENCE_STOP });
53
- const usageFail = (message) => Object.assign(new Error(`[agent-workflow-kit] ${message}`), { exitCode: 2 });
54
-
55
- const isoNow = () => new Date().toISOString();
56
-
57
- const gitRaw = (args, cwd) => spawnSync('git', args, { cwd, maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
58
-
59
- // Exported so a consumer computing over the SAME domain (the exec metric producer) reads git through
60
- // this one runner instead of growing a second spawnSync wrapper with its own buffer bound.
61
- export const gitBuf = (args, cwd) => {
62
- const r = gitRaw(args, cwd);
63
- if (r.error || r.status !== 0) return null;
64
- return r.stdout;
65
- };
66
-
67
- const gitLine = (args, cwd) => {
68
- const buf = gitBuf(args, cwd);
69
- return buf == null ? null : buf.toString('utf8').replace(/\r?\n$/, '');
70
- };
71
-
72
- // ── the canonical review-domain primitives (ONE home; review-state re-exports its public API) ─────
73
- // The uncommitted-state identity + the receipt read path + the attesting predicate live HERE — the
74
- // DAG bottom every checker (review-state gate, ledger cross-check, this module's summary) imports
75
- // from, so no two consumers can ever disagree about what a fingerprint or an attestation is.
76
-
77
- // resolveBase(cwd) → the current HEAD commit sha, or null on an unborn branch / outside a git tree.
78
- export const resolveBase = (cwd) => gitLine(['rev-parse', '--verify', '--quiet', 'HEAD'], cwd);
79
-
80
- // testId FORMAT: "<repo-relative test file>#<test-name-pattern>" — a "#" separator with BOTH halves
81
- // non-empty. Format-only (no suffix rule); resolvability is the probe's job.
82
- const TESTID_SEPARATOR = '#';
83
- export const isWellFormedTestId = (v) => {
84
- if (typeof v !== 'string') return false;
85
- const at = v.indexOf(TESTID_SEPARATOR);
86
- return at > 0 && at < v.length - 1;
87
- };
88
- export const splitTestId = (v) => {
89
- const at = v.indexOf(TESTID_SEPARATOR);
90
- return { file: v.slice(0, at), pattern: v.slice(at + 1) };
91
- };
92
-
93
- // First 8 KiB contain a NUL byte → binary (git's own heuristic; mirrors the wrappers' is_binary
94
- // bash twin). An unreadable path reads as text (the fail-safe arm — the payload walk then surfaces
95
- // its own read failure).
96
- export const isBinaryFile = (path) => {
97
- let fd;
98
- try {
99
- fd = openSync(path, 'r');
100
- const buf = Buffer.alloc(8192);
101
- const n = readSync(fd, buf, 0, 8192, 0);
102
- return buf.subarray(0, n).includes(0);
103
- } catch {
104
- return false;
105
- } finally {
106
- if (fd !== undefined) closeSync(fd);
107
- }
108
- };
109
-
110
- // The never-committable untracked stat classes: character/block devices, FIFOs, sockets — excluded
111
- // from the ENTIRE review domain (fingerprint payload, clean check); lstat-keyed because a lying
112
- // dirent is exactly how sandbox masks surface.
113
- export const isNeverCommittableStat = (stat) =>
114
- stat != null &&
115
- (stat.isCharacterDevice() || stat.isBlockDevice() || stat.isFIFO() || stat.isSocket());
116
-
117
- // The git runner seam the tree computations share — (args, dir, input, env) — and its guard: a
118
- // runner that THROWS answers as a failed spawn (measured: a raw stack out of a fingerprint).
119
- const defaultRunGit = (args, dir, input, env) => spawnSync('git', args, { cwd: dir, input, env: withGitPath(env ?? process.env), maxBuffer: GIT_MAX_BUFFER, windowsHide: true });
120
- const guardedRunner = (runGit, env) => (args, dir, input, override) => {
121
- try { return runGit(args, dir, input, override ?? env); } catch (err) { return { error: err, status: null, signal: null, stdout: Buffer.alloc(0), stderr: '' }; }
122
- };
123
- const stdoutOf = (r) => (r.error || r.status !== 0 ? null : r.stdout);
124
- // Only a work-tree location is computed over (git-env's table under the runner's own env) — a redirecting GIT_DIR never hashes another repository in silence.
125
- const locateWorkTree = (cwd, run, env) => {
126
- const location = resolveGitLocation(cwd, { spawn: (cmd, args, opts) => run(args, opts.cwd, undefined, opts.env), env });
127
- return location.state === 'work-tree' ? location.top : null;
128
- };
129
-
130
- // The canonical payload bytes: staged diff + unstaged diff + the untracked-not-ignored section —
131
- // byte-identical to the wrappers' emit_fingerprint_payload, emitted from the work-tree ROOT. Null on
132
- // every location but a work tree. lstat is injectable ONLY for the never-committable filter tests.
133
- export const FINGERPRINT_CACHED_DIFF_ARGV = Object.freeze(['diff', '--cached', '--no-ext-diff', '--no-textconv', '--ignore-submodules=none']);
134
- export const FINGERPRINT_UNSTAGED_DIFF_ARGV = Object.freeze(['diff', '--no-ext-diff', '--no-textconv', '--ignore-submodules=none']);
135
- export const computeFingerprintPayload = (cwd, { lstat = lstatSync, runGit = null, env = process.env } = {}) => {
136
- const run = guardedRunner(runGit ?? defaultRunGit, env);
137
- const top = locateWorkTree(cwd, run, env);
138
- if (top == null) return null;
139
- const buf = (args) => stdoutOf(run(args, top));
140
- const staged = buf(FINGERPRINT_CACHED_DIFF_ARGV);
141
- const unstaged = buf(FINGERPRINT_UNSTAGED_DIFF_ARGV);
142
- const untrackedZ = buf(['ls-files', '--others', '--exclude-standard', '-z']);
143
- if (staged == null || unstaged == null || untrackedZ == null) return null;
144
- const chunks = [staged, unstaged];
145
- for (const rel of untrackedZ.toString('utf8').split('\0').filter(Boolean)) {
146
- const full = join(top, rel);
147
- let stat = null;
148
- try {
149
- stat = lstat(full);
150
- } catch {
151
- stat = null;
152
- }
153
- if (isNeverCommittableStat(stat)) continue;
154
- if (stat?.isSymbolicLink()) {
155
- let target = '?';
156
- try {
157
- target = readlinkSync(full);
158
- } catch {
159
- target = '?';
160
- }
161
- chunks.push(Buffer.from(`untracked-symlink:${rel} -> ${target}\n`));
162
- } else if (!stat?.isFile()) {
163
- chunks.push(Buffer.from(`untracked-nonregular:${rel}\n`));
164
- } else if (isBinaryFile(full)) {
165
- chunks.push(Buffer.from(`untracked-binary:${rel}\n`));
166
- } else {
167
- chunks.push(Buffer.from(`untracked:${rel}\n`));
168
- chunks.push(readFileSync(full));
169
- }
170
- }
171
- return Buffer.concat(chunks);
172
- };
173
-
174
- // sha256 hex of the canonical payload, or null outside a git work tree.
175
- export const computeTreeFingerprint = (cwd, fsx) => {
176
- const payload = computeFingerprintPayload(cwd, fsx);
177
- return payload == null ? null : createHash('sha256').update(payload).digest('hex');
178
- };
179
-
180
- // The fingerprint of a CONTENT-FREE payload — a clean work tree emits no bytes at all, so this ONE
181
- // value is shared by every clean moment of every repository. It therefore identifies no working
182
- // state and correlates to no base: evidence found at it was minted by some other clean moment,
183
- // possibly at another base, and can decide nothing in either direction. Two situations reach it,
184
- // and only the INDEX tells them apart (never the payload): an empty commit, where the index equals
185
- // HEAD and no byte enters the repository, and staged content the payload cannot see — a gitlink
186
- // hidden from `git diff` by an ignore configuration. Read by the consumers that correlate a
187
- // fingerprint to a base (flow-check-rungs.mjs #65) and by commit-guard's two content-free lanes.
188
- export const CONTENT_FREE_FINGERPRINT = createHash('sha256').update(Buffer.alloc(0)).digest('hex');
189
-
190
- // The index↔worktree split the fingerprint deliberately CANNOT see: the payload above concatenates
191
- // the staged and unstaged diffs, so against an otherwise-empty index a hunk moving into the index
192
- // leaves it byte-identical — while `git commit` builds the commit from the INDEX alone. This is the
193
- // ONE computation of that split; isTreeClean and the commit guard's index-lag arm both read it, so
194
- // they can never disagree about what "the index carries the verified tree" means. Submodule paths
195
- // are separated because a root-level `git add -A` cannot reach a submodule's own worktree, so they
196
- // need their own recovery. Anchored at the work-tree ROOT (ls-files is cwd-scoped); null when not
197
- // decidable — every consumer treats that as fail-closed, never as clean.
198
- const LS_FILES_TAG_SKIP_WORKTREE = 'S';
199
- const GITLINK_MODE = '160000';
200
- const SYMLINK_MODE = '120000';
201
- const EXECUTABLE_MODE = '100755';
202
- const OWNER_EXECUTE_BIT = 0o100;
203
- // `ls-files -v` lowercases the tag for assume-unchanged; skip-worktree is `S`, and an entry
204
- // carrying BOTH bits prints lowercase `s` (live-pinned by test). The skip test is therefore
205
- // case-INSENSITIVE — a case-sensitive one would lose skip-worktree on such an entry and turn a
206
- // legitimate sparse checkout into an endless refusal.
207
- const isAssumeUnchangedTag = (tag) => tag >= 'a' && tag <= 'z';
208
- const isSkipWorktreeTag = (tag) => tag.toUpperCase() === LS_FILES_TAG_SKIP_WORKTREE;
209
-
210
- // `git diff` SKIPS index entries carrying skip-worktree or assume-unchanged, so such a path can
211
- // hold worktree bytes the gates read while `git commit` takes the stale INDEX blob — the same
212
- // capture blindness one layer down, and invisible to the plain probe. Those entries are therefore
213
- // compared DIRECTLY against the worktree. A MISSING skip-worktree path is an ordinary sparse
214
- // checkout and never a lag; a missing assume-unchanged path is one. Gitlinks belong to the
215
- // submodule lane. Any probe that cannot answer counts the path as lagging (fail-safe).
216
- const flaggedIndexLag = (top, runGit, lstat, readlink) => {
217
- const buf = (args) => {
218
- const r = runGit(args, top);
219
- return r.error || r.status !== 0 ? null : r.stdout;
220
- };
221
- const splitZ = (b) => b.toString('utf8').split('\0').filter(Boolean);
222
- // Git emits raw path BYTES. Decoding to UTF-8 first turns a name carrying invalid bytes into a
223
- // DIFFERENT path, whose lstat then answers ENOENT — which for a skip-worktree entry reads as a
224
- // de-materialised sparse path and lets a stale index walk through. So each record keeps its
225
- // original slice and a name that does not survive a byte round-trip is lagging by construction.
226
- const splitZBytes = (b) => {
227
- const out = [];
228
- let start = 0;
229
- for (let i = 0; i < b.length; i += 1) {
230
- if (b[i] !== 0) continue;
231
- if (i > start) out.push(b.subarray(start, i));
232
- start = i + 1;
233
- }
234
- if (start < b.length) out.push(b.subarray(start));
235
- return out;
236
- };
237
- const decodesExactly = (slice) => Buffer.from(slice.toString('utf8'), 'utf8').equals(slice);
238
- const taggedZ = buf(['ls-files', '-v', '-z']);
239
- if (taggedZ == null) return null;
240
- // The two bits are INDEPENDENT — an entry can carry both (which is what lowercases the `S`), so
241
- // they travel as a pair and the recovery clears whichever are actually set.
242
- const flagged = splitZBytes(taggedZ)
243
- .map((record) => {
244
- const tag = record.subarray(0, 1).toString('utf8');
245
- const pathBytes = record.subarray(2);
246
- return {
247
- rel: pathBytes.toString('utf8'),
248
- pathBytes,
249
- exactName: decodesExactly(pathBytes),
250
- skipWorktree: isSkipWorktreeTag(tag),
251
- assumeUnchanged: isAssumeUnchangedTag(tag),
252
- };
253
- })
254
- .filter(({ skipWorktree, assumeUnchanged }) => skipWorktree || assumeUnchanged);
255
- if (flagged.length === 0) return { paths: [], submodules: [], flagged: [] };
256
- const stagedZ = buf(['ls-files', '-s', '-z']);
257
- if (stagedZ == null) return null;
258
- const entries = new Map();
259
- for (const line of splitZ(stagedZ)) {
260
- const tab = line.indexOf('\t');
261
- if (tab === -1) continue;
262
- const [mode, oid] = line.slice(0, tab).split(' ');
263
- entries.set(line.slice(tab + 1), { mode, oid });
264
- }
265
- // `git diff` honours core.fileMode; mirroring it keeps a false-mode host (WSL, network mounts)
266
- // from reading every executable bit as a lag. Exit 1 is the only "unset" (git's default is true);
267
- // any other failure leaves the comparison undecidable rather than guessing.
268
- const boolConfig = (key) => {
269
- const r = runGit(['config', '--type=bool', '--get', key], top);
270
- if (r.error || (r.status !== 0 && r.status !== 1)) return null; // undecidable — never guessed
271
- return r.status === 1 || r.stdout.toString('utf8').trim() !== 'false'; // exit 1 = unset = git's default true
272
- };
273
- const honoursFileMode = boolConfig('core.fileMode');
274
- // On a host without symlink support git materialises a symlink as a REGULAR FILE holding the
275
- // target bytes; demanding a real link there would refuse forever.
276
- const materialisesSymlinks = boolConfig('core.symlinks');
277
- if (honoursFileMode === null || materialisesSymlinks === null) return null;
278
- const lagging = [];
279
- const laggingSubmodules = [];
280
- const laggingFlags = [];
281
- for (const { rel, pathBytes, exactName, skipWorktree, assumeUnchanged } of flagged) {
282
- const entry = entries.get(rel);
283
- if (entry === undefined) continue;
284
- const isGitlink = entry.mode === GITLINK_MODE;
285
- // Every path this loop reports needs its index bits cleared before ANY staging command can
286
- // pick it up — `git add -A` alone is a recovery that silently does nothing here.
287
- const lag = () => {
288
- (isGitlink ? laggingSubmodules : lagging).push(rel);
289
- laggingFlags.push({ rel, skipWorktree, assumeUnchanged, exactName });
290
- };
291
- // Probed by RAW BYTES, never by the decoded name: a lossy decode addresses a DIFFERENT path,
292
- // and answering ENOENT for it would either wave a stale index through or — worse — call a
293
- // legitimately absent sparse entry "lagging", whose prescribed bit-clear plus `git add -A`
294
- // would then stage its DELETION.
295
- const absPath = Buffer.concat([Buffer.from(top), Buffer.from(sep), pathBytes]);
296
- let stat = null;
297
- let absent = false;
298
- try {
299
- stat = lstat(absPath);
300
- } catch (err) {
301
- // ONLY a genuine absence is the sparse-checkout case; EACCES / EIO leave the path unproven,
302
- // and an unproven path can never be waved through as "not materialised".
303
- absent = err != null && err.code === 'ENOENT';
304
- }
305
- if (stat === null) {
306
- if (!absent || !skipWorktree) lag();
307
- continue;
308
- }
309
- if (!exactName) {
310
- // Materialised, but the name cannot be addressed through an argv string, so it can never be
311
- // PROVEN current — fail-safe. Absence was already decided above, on the real bytes.
312
- lag();
313
- continue;
314
- }
315
- // A flagged GITLINK is hidden from `git diff` too, so the submodule lane would never see it —
316
- // and it is deliberately NOT proven current here. Three consecutive review rounds each found a
317
- // new way for a nested probe to answer "clean" wrongly (inherited superproject GIT_*, status
318
- // config blindness, the submodule's OWN flagged entries, a symlink standing in for the
319
- // directory). The set was not shrinking, so the verdict is REDUCTION rather than another patch:
320
- // a materialised flagged gitlink LAGS by construction. It is a refusal, never an endless one —
321
- // the printed recovery (clear the bit, then git add -A) converges, and an UNflagged submodule
322
- // is unaffected because the ordinary --ignore-submodules=none probe still judges it.
323
- if (isGitlink) {
324
- lag();
325
- continue;
326
- }
327
- if (entry.mode === SYMLINK_MODE && !stat.isSymbolicLink() && !materialisesSymlinks) {
328
- // The placeholder file's RAW bytes are the stored target — no filters ever apply to a link.
329
- const placeholder = runGit(['hash-object', '--no-filters', '-t', 'blob', '--', join(top, rel)], top);
330
- if (placeholder.error || placeholder.status !== 0 || placeholder.stdout.toString('utf8').trim() !== entry.oid) lag();
331
- continue;
332
- }
333
- if (entry.mode === SYMLINK_MODE || stat.isSymbolicLink()) {
334
- if (entry.mode !== SYMLINK_MODE || !stat.isSymbolicLink()) {
335
- lag();
336
- continue;
337
- }
338
- let target = null;
339
- try {
340
- target = readlink(join(top, rel));
341
- } catch {
342
- target = null;
343
- }
344
- if (target === null) {
345
- lag(); // an unreadable link can never be proven current — fail-safe
346
- continue;
347
- }
348
- const hashed = runGit(['hash-object', '-t', 'blob', '--stdin'], top, target);
349
- if (hashed.error || hashed.status !== 0 || hashed.stdout.toString('utf8').trim() !== entry.oid) lag();
350
- continue;
351
- }
352
- if (!stat.isFile()) {
353
- lag();
354
- continue;
355
- }
356
- // Git canonicalises the executable mode on the OWNER bit alone — a file with only group/other
357
- // exec set is still `100644` to git, so testing 0o111 would call a stale index current.
358
- if (honoursFileMode && (entry.mode === EXECUTABLE_MODE) !== ((stat.mode & OWNER_EXECUTE_BIT) !== 0)) {
359
- lag();
360
- continue;
361
- }
362
- const hashed = runGit(['hash-object', '--path', rel, '--', join(top, rel)], top);
363
- if (hashed.error || hashed.status !== 0 || hashed.stdout.toString('utf8').trim() !== entry.oid) lag();
364
- }
365
- return { paths: lagging, submodules: laggingSubmodules, flagged: laggingFlags };
366
- };
367
-
368
- export const computeWorkingState = (cwd, { lstat = lstatSync, readlink = readlinkSync, runGit = null, env = process.env } = {}) => {
369
- const run = guardedRunner(runGit ?? defaultRunGit, env);
370
- const top = locateWorkTree(cwd, run, env);
371
- if (top == null) return null;
372
- // `--ignore-submodules=none` on the staged probe too: a config-hidden STAGED gitlink would
373
- // otherwise make stagedDirty false, and isTreeClean would call such a tree clean.
374
- const staged = run(['diff', '--cached', '--quiet', '--ignore-submodules=none'], top);
375
- // ONLY 0 or 1 is a usable answer: a signal-killed probe reports status null, which the old
376
- // `> 1` guard let through as "nothing staged" — a fail-OPEN the whole arm cannot afford.
377
- if (staged.error || (staged.status !== 0 && staged.status !== 1)) return null;
378
- const buf = (args) => stdoutOf(run(args, top));
379
- // The FULL probe forces submodules back in: diff.ignoreSubmodules / submodule.<n>.ignore would
380
- // otherwise erase a dirty submodule from the comparison entirely.
381
- const changedZ = buf(['diff', '--name-only', '-z', '--ignore-submodules=none']);
382
- const plainZ = buf(['diff', '--name-only', '-z', '--ignore-submodules=all']);
383
- const untrackedZ = buf(['ls-files', '--others', '--exclude-standard', '-z']);
384
- if (changedZ == null || plainZ == null || untrackedZ == null) return null;
385
- const split = (b) => b.toString('utf8').split('\0').filter(Boolean);
386
- const changed = [...new Set(split(changedZ))]; // an unmerged path is listed once per conflict side
387
- const plain = new Set(split(plainZ));
388
- const flagged = flaggedIndexLag(top, run, lstat, readlink);
389
- if (flagged == null) return null;
390
- const unstaged = changed.filter((rel) => plain.has(rel));
391
- const submodules = changed.filter((rel) => !plain.has(rel));
392
- return {
393
- stagedDirty: staged.status === 1,
394
- unstagedPaths: [...unstaged, ...flagged.paths.filter((rel) => !unstaged.includes(rel))],
395
- unstagedSubmodulePaths: [...submodules, ...flagged.submodules.filter((rel) => !submodules.includes(rel))],
396
- // Which lagging paths carry index bits, and which — `git add -A` cannot restage these at all.
397
- flaggedPaths: flagged.flagged,
398
- untrackedPaths: split(untrackedZ).filter((rel) => {
399
- try {
400
- return !isNeverCommittableStat(lstat(join(top, rel)));
401
- } catch {
402
- return true;
403
- }
404
- }),
405
- };
406
- };
407
-
408
- // Clean = nothing staged, nothing unstaged, no REVIEWABLE untracked-not-ignored paths — the same
409
- // never-committable filter as the fingerprint, so the two can never disagree about a masks-only
410
- // tree. Null when not decidable.
411
- export const isTreeClean = (cwd, fsx) => {
412
- const state = computeWorkingState(cwd, fsx);
413
- if (state == null) return null;
414
- return (
415
- !state.stagedDirty &&
416
- state.unstagedPaths.length === 0 &&
417
- state.unstagedSubmodulePaths.length === 0 &&
418
- state.untrackedPaths.length === 0
419
- );
420
- };
421
-
422
- // ── the review-receipt read path + attesting predicate (ONE home) ────────────────────────────────
423
-
424
- export const RECEIPTS_BASENAME = 'agent-workflow-review-receipts.jsonl';
425
-
426
- export const resolveReceiptsPath = (cwd, env = process.env) => {
427
- if (env.AW_REVIEW_RECEIPTS) return env.AW_REVIEW_RECEIPTS;
428
- const gitDir = gitLine(['rev-parse', '--absolute-git-dir'], cwd);
429
- return gitDir == null ? null : join(gitDir, RECEIPTS_BASENAME);
430
- };
431
-
432
- // Parse the receipt file → { receipts, malformed, readError? }. Absent file → empty (not an
433
- // error: no review ever ran). The read rides the fs-read-nofollow leaf (RECEIPTS-READER-NOFOLLOW):
434
- // a symlinked/FIFO/directory receipts path surfaces as readError — never content, never an empty
435
- // success — and any NON-ENOENT failure surfaces as readError too; an unreadable store must never
436
- // silently read as "no receipts" (the summary withholds its verdicts section on it). A malformed
437
- // line is counted + reported, never silently dropped. `io` is the injectable-read test seam
438
- // (readRegularFileNoFollow's descriptor-level io).
439
- export const readReceipts = (path, io = {}) => {
440
- const read = readRegularFileNoFollow(path, io);
441
- if (read.outcome === 'absent') return { receipts: [], malformed: 0 };
442
- if (read.outcome === 'foreign') {
443
- return { receipts: [], malformed: 0, readError: `the receipts store is a ${read.className}, not a regular file — refusing to read it (fail closed)` };
444
- }
445
- if (read.outcome === 'error') return { receipts: [], malformed: 0, readError: read.code };
446
- const raw = read.content;
447
- const receipts = [];
448
- let malformed = 0;
449
- for (const line of raw.split('\n')) {
450
- if (line.trim() === '') continue;
451
- try {
452
- const parsed = JSON.parse(line);
453
- if (parsed && typeof parsed === 'object' && typeof parsed.backend === 'string') receipts.push(parsed);
454
- else malformed += 1;
455
- } catch {
456
- malformed += 1;
457
- }
458
- }
459
- return { receipts, malformed };
460
- };
461
-
462
- // The closed review-verdict vocabulary (the union of both wrappers' closed grammars). Any
463
- // RECOGNIZED verdict is review EVIDENCE (a recognized negative is an authoritative veto); ONLY
464
- // ship-class SATISFIES the review-state gate.
465
- const SHIP_VERDICTS = new Set(['ship', 'ship with nits']);
466
- const NEGATIVE_VERDICTS = new Set(['revise', 'rethink', 'rework']);
467
- // Type-strict: only a STRING can enter the closed grammar — String() coercion would admit an
468
- // array like ["ship"] as ship-class.
469
- const normalizeVerdict = (verdict) => (typeof verdict === 'string' ? verdict.trim().toLowerCase() : null);
470
- export const isShipVerdict = (verdict) => {
471
- const v = normalizeVerdict(verdict);
472
- return v !== null && SHIP_VERDICTS.has(v);
473
- };
474
- export const isRecognizedVerdict = (verdict) => {
475
- const v = normalizeVerdict(verdict);
476
- return v !== null && (SHIP_VERDICTS.has(v) || NEGATIVE_VERDICTS.has(v));
477
- };
478
-
479
- export const REVIEW_RECEIPT_CLASS = Object.freeze({
480
- NOT_CURRENT: 'not-current',
481
- ATTESTING: 'attesting',
482
- UNGROUNDED: 'ungrounded',
483
- UNRECOGNIZED_VERDICT: 'unrecognized-verdict',
484
- PROBE: 'probe',
485
- UNMARKED: 'unmarked',
486
- MALFORMED_MARKER: 'malformed-marker',
487
- POSTURE_UNMARKED: 'posture-unmarked',
488
- MALFORMED_POSTURE: 'malformed-posture',
489
- DELIVERY_UNMARKED: 'delivery-unmarked',
490
- MALFORMED_DELIVERY: 'malformed-delivery',
491
- });
492
-
493
- // Backends whose `code` receipts must SELF-DECLARE how the change set reached the model (D8b).
494
- // Scoped, not universal: agy's oversized lane was the one observed returning a confident
495
- // fabrication, and codex's receipt semantics are deliberately untouched.
496
- const DELIVERY_DECLARING_BACKENDS = new Set(['agy']);
497
-
498
- // A delivery declaration is a lowercase token naming HOW delivery was established (`inline` when
499
- // the whole change set rode one prompt, `fed` when a chunked feed proved it by echo). The gate
500
- // requires PRESENT and WELL-FORMED, never a PARTICULAR value — which lane was used is the
501
- // wrapper's business; that the receipt declares one at all is the gate's.
502
- const isValidReceiptDelivery = (delivery) =>
503
- typeof delivery === 'string' && delivery.length > 0 && delivery.length <= 32 && /^[a-z][a-z0-9-]*$/.test(delivery);
504
-
505
- // The D5 posture declaration (strip Phase 4): an object whose `model` is a NON-EMPTY string;
506
- // `effort` (when present) a non-empty string; `tier` (when present) a non-empty string or null.
507
- // Backend-agnostic — the wrapper is the writer authority on WHICH keys it declares.
508
- const isValidReceiptPosture = (posture) => {
509
- if (posture === null || typeof posture !== 'object' || Array.isArray(posture)) return false;
510
- if (typeof posture.model !== 'string' || posture.model.length === 0) return false;
511
- if (Object.hasOwn(posture, 'effort') && (typeof posture.effort !== 'string' || posture.effort.length === 0)) return false;
512
- if (Object.hasOwn(posture, 'tier') && posture.tier !== null && (typeof posture.tier !== 'string' || posture.tier.length === 0)) return false;
513
- return true;
514
- };
515
-
516
- // Classify ONE receipt against a tree fingerprint. Order is load-bearing: identity first (is this
517
- // even about the current tree?), then the probe marker (may it attest at all?), then the D5
518
- // posture marker (same self-declaration doctrine — an absent/invalid posture is non-attesting,
519
- // fail closed; pre-D5 receipts stop satisfying, the recovery is re-running the review), then the
520
- // verdict-in-recognized-vocabulary arm BEFORE grounding — an unrecognized verdict is an
521
- // unconditional refusal that grounding must never reclassify (an ungrounded `unknown` receipt
522
- // otherwise reads UNGROUNDED and can be masked by another backend's SHIP), then grounding.
523
- export const classifyReviewReceiptForTree = (receipt, fingerprint) => {
524
- if (!(receipt !== null && typeof receipt === 'object' && !Array.isArray(receipt)) || receipt.fresh !== true || receipt.artifact !== 'code' || receipt.fingerprint !== fingerprint) {
525
- return REVIEW_RECEIPT_CLASS.NOT_CURRENT;
526
- }
527
- if (!Object.hasOwn(receipt, 'probe')) return REVIEW_RECEIPT_CLASS.UNMARKED;
528
- if (typeof receipt.probe !== 'boolean') return REVIEW_RECEIPT_CLASS.MALFORMED_MARKER;
529
- if (receipt.probe === true) return REVIEW_RECEIPT_CLASS.PROBE;
530
- if (!Object.hasOwn(receipt, 'posture')) return REVIEW_RECEIPT_CLASS.POSTURE_UNMARKED;
531
- if (!isValidReceiptPosture(receipt.posture)) return REVIEW_RECEIPT_CLASS.MALFORMED_POSTURE;
532
- if (!isRecognizedVerdict(receipt.verdict)) return REVIEW_RECEIPT_CLASS.UNRECOGNIZED_VERDICT;
533
- if (receipt.grounded !== true) return REVIEW_RECEIPT_CLASS.UNGROUNDED;
534
- // LAST on purpose. The delivery arm may only intercept a receipt that would otherwise ATTEST:
535
- // placing it earlier pulled delivery-less `unrecognized-verdict` / `ungrounded` agy receipts out
536
- // of summarize's latest-NORMAL selection, so an EARLIER ship survived a LATER bad receipt — the
537
- // selection-first doctrine, broken silently. Those two classes stay byte-identical to before;
538
- // the ONE class this arm changes is the previously-attesting one, which is the point.
539
- if (DELIVERY_DECLARING_BACKENDS.has(receipt.backend)) {
540
- if (!Object.hasOwn(receipt, 'delivery')) return REVIEW_RECEIPT_CLASS.DELIVERY_UNMARKED;
541
- if (!isValidReceiptDelivery(receipt.delivery)) return REVIEW_RECEIPT_CLASS.MALFORMED_DELIVERY;
542
- }
543
- return REVIEW_RECEIPT_CLASS.ATTESTING;
544
- };
545
-
546
- // Summarize one backend's receipts for a tree → { state, receipt, counts… }. The LATEST NORMAL
547
- // (probe-free, marker-valid, current-fingerprint) receipt is selected FIRST and THEN judged — a
548
- // later unknown-verdict or ungrounded receipt never lets an earlier SHIP survive, and a probe (or
549
- // forged marker) written after a real review never becomes the authoritative verdict.
550
- export const summarizeReviewReceiptsForTree = (receipts, fingerprint) => {
551
- const classified = receipts
552
- .map((receipt) => ({ receipt, classification: classifyReviewReceiptForTree(receipt, fingerprint) }))
553
- .filter(({ classification }) => classification !== REVIEW_RECEIPT_CLASS.NOT_CURRENT);
554
- const rowsFor = (classification) => classified.filter((row) => row.classification === classification);
555
- const normal = classified.filter(({ classification }) =>
556
- classification === REVIEW_RECEIPT_CLASS.ATTESTING ||
557
- classification === REVIEW_RECEIPT_CLASS.UNGROUNDED ||
558
- classification === REVIEW_RECEIPT_CLASS.UNRECOGNIZED_VERDICT);
559
- const probe = rowsFor(REVIEW_RECEIPT_CLASS.PROBE);
560
- const unmarked = rowsFor(REVIEW_RECEIPT_CLASS.UNMARKED);
561
- const malformedMarker = rowsFor(REVIEW_RECEIPT_CLASS.MALFORMED_MARKER);
562
- const postureUnmarked = rowsFor(REVIEW_RECEIPT_CLASS.POSTURE_UNMARKED);
563
- const malformedPosture = rowsFor(REVIEW_RECEIPT_CLASS.MALFORMED_POSTURE);
564
- const deliveryUnmarked = rowsFor(REVIEW_RECEIPT_CLASS.DELIVERY_UNMARKED);
565
- const malformedDelivery = rowsFor(REVIEW_RECEIPT_CLASS.MALFORMED_DELIVERY);
566
- const counts = {
567
- currentCount: classified.length,
568
- ungroundedCount: rowsFor(REVIEW_RECEIPT_CLASS.UNGROUNDED).length,
569
- unrecognizedVerdictCount: rowsFor(REVIEW_RECEIPT_CLASS.UNRECOGNIZED_VERDICT).length,
570
- probeExcluded: probe.length,
571
- markerRejected: malformedMarker.length,
572
- unmarkedRejected: unmarked.length,
573
- postureRejected: postureUnmarked.length + malformedPosture.length,
574
- deliveryRejected: deliveryUnmarked.length + malformedDelivery.length,
575
- };
576
- if (normal.length > 0) {
577
- const latest = normal[normal.length - 1];
578
- if (latest.classification === REVIEW_RECEIPT_CLASS.ATTESTING) return { state: 'current', receipt: latest.receipt, ...counts };
579
- if (latest.classification === REVIEW_RECEIPT_CLASS.UNGROUNDED) return { state: 'ungrounded', receipt: latest.receipt, ...counts };
580
- return { state: 'unrecognized-verdict', receipt: latest.receipt, ...counts };
581
- }
582
- if (classified.length > 0) {
583
- return {
584
- state: malformedMarker.length > 0 || unmarked.length > 0 || counts.postureRejected > 0 || counts.deliveryRejected > 0 ? 'rejected' : 'probe',
585
- receipt: null,
586
- ...counts,
587
- };
588
- }
589
- return { state: 'none', receipt: null, ...counts };
590
- };
591
-
592
- // Why this backend has no attestation — one stated sentence per distinct recovery.
593
- export const describeMissingReviewAttestation = (summary) => {
594
- if (summary.state === 'current') return null;
595
- const exclusions = [
596
- summary.probeExcluded > 0 ? `${summary.probeExcluded} probe receipt(s)` : null,
597
- summary.markerRejected > 0 ? `${summary.markerRejected} receipt(s) with a malformed probe marker` : null,
598
- summary.unmarkedRejected > 0 ? `${summary.unmarkedRejected} receipt(s) with no probe marker` : null,
599
- (summary.postureRejected ?? 0) > 0 ? `${summary.postureRejected} receipt(s) with an absent/invalid run posture (a pre-D5 wrapper — re-run the review on the current bridge)` : null,
600
- (summary.deliveryRejected ?? 0) > 0 ? `${summary.deliveryRejected} agy receipt(s) with an absent/invalid delivery declaration (minted before the change set was PROVEN delivered — re-run the review on the current bridge)` : null,
601
- ].filter(Boolean);
602
- const exclusionSuffix = exclusions.length > 0 ? `; excluded ${exclusions.join(', ')}` : '';
603
- if (summary.state === 'ungrounded') return `the latest normal receipt for the current tree is ungrounded${exclusionSuffix}`;
604
- if (summary.state === 'unrecognized-verdict') return `the latest normal receipt carries an unrecognized verdict (${JSON.stringify(summary.receipt?.verdict ?? null)}) — an unknown verdict never attests${exclusionSuffix}`;
605
- if (summary.state === 'probe') return 'only probe receipts exist for the current tree — a probe review never attests';
606
- if (summary.state === 'rejected') return `current-tree receipts have an untrustworthy probe marker, run posture or delivery declaration${exclusionSuffix}`;
607
- return 'no fresh code receipt exists for the current tree';
608
- };
609
-
610
- // ── the store: ONE git-dir JSONL file (D7) ────────────────────────────────────────────────────────
611
-
612
- export const EVIDENCE_BASENAME = 'agent-workflow-core-evidence.jsonl';
613
- export const EVIDENCE_SCHEMA_VERSION = 1;
614
-
615
- // AW_CORE_EVIDENCE overrides (the AW_REVIEW_RECEIPTS idiom); else <git dir>/basename; null outside
616
- // a git work tree.
617
- export const resolveEvidencePath = (cwd, env = process.env) => {
618
- if (env.AW_CORE_EVIDENCE) return env.AW_CORE_EVIDENCE;
619
- const gitDir = gitLine(['rev-parse', '--absolute-git-dir'], cwd);
620
- return gitDir == null ? null : join(gitDir, EVIDENCE_BASENAME);
621
- };
622
-
623
- // ── schema validation (closed kinds, per-kind arms; unknown schema/kind fail CLOSED) ──────────────
624
-
625
- const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
626
- const isNonEmptyString = (v) => typeof v === 'string' && v.length > 0;
627
- const HEX64_RE = /^[0-9a-f]{64}$/;
628
- const HEX40_RE = /^[0-9a-f]{40}$/;
38
+ import { EVIDENCE_BASENAME } from './core-evidence-store-read.mjs';
39
+ import { usageFail, runRedProof, runDegrade } from './core-evidence-store.mjs';
40
+ import { buildSummaryState, renderSummary } from './core-evidence-summary.mjs';
629
41
 
630
42
  // The LEXICAL half of the repo-relative rule lives in the repo-lex.mjs LEAF (ONE home shared by
631
- // the record validators — flow-record has no fs to resolve against — and the fs resolver below,
632
- // so the two can never drift); re-exported here so every historical consumer keeps its import site.
43
+ // the record validators — flow-record has no fs to resolve against — and the fs resolver in the
44
+ // red-proof leaf, so the two can never drift); re-exported here so every historical consumer keeps its import site.
633
45
  export { lexicalRepoRelative } from './repo-lex.mjs';
634
-
635
- export const validateEvidenceRecord = (record) => {
636
- if (!isPlainObject(record)) return { ok: false, reason: 'record is not an object' };
637
- if (record.schema !== EVIDENCE_SCHEMA_VERSION) {
638
- return { ok: false, reason: `unknown schema ${JSON.stringify(record.schema)} — this reader accepts schema ${EVIDENCE_SCHEMA_VERSION} only (fail closed)` };
639
- }
640
- if (record.kind === 'red-proof') {
641
- if (!isWellFormedTestId(record.testId)) return { ok: false, reason: 'red-proof: testId must be "<test-file>#<test-name-pattern>" (a "#" separator, both halves non-empty)' };
642
- if (!isNonEmptyString(record.file)) return { ok: false, reason: 'red-proof: file must be the non-empty repo-relative test-file path' };
643
- const lex = lexicalRepoRelative(record.file);
644
- if (!lex.ok) return { ok: false, reason: `red-proof: file must be lexically repo-relative — ${lex.reason}` };
645
- if (record.file !== splitTestId(record.testId).file) return { ok: false, reason: 'red-proof: file must equal the testId file half — one path source of truth (a mismatched declaration is refused)' };
646
- if (typeof record.fileHash !== 'string' || !HEX64_RE.test(record.fileHash)) return { ok: false, reason: 'red-proof: fileHash must be a 64-hex sha256 of the test-file bytes' };
647
- if (!Number.isInteger(record.runs) || record.runs < 1) return { ok: false, reason: 'red-proof: runs must be a positive integer' };
648
- if (record.reds !== record.runs) return { ok: false, reason: 'red-proof: reds must equal runs — an observed-red declaration is N/N by construction' };
649
- if (record.base !== null && (typeof record.base !== 'string' || !(HEX40_RE.test(record.base) || HEX64_RE.test(record.base)))) return { ok: false, reason: 'red-proof: base must be the 40- or 64-hex HEAD sha (git sha1/sha256 object formats), or null on an unborn branch' };
650
- if (typeof record.fingerprint !== 'string' || !HEX64_RE.test(record.fingerprint)) return { ok: false, reason: 'red-proof: fingerprint must be the 64-hex PRE-FIX tree fingerprint' };
651
- if (!isNonEmptyString(record.timestamp)) return { ok: false, reason: 'red-proof: timestamp must be a non-empty string' };
652
- return { ok: true };
653
- }
654
- if (record.kind === 'degrade') {
655
- if (typeof record.backend !== 'string' || record.backend.trim() === '') return { ok: false, reason: 'degrade: backend must be a non-empty string (whitespace-only is refused)' };
656
- if (typeof record.reason !== 'string' || record.reason.trim() === '') return { ok: false, reason: 'degrade: reason must be a non-empty string — a degrade without a stated reason is refused (fail closed)' };
657
- if (typeof record.fingerprint !== 'string' || !HEX64_RE.test(record.fingerprint)) return { ok: false, reason: 'degrade: fingerprint must be the 64-hex tree fingerprint the degrade attests' };
658
- if (!isNonEmptyString(record.timestamp)) return { ok: false, reason: 'degrade: timestamp must be a non-empty string' };
659
- return { ok: true };
660
- }
661
- if (record.kind === 'final-start') {
662
- if (typeof record.fingerprint !== 'string' || !HEX64_RE.test(record.fingerprint)) return { ok: false, reason: 'final-start: fingerprint must be the 64-hex tree fingerprint the attempt targets' };
663
- if (!isNonEmptyString(record.attempt)) return { ok: false, reason: 'final-start: attempt must be a non-empty id — the completion closes exactly THIS start (linkage)' };
664
- if (!isNonEmptyString(record.timestamp)) return { ok: false, reason: 'final-start: timestamp must be a non-empty string' };
665
- return { ok: true };
666
- }
667
- if (record.kind === 'final') {
668
- if (record.status !== 'green' && record.status !== 'red') return { ok: false, reason: `final: status must be "green" or "red" (got ${JSON.stringify(record.status)})` };
669
- if (!isNonEmptyString(record.attempt)) return { ok: false, reason: 'final: attempt must be the non-empty id of the start this completion closes' };
670
- for (const field of ['fingerprintBefore', 'fingerprintAfter']) {
671
- if (typeof record[field] !== 'string' || !HEX64_RE.test(record[field])) return { ok: false, reason: `final: ${field} must be a 64-hex tree fingerprint` };
672
- }
673
- if (!Array.isArray(record.declared) || record.declared.length === 0 || !record.declared.every((d) => isPlainObject(d) && isNonEmptyString(d.id) && isNonEmptyString(d.cmd))) {
674
- return { ok: false, reason: 'final: declared must be the non-empty ordered {id, cmd} array the run executed — an empty declaration never attests' };
675
- }
676
- if (!Array.isArray(record.results) || !record.results.every((r) => isPlainObject(r) && isNonEmptyString(r.id) && typeof r.ok === 'boolean' && (r.code === null || Number.isInteger(r.code)))) {
677
- return { ok: false, reason: 'final: results must be the per-gate {id, ok, code} array (code integer, or null on a spawn failure)' };
678
- }
679
- if (record.results.length !== record.declared.length || !record.results.every((r, i) => r.id === record.declared[i].id)) {
680
- return { ok: false, reason: 'final: results must mirror the declared gates 1:1 IN ORDER — a partial or shuffled attribution never attests' };
681
- }
682
- if (record.integrityFailure !== null && !isNonEmptyString(record.integrityFailure)) {
683
- return { ok: false, reason: 'final: integrityFailure must be null or a non-empty reason (an artifact moved under the run)' };
684
- }
685
- const derived = record.results.every((r) => r.ok) && record.integrityFailure === null ? 'green' : 'red';
686
- if (record.status !== derived) {
687
- return { ok: false, reason: `final: status ${JSON.stringify(record.status)} contradicts the results/integrity content (derived ${JSON.stringify(derived)}) — status is DERIVED, never asserted` };
688
- }
689
- if (!isPlainObject(record.evidenceHashes)
690
- || typeof record.evidenceHashes.redProof !== 'string' || !HEX64_RE.test(record.evidenceHashes.redProof)
691
- || typeof record.evidenceHashes.degrade !== 'string' || !HEX64_RE.test(record.evidenceHashes.degrade)) {
692
- return { ok: false, reason: 'final: evidenceHashes must carry 64-hex sha256 of the canonical red-proof and degrade serializations' };
693
- }
694
- // The D10 flow binding (Plan 4 Decision 2) is ADDITIVE: absent = a pre-flow-binding final
695
- // (still valid); present must be the 64-hex owner-scoped projection hash.
696
- if ('flow' in record.evidenceHashes && (typeof record.evidenceHashes.flow !== 'string' || !HEX64_RE.test(record.evidenceHashes.flow))) {
697
- return { ok: false, reason: 'final: evidenceHashes.flow, when present, must be a 64-hex sha256 of the owner-scoped flow projection' };
698
- }
699
- if (record.lcovSha256 !== null && (typeof record.lcovSha256 !== 'string' || !HEX64_RE.test(record.lcovSha256))) {
700
- return { ok: false, reason: 'final: lcovSha256 must be a 64-hex sha256 of the consumed lcov file, or null when none was produced' };
701
- }
702
- // The run's coverage token is ADDITIVE (absent = a pre-token receipt, still valid) and CLOSED.
703
- // `certified` is cross-checked against the bound digest — a run that certified a verdict read
704
- // bytes, so the two fields can never disagree in the store.
705
- if ('coverage' in record) {
706
- if (!FINAL_COVERAGE_STATES.includes(record.coverage)) {
707
- return { ok: false, reason: `final: coverage, when present, must be one of ${FINAL_COVERAGE_STATES.join(' | ')} (got ${JSON.stringify(record.coverage)}; a final run always selects the canonical checker, so "none" never rides a final receipt)` };
708
- }
709
- if (record.coverage === COVERAGE.certified && record.lcovSha256 === null) {
710
- return { ok: false, reason: 'final: coverage "certified" requires a bound lcovSha256 — a certified verdict was issued over lcov bytes that were read' };
711
- }
712
- }
713
- if (!isNonEmptyString(record.timestamp)) return { ok: false, reason: 'final: timestamp must be a non-empty string' };
714
- return { ok: true };
715
- }
716
- return { ok: false, reason: `unknown kind ${JSON.stringify(record.kind)} — closed set: red-proof | degrade | final-start | final (fail closed)` };
717
- };
718
-
719
- // ── the fail-closed reader ────────────────────────────────────────────────────────────────────────
720
-
721
- // parseEvidenceText(raw) → { records, malformed, malformedReasons } — the ONE per-line parse +
722
- // validation pass. readEvidence AND the append preflight consume it over the SAME captured bytes
723
- // (a second read between check and write would be a TOCTOU gap).
724
- export const parseEvidenceText = (raw) => {
725
- const records = [];
726
- const malformedReasons = [];
727
- const lines = String(raw).split('\n');
728
- for (let i = 0; i < lines.length; i += 1) {
729
- if (lines[i].trim() === '') continue;
730
- let parsed;
731
- try {
732
- parsed = JSON.parse(lines[i]);
733
- } catch {
734
- malformedReasons.push(`line ${i + 1}: invalid JSON`);
735
- continue;
736
- }
737
- const v = validateEvidenceRecord(parsed);
738
- if (v.ok) records.push(parsed);
739
- else malformedReasons.push(`line ${i + 1}: ${v.reason}`);
740
- }
741
- return { records, malformed: malformedReasons.length, malformedReasons };
742
- };
743
-
744
- // readEvidence(path) → { records, malformed, malformedReasons, readError? }. Absent file → empty
745
- // (no evidence yet is not an error). Every non-parsing / non-validating line is COUNTED with its
746
- // reason — consumers fail closed on malformed > 0, never silently drop.
747
- export const readEvidence = (path, readFile = readFileSync) => {
748
- let raw;
749
- try {
750
- raw = readFile(path, 'utf8');
751
- } catch (err) {
752
- if (err && err.code === 'ENOENT') return { records: [], malformed: 0, malformedReasons: [] };
753
- return { records: [], malformed: 0, malformedReasons: [], readError: (err && err.code) || (err && err.message) || 'read failed' };
754
- }
755
- return parseEvidenceText(raw);
756
- };
757
-
758
- // ── the ONE append (validated, duplicate-refusing, atomic) ────────────────────────────────────────
759
-
760
- export const appendEvidenceRecord = ({ path, record }) => {
761
- const v = validateEvidenceRecord(record);
762
- if (!v.ok) throw stop(`refusing to write a malformed evidence record: ${v.reason}`);
763
- const line = JSON.stringify(record);
764
- let existing = '';
765
- try {
766
- existing = readFileSync(path, 'utf8');
767
- } catch (err) {
768
- if (err && err.code === 'ENOENT') existing = '';
769
- else throw stop(`cannot read the evidence store before appending (${(err && err.code) || (err && err.message) || err}) — refusing to overwrite it (fail closed)`);
770
- }
771
- // Write-side D6a fail-closed: never extend a store whose existing lines are unreadable — the
772
- // SAME captured bytes are validated here and appended below (one snapshot, no second read).
773
- const existingIssues = parseEvidenceText(existing).malformedReasons;
774
- if (existingIssues.length > 0) {
775
- throw stop(`refusing to append to an evidence store carrying ${existingIssues.length} malformed line(s) (${existingIssues[0]}) — inspect ${path}; nothing was written (fail closed)`);
776
- }
777
- if (existing.split('\n').some((l) => l === line)) {
778
- throw stop(`refusing a byte-identical replayed line (duplicate) — a genuine re-observation carries new content or timestamp; nothing was written`);
779
- }
780
- const prefix = existing === '' ? '' : existing.endsWith('\n') ? existing : `${existing}\n`;
781
- writeContainedFileAtomic(dirname(path), path, `${prefix}${line}\n`, {}, { stop, label: path });
782
- return { writtenPath: path, record };
783
- };
784
-
785
- // ── D6a: per-kind keys, authoritative selection, canonical serialization ──────────────────────────
786
-
787
- // Space-joined with the CLOSED (validated hex-or-null) field in the MIDDLE, the free-form field
788
- // last — content can then never forge a separator collision (base/fingerprint carry no spaces by
789
- // validation; the last field absorbs anything).
790
- export const evidenceKey = (r) =>
791
- r.kind === 'red-proof' ? `red-proof ${r.base} ${r.testId}`
792
- : r.kind === 'degrade' ? `degrade ${r.fingerprint} ${r.backend}`
793
- : r.kind === 'final' ? `final ${r.fingerprintBefore}`
794
- : r.kind === 'final-start' ? `final-start ${r.fingerprint}`
795
- : null;
796
-
797
- // The authoritative subset: the LATEST record per key, in file order of that latest appearance.
798
- export const authoritativeEvidence = (records) => {
799
- const lastByKey = new Map();
800
- records.forEach((r, i) => {
801
- const k = evidenceKey(r);
802
- if (k != null) lastByKey.set(k, i);
803
- });
804
- const keep = new Set(lastByKey.values());
805
- return records.filter((_, i) => keep.has(i));
806
- };
807
-
808
- export const authoritativeOfKind = (records, kind) => authoritativeEvidence(records).filter((r) => r.kind === kind);
809
-
810
- // Canonical bytes: key-sorted JSON per record, one line each, newline-terminated ('' when empty) —
811
- // independent of the byte layout the record was originally written with. The D3(a) receipt hashes
812
- // EXACTLY these bytes per kind, so receipts (a different kind/file) are outside the domain by
813
- // construction.
814
- const stableStringify = (v) => {
815
- if (Array.isArray(v)) return `[${v.map(stableStringify).join(',')}]`;
816
- if (v !== null && typeof v === 'object') {
817
- return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(v[k])}`).join(',')}}`;
818
- }
819
- return JSON.stringify(v);
820
- };
821
-
822
- export const canonicalKindSerialization = (records, kind) => {
823
- const lines = authoritativeOfKind(records, kind).map(stableStringify);
824
- return lines.length === 0 ? '' : `${lines.join('\n')}\n`;
825
- };
826
-
827
- // ── the probe safeguards (moved verbatim from the retired fold runner) ────────────────────────────
828
-
829
- // Node sets NODE_TEST_CONTEXT for any process running UNDER `node --test`; a fresh `node --test`
830
- // that inherits it silently SKIPS running its files. The probe spawns `node --test`, so it strips
831
- // that var (a no-op in normal invocation).
832
- export const childTestEnv = (env, extra = {}) => {
833
- const out = { ...env, ...extra };
834
- delete out.NODE_TEST_CONTEXT;
835
- return out;
836
- };
837
-
838
- const PROBE_RESULT_RE = /^(ok|not ok) \d+ - (.*)$/;
839
- const PROBE_FAIL_RE = /^# fail (\d+)$/;
840
- const PROBE_DIRECTIVE_RE = /#\s*(?:skip|todo)\b/i; // a TAP SKIP/TODO directive — the test did NOT run
841
-
842
- // parseProbeOutput({ stdout, code, fileArg }) → { resolvable, executed, baselineGreen }. The file
843
- // wrapper is matched by BASENAME (node normalizes the echoed path); a directive-carrying result
844
- // line never counts (some Node lines emit pattern-filtered tests as SKIP — the guard is
845
- // version-agnostic on purpose).
846
- export const parseProbeOutput = ({ stdout, code, fileArg }) => {
847
- let matched = 0;
848
- let notOk = 0;
849
- let failCount = null;
850
- const wanted = basename(String(fileArg).trim());
851
- for (const line of String(stdout).split('\n')) {
852
- const m = PROBE_RESULT_RE.exec(line);
853
- if (m && !PROBE_DIRECTIVE_RE.test(m[2]) && basename(m[2].trim()) !== wanted) {
854
- matched += 1;
855
- if (m[1] === 'not ok') notOk += 1;
856
- }
857
- const f = PROBE_FAIL_RE.exec(line.trim());
858
- if (f) failCount = Number(f[1]);
859
- }
860
- const resolvable = matched > 0;
861
- const fails = (failCount ?? 0) + notOk;
862
- return { resolvable, executed: matched, baselineGreen: resolvable && code === 0 && fails === 0 };
863
- };
864
-
865
- // The shell-free node:test argv. The pattern rides `=`-joined: as a separate token a pattern
866
- // beginning with "-" would parse as an OPTION and silently select no test.
867
- export const defaultBoundArgv = (file, pattern) => ['node', '--test', '--test-reporter', 'tap', `--test-name-pattern=${pattern}`, file];
868
-
869
- // containsPath(realRoot, realAbs) → strictly INSIDE. Segment-safe ('/a' never contains '/ab') and
870
- // correct for a repo at the filesystem root.
871
- export const containsPath = (realRoot, realAbs) => realAbs.startsWith(realRoot.endsWith(sep) ? realRoot : realRoot + sep);
872
-
873
- // resolveTestFile(rootTop, rel, deps?) → { ok: true, abs } | { ok: false, reason } (never throws).
874
- // Repo-relative only; a REGULAR file under no-follow lstat; the RESOLVED real path contained under
875
- // the REAL repo root (a leaf check alone would let a symlinked PARENT directory escape).
876
- export const resolveTestFile = (rootTop, rel, deps = {}) => {
877
- const lstat = deps.lstat ?? lstatSync;
878
- const realpath = deps.realpath ?? realpathSync;
879
- const lex = lexicalRepoRelative(rel);
880
- if (!lex.ok) return { ok: false, reason: lex.reason };
881
- const abs = join(rootTop, normalize(rel));
882
- let st;
883
- try {
884
- st = lstat(abs);
885
- } catch {
886
- return { ok: false, reason: `file "${rel}" does not exist` };
887
- }
888
- if (!st.isFile()) return { ok: false, reason: `"${rel}" is not a regular file (a symlink/directory/device is never followed — fail closed)` };
889
- let realAbs;
890
- let realRoot;
891
- try {
892
- realAbs = realpath(abs);
893
- realRoot = realpath(rootTop);
894
- } catch {
895
- return { ok: false, reason: `cannot resolve the real path of "${rel}"` };
896
- }
897
- if (!containsPath(realRoot, realAbs)) return { ok: false, reason: `"${rel}" resolves outside the repo root (a symlinked parent directory) — fail closed` };
898
- return { ok: true, abs: realAbs };
899
- };
900
-
901
- // sha-256 over the file's BYTES; null on a read failure (the caller reads that as unresolvable).
902
- export const hashFileBytes = (abs) => {
903
- try {
904
- return createHash('sha256').update(readFileSync(abs)).digest('hex');
905
- } catch {
906
- return null;
907
- }
908
- };
909
-
910
- // N and the per-run timeout — pinned from the old runner's own constants (AW_FOLD_RERUNS default 3,
911
- // AW_FOLD_PROBE_TIMEOUT_S default 120); zero/negative/fractional/non-numeric refused by name.
912
- export const probeKnobsFromEnv = (env = process.env) => ({
913
- reruns: parsePositiveIntKnob(env, 'AW_CORE_EVIDENCE_RERUNS', 3, stop),
914
- timeoutS: parsePositiveIntKnob(env, 'AW_CORE_EVIDENCE_TIMEOUT_S', 120, stop),
915
- });
916
-
917
- // The N-rerun probe. The custody hash is taken BEFORE the runs (the content the observation
918
- // attests); the spawn ALWAYS uses the resolver's canonical absolute path — the executed file must
919
- // be the hashed file independent of runner path semantics. A timed-out or signal-killed run is
920
- // neither red nor green — it lands in `timeouts` (quarantine fuel).
921
- const probeBound = ({ testId, rootTop, env, reruns, timeoutS }) => {
922
- const { file, pattern } = splitTestId(testId);
923
- const resolved = resolveTestFile(rootTop, file);
924
- const fileHash = resolved.ok ? hashFileBytes(resolved.abs) : null;
925
- let executed = 0;
926
- let greens = 0;
927
- let reds = 0;
928
- let timeouts = 0;
929
- if (resolved.ok && fileHash != null) {
930
- for (let i = 0; i < reruns; i += 1) {
931
- const argv = defaultBoundArgv(resolved.abs, pattern);
932
- const res = spawnSync(argv[0], argv.slice(1), {
933
- cwd: rootTop, env: childTestEnv(env), encoding: 'utf8', maxBuffer: GIT_MAX_BUFFER, timeout: timeoutS * 1000,
934
- });
935
- if ((res.error && res.error.code === 'ETIMEDOUT') || res.signal != null) {
936
- timeouts += 1;
937
- continue;
938
- }
939
- const p = parseProbeOutput({ stdout: res.stdout ?? '', code: res.error ? 1 : res.status ?? 1, fileArg: file });
940
- executed = Math.max(executed, p.executed);
941
- if (!p.resolvable) continue;
942
- if (p.baselineGreen) greens += 1;
943
- else reds += 1;
944
- }
945
- }
946
- const entry = { executed, runs: reruns, greens, reds, timeouts, fileHash };
947
- return { entry, file, resolveReason: resolved.ok ? (fileHash == null ? `cannot read "${file}"` : null) : resolved.reason };
948
- };
949
-
950
- // ── the red-proof verb (D3(c)): observed red or nothing ───────────────────────────────────────────
951
-
952
- // runRedProof({ cwd, env, testId }) → { writtenPath, record }. Observes `testId` on the CURRENT
953
- // (pre-fix) tree: N/N red → appends the declaration; anything else is a DISTINGUISHED refusal and
954
- // NOTHING is written.
955
- export const runRedProof = ({ cwd = process.cwd(), env = process.env, testId } = {}) => {
956
- if (!isWellFormedTestId(testId)) {
957
- throw usageFail(`red-proof needs a well-formed testId "<test-file>#<test-name-pattern>" (a "#" separator, both halves non-empty; got ${JSON.stringify(testId)})`);
958
- }
959
- const rootTop = gitLine(['rev-parse', '--show-toplevel'], cwd);
960
- if (rootTop == null) throw stop('not a git work tree — nothing to observe');
961
- const { reruns, timeoutS } = probeKnobsFromEnv(env);
962
- // Pre-run capture: the record's base/fingerprint and the custody hash must attest ONE tree —
963
- // captured BEFORE the runs and re-checked after them; any drift refuses below.
964
- const base = resolveBase(cwd);
965
- const fingerprint = computeTreeFingerprint(cwd);
966
- if (fingerprint == null) throw stop('cannot compute the tree fingerprint — the git location is not a work tree; nothing was observed, no runs were spent');
967
- const { entry, file, resolveReason } = probeBound({ testId, rootTop, env, reruns, timeoutS });
968
- const verdict = probeVerdict(entry);
969
- const counts = `${entry.greens} green / ${entry.reds} red / ${entry.timeouts} timed out / ${entry.runs - entry.greens - entry.reds - entry.timeouts} unresolved of ${entry.runs} run(s)`;
970
- if (verdict === 'unresolvable') {
971
- throw stop(
972
- `red-proof refused for "${testId}": unresolvable — ${resolveReason ?? 'the pattern selects no test'} (${counts}). ` +
973
- `If the test cannot even LOAD pre-fix (it imports an export the fix introduces), author it with a dynamic import() so it loads and FAILS pre-fix. Nothing was recorded.`,
974
- );
975
- }
976
- if (verdict === 'green') {
977
- throw stop(
978
- `red-proof refused for "${testId}": observed GREEN on ${entry.greens}/${entry.runs} runs — the test does not fail on the current (pre-fix) tree, so it proves nothing about the fix. ` +
979
- `Write a test that FAILS before the fix is applied, then record the red-proof BEFORE folding the fix. Nothing was recorded.`,
980
- );
981
- }
982
- if (verdict === 'quarantine') {
983
- const flavor = entry.timeouts > 0
984
- ? `${entry.timeouts} of ${entry.runs} probe run(s) timed out (AW_CORE_EVIDENCE_TIMEOUT_S=${timeoutS}) — a timed-out run is neither red nor green`
985
- : `mixed outcomes (${counts}) — a flaky test can launder a fake red`;
986
- throw stop(
987
- `red-proof refused for "${testId}": QUARANTINE — ${flavor}. QUARANTINE never converts and has no override lane: ` +
988
- `${entry.timeouts > 0 ? 'raise the timeout or make the test faster' : 'replace the flaky test'}, then re-observe. Nothing was recorded.`,
989
- );
990
- }
991
- // Post-run drift recheck: fingerprint, base, and the test-file bytes must all equal the
992
- // pre-run capture — a mutating test / parallel edit / commit would otherwise mint a record
993
- // whose fields attest different trees.
994
- const resolvedAfter = resolveTestFile(rootTop, file);
995
- const hashAfter = resolvedAfter.ok ? hashFileBytes(resolvedAfter.abs) : null;
996
- if (computeTreeFingerprint(cwd) !== fingerprint || resolveBase(cwd) !== base || hashAfter !== entry.fileHash) {
997
- throw stop(
998
- `red-proof refused for "${testId}": the tree moved during the observation (fingerprint, base, or test-file bytes drifted between the pre-run capture and the post-run recheck) — a record would bind fields from different trees. Re-observe on a quiescent tree. Nothing was recorded.`,
999
- );
1000
- }
1001
- const record = {
1002
- schema: EVIDENCE_SCHEMA_VERSION,
1003
- kind: 'red-proof',
1004
- testId,
1005
- file,
1006
- fileHash: entry.fileHash,
1007
- runs: entry.runs,
1008
- reds: entry.reds,
1009
- base,
1010
- fingerprint,
1011
- timestamp: isoNow(),
1012
- };
1013
- const path = resolveEvidencePath(cwd, env);
1014
- if (path == null) throw stop('cannot resolve the evidence-store path — not a git work tree and AW_CORE_EVIDENCE is unset');
1015
- return appendEvidenceRecord({ path, record });
1016
- };
1017
-
1018
- // ── the degrade verb (D3(b)): the explicit per-backend, per-tree escape ───────────────────────────
1019
-
1020
- export const runDegrade = ({ cwd = process.cwd(), env = process.env, backend, reason } = {}) => {
1021
- if (typeof backend !== 'string' || backend.trim() === '') throw usageFail('degrade needs --backend <name> — a missing or whitespace-only backend is refused (the gate matches records by backend name)');
1022
- if (typeof reason !== 'string' || reason.trim() === '') throw usageFail('degrade needs a non-empty --reason "<why this backend cannot review this tree>" — a reasonless degrade is refused');
1023
- const fingerprint = computeTreeFingerprint(cwd);
1024
- if (fingerprint == null) throw stop('not a git work tree — a degrade attests a specific tree fingerprint');
1025
- const record = {
1026
- schema: EVIDENCE_SCHEMA_VERSION,
1027
- kind: 'degrade',
1028
- backend: backend.trim(),
1029
- reason: reason.trim(),
1030
- fingerprint,
1031
- timestamp: isoNow(),
1032
- };
1033
- const path = resolveEvidencePath(cwd, env);
1034
- if (path == null) throw stop('cannot resolve the evidence-store path — not a git work tree and AW_CORE_EVIDENCE is unset');
1035
- return appendEvidenceRecord({ path, record });
1036
- };
1037
-
1038
- // ── the summary verb (D6): ONE stateless render — receipts + evidence store, no ledger ────────────
1039
-
1040
- // A red-proof binds the BYTES of the test file it was observed on, and the final-run checker refuses
1041
- // when those bytes have moved (coverage-check.mjs, the hash-mismatch arm). This render used to print a
1042
- // stale record byte-identically to a live one, so the only place staleness surfaced was that gate —
1043
- // after both council dispatches, at the commit boundary, which is the most expensive discovery point
1044
- // in the loop. The currency is therefore computed HERE, from the SAME two helpers the checker uses, so
1045
- // the render and the gate can never disagree about what stale means.
1046
- //
1047
- // An unresolvable or unreadable file is STALE, not unknown: a proof nobody can re-check is not a proof
1048
- // anyone should read as current.
1049
- export const redProofCurrency = (rootTop, record) => {
1050
- if (rootTop == null) return { state: 'stale', detail: 'the work-tree root is not resolvable' };
1051
- const resolved = resolveTestFile(rootTop, record.file);
1052
- if (!resolved.ok) return { state: 'stale', detail: resolved.reason };
1053
- const current = hashFileBytes(resolved.abs);
1054
- if (current == null) return { state: 'stale', detail: `cannot read "${record.file}"` };
1055
- return current === record.fileHash
1056
- ? { state: 'current' }
1057
- : { state: 'stale', detail: 'the bound test file changed after the mint' };
1058
- };
1059
-
1060
- export const buildSummaryState = ({ cwd = process.cwd(), env = process.env } = {}) => {
1061
- const fingerprint = computeTreeFingerprint(cwd);
1062
- const base = resolveBase(cwd);
1063
- const storePath = resolveEvidencePath(cwd, env);
1064
- const store = storePath ? readEvidence(storePath) : { records: [], malformed: 0, malformedReasons: [] };
1065
- const receiptsPath = resolveReceiptsPath(cwd, env);
1066
- const receiptsRead = receiptsPath ? readReceipts(receiptsPath) : { receipts: [], malformed: 0 };
1067
- const { receipts, malformed: receiptsMalformed } = receiptsRead;
1068
- const receiptsReadError = receiptsRead.readError ?? null;
1069
- const backends = [...new Set(receipts.map((r) => r.backend))].sort();
1070
- const verdicts = backends.map((b) => ({
1071
- backend: b,
1072
- summary: summarizeReviewReceiptsForTree(receipts.filter((r) => r.backend === b), fingerprint),
1073
- }));
1074
- const rootTop = gitLine(['rev-parse', '--show-toplevel'], cwd);
1075
- const redProofs = authoritativeOfKind(store.records, 'red-proof')
1076
- .filter((r) => r.base === base)
1077
- .map((r) => ({ ...r, currency: redProofCurrency(rootTop, r) }));
1078
- const degrades = authoritativeOfKind(store.records, 'degrade').filter((r) => r.fingerprint === fingerprint);
1079
- const finalRun = authoritativeOfKind(store.records, 'final').find((r) => r.fingerprintBefore === fingerprint) ?? null;
1080
- // A malformed/unreadable store makes the AUTHORITATIVE selection untrustworthy (a dropped later
1081
- // line could resurrect a superseded record — or hide a newer verdict) — the summary WITHHOLDS
1082
- // the affected sections and exits non-zero instead of rendering a lie. Both stores get the same
1083
- // posture: the evidence store and the receipts store.
1084
- const evidenceUnavailable = store.malformed > 0 || store.readError != null;
1085
- const receiptsUnavailable = receiptsMalformed > 0 || receiptsReadError != null;
1086
- return {
1087
- fingerprint, base, storePath,
1088
- storeRecords: store.records.length, storeMalformed: store.malformed, storeReadError: store.readError ?? null,
1089
- evidenceUnavailable, receiptsUnavailable, receiptsReadError,
1090
- receiptsPath, receiptsMalformed, verdicts, redProofs, degrades, finalRun,
1091
- };
1092
- };
1093
-
1094
- const verdictLine = ({ backend, summary }) => {
1095
- if (summary.state === 'current') return `${backend}: ${summary.receipt.verdict ?? 'unknown'} (attesting, ${summary.receipt.timestamp ?? '?'})`;
1096
- if (summary.state === 'ungrounded') return `${backend}: ${summary.receipt.verdict ?? 'unknown'} (ungrounded — never attests)`;
1097
- if (summary.state === 'unrecognized-verdict') return `${backend}: unrecognized verdict (${JSON.stringify(summary.receipt?.verdict ?? null)}) — never attests (fail-closed)`;
1098
- if (summary.state === 'probe') return `${backend}: probe receipts only for the current tree (never attest)`;
1099
- if (summary.state === 'rejected') return `${backend}: current-tree receipts rejected (untrustworthy probe marker)`;
1100
- return `${backend}: no attesting receipt for the current tree (stale or missing)`;
1101
- };
1102
-
1103
- // Why the run issued no verdict, keyed by the token IT recorded. `certified` maps to nothing —
1104
- // there is no absence to name. A legacy receipt predates the token: the only honest thing to say
1105
- // is what the RECORD carries, which is true by construction. Exported as a test seam (keyFor).
1106
- const COVERAGE_QUALIFIER = Object.freeze({
1107
- [COVERAGE.notRun]: 'the recorded run issued no coverage verdict',
1108
- [COVERAGE.unknown]: 'the recorded run produced no readable coverage signal',
1109
- });
1110
- const LEGACY_COVERAGE_QUALIFIER = `coverage=${COVERAGE.unknown}: this legacy receipt carries no coverage token and binds no lcov digest`;
1111
-
1112
- export const coverageQualifierFor = (finalRun) => {
1113
- if (!finalRun) return '';
1114
- if (typeof finalRun.coverage === 'string') {
1115
- const why = COVERAGE_QUALIFIER[finalRun.coverage];
1116
- return why ? ` — coverage=${finalRun.coverage}: ${why}` : '';
1117
- }
1118
- return finalRun.lcovSha256 == null ? ` — ${LEGACY_COVERAGE_QUALIFIER}` : '';
1119
- };
1120
-
1121
- export const renderSummary = (s) => {
1122
- const short = (hex) => (typeof hex === 'string' ? `${hex.slice(0, 12)}…` : String(hex));
1123
- const evidenceSections = s.evidenceUnavailable
1124
- ? [` evidence sections WITHHELD — the store is unavailable (${s.storeMalformed} malformed line(s)${s.storeReadError ? `, read error: ${s.storeReadError}` : ''}); a dropped line could resurrect a superseded record — inspect ${s.storePath}`]
1125
- : [
1126
- ` red-proof records (current base): ${s.redProofs.length ? '' : '(none)'}`,
1127
- ...s.redProofs.map((r) => {
1128
- const stale = r.currency?.state === 'stale';
1129
- const mark = stale ? `STALE (${r.currency.detail})` : 'CURRENT';
1130
- return ` ${r.testId} — ${mark} · ${r.reds}/${r.runs} red, hash ${short(r.fileHash)}, pre-fix fingerprint ${short(r.fingerprint)}`;
1131
- }),
1132
- // The recovery, printed once and only when it is needed — it was written down nowhere, and
1133
- // the loop paid for that three times.
1134
- ...(s.redProofs.some((r) => r.currency?.state === 'stale')
1135
- ? [' ↳ a STALE proof is refused by run-gates --final: park the fix so the bound test fails again, re-observe it with `core-evidence red-proof "<testId>"`, then restore the fix. Edit test files FIRST and re-observe ONCE per park — every proof bound to an edited file goes stale together.']
1136
- : []),
1137
- ` degrade records (current tree): ${s.degrades.length ? '' : '(none)'}`,
1138
- ...s.degrades.map((d) => ` ${d.backend} — ${d.reason} (${d.timestamp})`),
1139
- ];
1140
- const verdictsSection = s.receiptsUnavailable
1141
- ? [` review verdicts WITHHELD — the receipts store is unavailable (${s.receiptsMalformed} malformed line(s)${s.receiptsReadError ? `, read error: ${s.receiptsReadError}` : ''}); a dropped line could hide a newer verdict — inspect ${s.receiptsPath}`]
1142
- : [
1143
- ' review verdicts (current tree):',
1144
- ...(s.verdicts.length ? s.verdicts.map((v) => ` ${verdictLine(v)}`) : [' (no receipts)']),
1145
- ];
1146
- // The withheld coverage verdict travels here too: an unqualified GREEN repeats, one surface
1147
- // further on, the false reassurance the checker's own attested=no exists to close. It is read
1148
- // from the token the RUN recorded — `lcovSha256` says what the receipt binds, never whether a
1149
- // verdict was issued — and it rides RED as well: an absent verdict is a property of the run, not
1150
- // of its colour. A LEGACY receipt (no token) is named as exactly that, never as a claim about
1151
- // what it read. DETAIL beside an unchanged status word.
1152
- const coverageQualifier = coverageQualifierFor(s.finalRun);
1153
- const finalLine = s.evidenceUnavailable
1154
- ? null
1155
- : s.finalRun
1156
- ? ` final gate run: ${s.finalRun.status === 'green' ? 'GREEN' : 'RED'}${coverageQualifier} (${s.finalRun.results.filter((r) => r.ok).length}/${s.finalRun.results.length} gates, ${s.finalRun.timestamp})`
1157
- : ' final gate run: (none recorded for the current tree)';
1158
- const lines = [
1159
- 'core-evidence summary — stateless render (review receipts + evidence store; no ledger, no rounds)',
1160
- ` tree fingerprint: ${s.fingerprint ?? '(not a git work tree)'}`,
1161
- ` base: ${s.base ?? '(unborn branch)'}`,
1162
- ...(finalLine ? [finalLine] : []),
1163
- ...verdictsSection,
1164
- ...evidenceSections,
1165
- ` evidence store: ${s.storePath ?? '(unresolvable — no git dir)'} (${s.storeRecords} record(s)${s.storeMalformed ? `, ${s.storeMalformed} malformed — inspect the file` : ''}${s.storeReadError ? `, read error: ${s.storeReadError}` : ''})`,
1166
- ];
1167
- return lines.join('\n');
1168
- };
46
+ export {
47
+ gitBuf, resolveBase, isBinaryFile, isNeverCommittableStat, FINGERPRINT_CACHED_DIFF_ARGV, FINGERPRINT_UNSTAGED_DIFF_ARGV,
48
+ computeFingerprintPayload, computeTreeFingerprint, CONTENT_FREE_FINGERPRINT, computeWorkingState, isTreeClean,
49
+ } from './core-evidence-tree.mjs';
50
+ export {
51
+ RECEIPTS_BASENAME, resolveReceiptsPath, readReceipts, isShipVerdict, isRecognizedVerdict, REVIEW_RECEIPT_CLASS,
52
+ classifyReviewReceiptForTree, summarizeReviewReceiptsForTree, describeMissingReviewAttestation,
53
+ } from './core-evidence-receipts.mjs';
54
+ export {
55
+ isWellFormedTestId, splitTestId, EVIDENCE_BASENAME, EVIDENCE_SCHEMA_VERSION, resolveEvidencePath, validateEvidenceRecord,
56
+ parseEvidenceText, readEvidence, evidenceKey, authoritativeEvidence, authoritativeOfKind, canonicalKindSerialization,
57
+ } from './core-evidence-store-read.mjs';
58
+ export { childTestEnv, parseProbeOutput, defaultBoundArgv, containsPath, resolveTestFile, hashFileBytes } from './core-evidence-red-proof.mjs';
59
+ export { CORE_EVIDENCE_STOP, appendEvidenceRecord, probeKnobsFromEnv, runRedProof, runDegrade } from './core-evidence-store.mjs';
60
+ export { redProofCurrency, buildSummaryState, coverageQualifierFor, renderSummary } from './core-evidence-summary.mjs';
1169
61
 
1170
62
  // ── CLI ───────────────────────────────────────────────────────────────────────────────────────────
1171
63