Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_verifyverb.mjs

28.5 KiB, 1 run

created by r2519314175:781, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1// verify_verifyverb.mjs — the `verify` verb, against the real hand binary.
2//
3// The verb exists because a daimon cannot produce browser evidence: `listen()` is
4// refused by the hand's seccomp filter, a browser needs the display server's unix
5// socket and the filter takes that away, so every `dev/verify_*.mjs` that drives a
6// real page dies under the fence a command gets. What replaces the fence here is
7// PROVENANCE: the verb takes a NAME, looks it up in the granted tree, and builds
8// the command itself, so nothing the model wrote reaches a process.
9//
10// ── What this drives, and what it does not ──────────────────────────
11//
12// The real `daimond-hand` binary, spawned as a process, spoken to over Chrome's
13// native messaging framing — a 4-byte native-endian length prefix and UTF-8 JSON.
14// No browser and no extension: `ext/hand.js` is a second line that refuses a
15// message type it does not know, and it is not what decides any of the properties
16// below. The hand is.
17//
18// The verifiers it runs are FIXTURES this file writes into a scratch git
19// repository, never the real ones — the real suite takes hours and half of it
20// wants a browser. What is being measured is the verb, not the suite.
21//
22// ── THE ONE PROPERTY THIS FILE EXISTS FOR ───────────────────────────
23//
24// A verb that runs a verifier clean and reports "27 checks passed" is exactly the
25// evidence that has been lying. So the fixture repository contains a DELIBERATELY
26// DEAD BREAK: `verify_fixdead.mjs` declares `--break dead` and its `dead` mode
27// does nothing whatsoever. A run of that verifier must come back saying, in its
28// own trailer, that one break proved nothing — and must name it. That is check 7,
29// and it is the whole feature.
30//
31// node dev/verify_verifyverb.mjs the checks
32// node dev/verify_verifyverb.mjs --break realname 3 fails: a real name is sent where a bad one is expected
33// node dev/verify_verifyverb.mjs --break declaredbreak 5 fails: a declared break is sent where an invented one is expected
34// node dev/verify_verifyverb.mjs --break proveclean 6 fails: every break is run where clean_only is expected
35// node dev/verify_verifyverb.mjs --break liveinstrument 7 fails: the dead break is made to bite
36// node dev/verify_verifyverb.mjs --break silentdead 8 fails: the dead break prints an extra line
37// node dev/verify_verifyverb.mjs --break noshot 9 fails: the fixture writes no picture
38// node dev/verify_verifyverb.mjs --break committed 10 fails: the untracked fixture is committed
39// node dev/verify_verifyverb.mjs --break slander 10b fails: the committed fixture is edited after its commit
40// node dev/verify_verifyverb.mjs --break noedit 10c fails: the edited fixture is left as the commit wrote it
41// node dev/verify_verifyverb.mjs --break nodev 1 fails: the dev directory is out of the way at the handshake
42// node dev/verify_verifyverb.mjs --break deadlive 7d fails: the control verifier's second break is made dead
43// node dev/verify_verifyverb.mjs --break ghost 12 fails: the fixture leaves no trace on disk
44// node dev/verify_verifyverb.mjs --break wrongjournal 14 fails: the journal is read from an empty directory
45//
46// Each break damages ONE thing and reddens the check named beside it. THREE checks
47// have no break, and it is worth saying which rather than leaving a reader to
48// count. Check 11 -- that the report says it ran outside the command fence -- is a
49// constant in `hand/src/verify.rs` and nothing out here can reach it. Check 1b --
50// that `ext/hand.js` and the hand agree on the protocol version -- reads both
51// shipped numbers, and damaging either of them is damaging the tree rather than
52// this file. Check 1a --
53// that the binary being driven is this source and not an older build -- has been
54// demonstrated by hand (`DAIMOND_HAND_BIN=hand/target/release/daimond-hand` turns
55// eighteen checks red against a binary from three days before the verb existed)
56// and is deliberately NOT a break mode: a break that aborts the run leaves every
57// later check missing rather than failing, which is the shape of instrument this
58// whole file exists to warn about.
59//
60// The hand binary: `DAIMOND_HAND_BIN` if set, else the first of
61// `hand/target/{release,debug}` or this slot's cache that is NEWER than `hand/src`,
62// else built. A stale binary is refused rather than driven -- see `newestSource`.
63import fs from 'node:fs';
64import os from 'node:os';
65import path from 'node:path';
66import { spawn, spawnSync } from 'node:child_process';
67import { fileURLToPath } from 'node:url';
68
69// Derived, never written down: gate.sh runs the suite inside a git worktree at a
70// different path, and an absolute path here would measure the main tree.
71const HERE = path.dirname(fileURLToPath(import.meta.url));
72const ROOT = path.dirname(HERE);
73
74const BI = process.argv.indexOf('--break');
75const BEQ = process.argv.find(a => a.startsWith('--break='));
76const BREAK = BEQ ? BEQ.split('=')[1] : (BI >= 0 ? (process.argv[BI + 1] || '') : '');
77const KNOWN = ['nodev', 'realname', 'declaredbreak', 'proveclean', 'liveinstrument', 'deadlive',
78 'silentdead', 'noshot', 'ghost', 'committed', 'slander', 'noedit', 'wrongjournal'];
79if (BREAK && !KNOWN.includes(BREAK)) {
80 console.error(`unknown break '${BREAK}'; known: ${KNOWN.join(', ')}`);
81 process.exit(2);
82}
83if (BREAK) console.log(`\n*** RUNNING UNDER --break ${BREAK}: failures below are the point ***\n`);
84
85const ok = [], bad = [];
86const check = (name, pass, detail) => {
87 (pass ? ok : bad).push(name);
88 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
89};
90const note = (line) => console.log(' .. ' + line);
91
92// ── The scratch tree ────────────────────────────────────────────────
93//
94// Under the home cache and never /tmp: that is a tmpfs here, its pages are charged
95// to whoever wrote them, and filling it has taken this machine down before.
96const BASE = path.join(os.homedir(), '.cache/daimond/lane-verifyverb/run');
97const GRANT = path.join(BASE, 'grant'); // what the hand is granted
98const JOURNAL = path.join(BASE, 'journal'); // OUTSIDE the grant, as the hand requires
99const DEV = path.join(GRANT, 'dev');
100
101fs.rmSync(BASE, { recursive: true, force: true });
102fs.mkdirSync(DEV, { recursive: true });
103
104// A nonce this run generated a moment ago. A fixture that prints it cannot have
105// been stood in for: nothing but a process that read this file could know it.
106const NONCE = 'n' + Math.random().toString(36).slice(2) + Date.now().toString(36);
107fs.writeFileSync(path.join(DEV, 'nonce.txt'), NONCE + '\n');
108
109/// The line every fixture prints its checks with, copied from the real ones.
110const HELPER = `const say = (n, pass, detail) => console.log((pass ? ' ok ' : ' FAIL ') + n + (detail ? ' \\u2014 ' + detail : ''));
111const bi = process.argv.indexOf('--break');
112const BRK = bi >= 0 ? (process.argv[bi + 1] || '') : '';
113`;
114
115// A fixture whose breaks BOTH BITE. The control: it is what proves that the verb
116// can tell a live instrument from a dead one, and that check 7 is not simply
117// reporting "dead" about everything.
118fs.writeFileSync(path.join(DEV, 'verify_fixlive.mjs'), `// A fixture. Not a check of anything.
119// node dev/verify_fixlive.mjs --break bites # 'the nonce is read' goes red
120// node dev/verify_fixlive.mjs --break second # 'the second check' goes red
121import fs from 'node:fs';
122${HELPER}const nonce = fs.readFileSync('dev/nonce.txt', 'utf8').trim();
123say('the fixture ran', true);
124say('the nonce is read', BRK !== 'bites', BRK === 'bites' ? 'not read' : nonce);
125${BREAK === 'deadlive' ? "say('the second check', true);" : "say('the second check', BRK !== 'second');"}
126`);
127
128// **THE POINT OF THE FILE.** `dead` is declared and does nothing at all, so its
129// run is byte-for-byte the clean run's. An instrument that cannot fail.
130const deadBody = BREAK === 'liveinstrument'
131 ? `say('the second check', BRK !== 'dead');` // now it bites, so nothing is dead
132 : (BREAK === 'silentdead'
133 ? `if (BRK === 'dead') console.log('a line the clean run does not print');\nsay('the second check', true);`
134 : `say('the second check', true);`);
135const shotBody = BREAK === 'noshot'
136 ? ''
137 : `fs.mkdirSync('dev/shots', { recursive: true });\nfs.writeFileSync('dev/shots/fixture.png', nonce);`;
138fs.writeFileSync(path.join(DEV, 'verify_fixdead.mjs'), `// A fixture with a DELIBERATELY DEAD BREAK. Not a check of anything.
139// node dev/verify_fixdead.mjs --break bites # 'the nonce is read' goes red
140// node dev/verify_fixdead.mjs --break dead # does nothing at all, on purpose
141import fs from 'node:fs';
142${HELPER}const nonce = fs.readFileSync('dev/nonce.txt', 'utf8').trim();
143say('the fixture ran', true);
144say('the nonce is read', BRK !== 'bites', BRK === 'bites' ? 'not read' : nonce);
145${deadBody}
146${BREAK === 'ghost' ? '' : "fs.writeFileSync('dev/ran-' + (BRK || 'clean') + '.txt', nonce);"}
147${shotBody}
148`);
149
150// ── The three provenance fixtures ───────────────────────────────────
151//
152// REVIEW.md 1.21. A verifier runs OUTSIDE the command fence and the whole of the
153// reason is that its bytes came with the checkout; until 2026-08-25 nothing
154// enforced that, so a daimon could write `dev/verify_x.mjs` with `file_write` --
155// every turn may, the granted root is writable -- and have it run unfenced.
156//
157// Each of these three is used by ONE check and by nothing else, so the break
158// aimed at that check damages one thing. Reusing `fixlive` would refuse every run
159// of the control verifier and redden half this file, which is the shape of
160// instrument this whole file exists to warn against.
161
162// NOT in git. Once that only meant a gate building its tree with `git worktree
163// add` would never run it; now it means the verb refuses it.
164fs.writeFileSync(path.join(DEV, 'verify_fixloose.mjs'), `// A fixture that is not tracked.
165import fs from 'node:fs';
166${HELPER}say('the loose fixture ran', true);
167`);
168
169// Committed and left alone: the file that IS the commit, which must still run.
170fs.writeFileSync(path.join(DEV, 'verify_fixclean.mjs'), `// A fixture that is committed.
171import fs from 'node:fs';
172${HELPER}say('the committed fixture ran', true);
173`);
174
175// Committed and then edited, below, after the commit. `git ls-files
176// --error-unmatch` goes on exiting 0 for this file while `git diff --quiet HEAD`
177// exits 1, which is the whole of why the question is content and not membership.
178fs.writeFileSync(path.join(DEV, 'verify_fixedit.mjs'), `// A fixture that is edited after its commit.
179import fs from 'node:fs';
180${HELPER}say('the edited fixture ran', true);
181`);
182
183const git = (...args) => spawnSync('git', ['-C', GRANT, ...args],
184 { stdio: 'ignore', env: { ...process.env, GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_SYSTEM: '/dev/null' } });
185git('init', '-q');
186git('config', 'user.email', 'fixture@example.invalid');
187git('config', 'user.name', 'Fixture');
188git('add', 'dev/verify_fixlive.mjs', 'dev/verify_fixdead.mjs', 'dev/verify_fixclean.mjs',
189 'dev/verify_fixedit.mjs', 'dev/nonce.txt');
190// COMMITTED and not merely added, because the check this damages is about the
191// bytes and not about the index -- `git add` alone leaves the file refused, so a
192// break that only staged it would damage nothing and read as green.
193if (BREAK === 'committed') {
194 git('add', 'dev/verify_fixloose.mjs');
195}
196git('commit', '-q', '-m', 'fixtures');
197const editLine = '// and now it is mine\n';
198// Unconditional: `fixedit` is the edited case, and 'noedit' takes the edit away.
199if (BREAK !== 'noedit') fs.appendFileSync(path.join(DEV, 'verify_fixedit.mjs'), editLine);
200// And the one that damages 10b, by making the unslandered file slanderable.
201if (BREAK === 'slander') fs.appendFileSync(path.join(DEV, 'verify_fixclean.mjs'), editLine);
202
203// ── The hand ────────────────────────────────────────────────────────
204
205/// When the newest of the hand's own sources was last changed.
206///
207/// **The staleness guard, and it is not decoration.** `hand/target/release/daimond-hand`
208/// on this machine was three days older than the verb, and a stale binary answers
209/// `verify` with "there is no request called verify" — which every check below would
210/// have read as a refusal and half of them would have PASSED on. A test that measures
211/// the wrong binary proves nothing in either direction, and it fails silently towards
212/// green, which is the worst way for it to fail.
213function newestSource() {
214 let newest = 0;
215 const look = (dir) => {
216 let names = [];
217 try { names = fs.readdirSync(dir); } catch (e) { return; }
218 for (const n of names) {
219 const f = path.join(dir, n);
220 let st;
221 try { st = fs.statSync(f); } catch (e) { continue; }
222 if (st.isDirectory()) look(f);
223 else if (/\.(rs|toml)$/.test(n)) newest = Math.max(newest, st.mtimeMs);
224 }
225 };
226 look(path.join(ROOT, 'hand/src'));
227 try { newest = Math.max(newest, fs.statSync(path.join(ROOT, 'hand/Cargo.toml')).mtimeMs); }
228 catch (e) { /* no manifest, and the build below will say so */ }
229 return newest;
230}
231
232/// The hand binary to drive, built if the ones lying about are older than the source.
233///
234/// **The ambient CARGO_TARGET_DIR is deliberately NOT used.** The hand is its own
235/// workspace root; building it into a target directory the app is also built into
236/// puts two differently resolved copies of one crate in the same place, and the
237/// binary then links a mix. The tell is rustc's "multiple different versions of
238/// crate X" while `cargo tree -d` shows one. So the hand gets a directory of its
239/// own, named for it, and `DAIMOND_HAND_BIN` skips the question entirely.
240function handBinary() {
241 if (process.env.DAIMOND_HAND_BIN) return { bin: process.env.DAIMOND_HAND_BIN, fresh: true, why: 'named by DAIMOND_HAND_BIN' };
242 const src = newestSource();
243 const target = process.env.DAIMOND_HAND_TARGET_DIR
244 || path.join(os.homedir(), '.cache/cargo-targets', process.env.RC_SLOT || 'solo', 'daimond-hand');
245 // What `hand/install/README.md` names and what verify_handreal.mjs builds, reused
246 // where it is already there AND newer than the source.
247 const seen = [];
248 for (const c of [
249 path.join(ROOT, 'hand/target/release/daimond-hand'),
250 path.join(ROOT, 'hand/target/debug/daimond-hand'),
251 path.join(target, 'debug', 'daimond-hand'),
252 ]) {
253 let st;
254 try { st = fs.statSync(c); } catch (e) { continue; }
255 if (st.mtimeMs >= src) return { bin: c, fresh: true, why: 'newer than hand/src' };
256 seen.push(`${c} is ${Math.round((src - st.mtimeMs) / 1000)}s older than hand/src`);
257 }
258 note('the hand binaries here are older than its source; building into ' + target);
259 const r = spawnSync('cargo', ['build', '--manifest-path', path.join(ROOT, 'hand/Cargo.toml')],
260 { cwd: ROOT, stdio: 'inherit', env: { ...process.env, CARGO_TARGET_DIR: target } });
261 const built = path.join(target, 'debug', 'daimond-hand');
262 if (r.status !== 0 || !fs.existsSync(built)) {
263 return { bin: null, fresh: false, why: seen.join('; ') || 'the hand did not build' };
264 }
265 return { bin: built, fresh: true, why: 'built here' };
266}
267
268const { bin: HAND, fresh: FRESH, why: WHY } = handBinary();
269check('1a the hand binary is there, and is this source rather than an older one',
270 !!HAND && fs.existsSync(HAND) && FRESH, `${HAND || 'none'} — ${WHY}`);
271if (!HAND || !fs.existsSync(HAND) || !FRESH) {
272 console.log(`\n${ok.length} ok, ${bad.length} failed`);
273 process.exit(1);
274}
275
276const LE = os.endianness() === 'LE';
277
278/// One conversation with a fresh hand process.
279///
280/// A process per exchange, which is what Chrome does: it starts a host for each
281/// port and kills it when the port closes. It also means one request cannot leave
282/// state behind for the next, so a check that passed because of an earlier one is
283/// not a failure mode this file has.
284function talk(messages, ms = 60000) {
285 return new Promise((resolve) => {
286 const child = spawn(HAND, [], {
287 cwd: BASE,
288 env: {
289 ...process.env,
290 DAIMOND_HAND_ROOT: GRANT,
291 DAIMOND_HAND_JOURNAL_DIR: JOURNAL,
292 },
293 stdio: ['pipe', 'pipe', 'pipe'],
294 });
295 const got = [];
296 let buf = Buffer.alloc(0);
297 let err = '';
298 let done = false;
299 const finish = () => {
300 if (done) return;
301 done = true;
302 clearTimeout(timer);
303 try { child.kill('SIGKILL'); } catch (e) { /* already gone */ }
304 resolve({ msgs: got, stderr: err });
305 };
306 const timer = setTimeout(finish, ms);
307 child.stderr.on('data', (d) => { err += d.toString(); });
308 child.stdout.on('data', (d) => {
309 buf = Buffer.concat([buf, d]);
310 for (;;) {
311 if (buf.length < 4) return;
312 const n = LE ? buf.readUInt32LE(0) : buf.readUInt32BE(0);
313 if (buf.length < 4 + n) return;
314 const body = buf.subarray(4, 4 + n).toString('utf8');
315 buf = buf.subarray(4 + n);
316 try { got.push(JSON.parse(body)); } catch (e) { got.push({ t: '__unparseable', body }); }
317 // `ended` and `refused` are the two ways an answer finishes.
318 const last = got[got.length - 1];
319 // `error` is terminal here as well as `ended` and `refused`: a hand that does
320 // not know the message answers with one and never with an ending, and waiting
321 // for an ending that is not coming is a test that hangs rather than fails.
322 if (last && (last.t === 'ended' || last.t === 'refused' || last.t === 'error')) {
323 // Give the writer a moment to flush anything queued behind it.
324 setTimeout(finish, 150);
325 }
326 }
327 });
328 child.on('error', finish);
329 child.on('exit', () => setTimeout(finish, 50));
330 for (const m of messages) {
331 const body = Buffer.from(JSON.stringify(m), 'utf8');
332 const head = Buffer.alloc(4);
333 if (LE) head.writeUInt32LE(body.length, 0); else head.writeUInt32BE(body.length, 0);
334 child.stdin.write(Buffer.concat([head, body]));
335 }
336 });
337}
338
339/// The protocol version the PAGE announces, read out of the page's own relay.
340///
341/// Written down here as `1` until 2026-08-25, when `hand/src/lib.rs` went to 2 and
342/// `ext/hand.js` went with it. Every exchange below then met `proto_refusal` and
343/// twenty-three checks went red at once, none of them about anything this file
344/// measures. A constant restated in a third place is a third place to forget.
345///
346/// It is read from `ext/hand.js` rather than from the hand, deliberately: this file
347/// STANDS IN FOR THE PAGE, and a stand-in that asked the hand what to say could not
348/// be wrong about it. Reading the page's own number means the two shipped halves
349/// have to agree, and check 1b below says so in one line when they do not.
350function pageProto() {
351 const src = fs.readFileSync(path.join(ROOT, 'ext/hand.js'), 'utf8');
352 const m = /^\s*const PROTO\s*=\s*(\d+)\s*;/m.exec(src);
353 return m ? Number(m[1]) : null;
354}
355
356/// What the hand itself says it speaks, from the mode a person runs.
357function handProto(bin) {
358 const r = spawnSync(bin, ['--report'], { encoding: 'utf8', timeout: 30000 });
359 const m = /^protocol (\d+)$/m.exec((r.stdout || '') + (r.stderr || ''));
360 return m ? Number(m[1]) : null;
361}
362
363const PAGE_PROTO = pageProto();
364const HAND_PROTO = handProto(HAND);
365check('1b the hand and the page agree on what protocol they speak',
366 PAGE_PROTO !== null && PAGE_PROTO === HAND_PROTO,
367 `ext/hand.js says ${PAGE_PROTO}, ${path.basename(HAND)} --report says ${HAND_PROTO}`);
368
369const HELLO = { t: 'hello', proto: PAGE_PROTO, client: 'verify_verifyverb' };
370
371/// One verify request, and everything the hand said about it.
372async function verify(req, ms) {
373 const r = await talk([HELLO, { t: 'verify', id: 'v1', timeout_ms: 60000, break: null, ...req }], ms);
374 const hello = r.msgs.find(m => m.t === 'hello');
375 const refused = r.msgs.find(m => m.t === 'refused');
376 const ended = r.msgs.find(m => m.t === 'ended');
377 const out = r.msgs.filter(m => m.t === 'chunk' && m.stream === 'out').map(m => m.data).join('');
378 const progress = r.msgs.filter(m => m.t === 'chunk' && m.stream === 'err').map(m => m.data).join('');
379 return { hello, refused, ended, out, progress, stderr: r.stderr, msgs: r.msgs };
380}
381
382/// The trailer line, which is where all three numbers live.
383const trailerOf = (out) =>
384 (out.split('\n').reverse().find(l => l.trim().startsWith('[verify:')) || '').trim();
385
386// ── 1. The handshake says whether this folder has verifiers ─────────
387//
388// No caller break: the capability is computed in the hand from the directory, and
389// there is nothing out here that can make it lie.
390
391{
392 // Surgical: the directory goes back before anything else asks about it, so this
393 // break reddens check 1 and nothing else.
394 const aside = path.join(GRANT, 'dev-aside');
395 if (BREAK === 'nodev') fs.renameSync(DEV, aside);
396 const r = await talk([HELLO, { t: 'bye' }]);
397 if (BREAK === 'nodev') fs.renameSync(aside, DEV);
398 const caps = (r.msgs.find(m => m.t === 'hello') || {}).caps || [];
399 check('1 the handshake says this folder has verifiers', caps.includes('verify:dev'),
400 JSON.stringify(caps));
401}
402
403// ── 2, 3, 4. A name is a selector, and nothing else ─────────────────
404
405{
406 const r = await verify({ name: BREAK === 'realname' ? 'fixlive' : 'nosuchthing', breaks: 'none' });
407 check('2 a name that is not a verifier is refused',
408 !!r.refused && /Refused:/.test(r.refused.reason || ''),
409 r.refused ? r.refused.reason.slice(0, 110) : `no refusal; ended=${JSON.stringify(r.ended)}`);
410 check('3 the refusal says nothing was run',
411 !!r.refused && /Nothing was run/.test(r.refused.reason || ''),
412 r.refused ? r.refused.reason.slice(0, 110) : 'no refusal');
413}
414
415{
416 // A path, which is the shape a model reaches for first. Refused on its
417 // alphabet, before the directory is read at all.
418 const r = await verify({ name: BREAK === 'realname' ? 'fixlive' : 'dev/verify_fixlive.mjs', breaks: 'none' });
419 check('4 a path is refused as a name',
420 !!r.refused && /not a verifier name|does not know the message/.test(r.refused.reason || ''),
421 r.refused ? r.refused.reason.slice(0, 110) : 'no refusal');
422}
423
424// ── 5. A break must be one the verifier itself declares ─────────────
425
426{
427 const r = await verify({
428 name: 'fixdead',
429 breaks: 'one',
430 break: BREAK === 'declaredbreak' ? 'bites' : 'inventedbreak',
431 });
432 const why = r.refused ? r.refused.reason : '';
433 check('5 a break the verifier does not declare is refused',
434 !!r.refused && /does not declare a break/.test(why),
435 r.refused ? why.slice(0, 120) : `no refusal; trailer=${trailerOf(r.out)}`);
436 check('5b the refusal lists the breaks it does declare',
437 /\bbites\b/.test(why) && /\bdead\b/.test(why),
438 why.slice(0, 160) || 'no refusal');
439}
440
441// ── 6. A clean-only run is labelled, in the words a model repeats ───
442
443{
444 const r = await verify({ name: 'fixlive', breaks: BREAK === 'proveclean' ? 'all' : 'none' });
445 const t = trailerOf(r.out);
446 check('6 a clean-only run is labelled NOT PROVEN', /NOT PROVEN/.test(t), t || r.out.slice(0, 200));
447 check('6b and says it is not evidence', /not evidence/i.test(r.out),
448 t || r.out.slice(0, 200));
449}
450
451// ── 7. THE FEATURE: a dead break is counted and named ───────────────
452
453{
454 const r = await verify({ name: 'fixdead', breaks: 'all' }, 120000);
455 const t = trailerOf(r.out);
456 check('7 a break that reddens nothing is counted in the third number',
457 /1 breaks proved nothing/.test(t), t || r.out.slice(0, 300));
458 check('7b the live break beside it is counted red',
459 /1 breaks confirmed red/.test(t), t || r.out.slice(0, 300));
460 check('7c the dead break is named so the check it aims at can be disowned',
461 /PROVED NOTHING/.test(r.out) && /^BREAK dead\b/m.test(r.out),
462 (r.out.match(/^BREAK .*$/gm) || []).join(' | ').slice(0, 200));
463 check('8 a dead break whose output is the clean run\'s is said to be exactly that',
464 /its output was the clean run's/.test(r.out),
465 (r.out.match(/^BREAK dead.*$/m) || ['(no dead line)'])[0].slice(0, 200));
466 check('9 pictures the run wrote are named by path',
467 /dev\/shots\/fixture\.png/.test(r.out),
468 (r.out.match(/dev\/shots\/[^\s]*/g) || []).join(' ') || 'none named');
469 check('11 the report says it ran outside the command fence',
470 /OUTSIDE the command fence/.test(r.out), r.out.slice(0, 120));
471 // A report is text and text can be fabricated. What cannot is a file on this
472 // disk holding a nonce generated seconds ago: only a process that read
473 // dev/nonce.txt could have written it, and there is one per run, so the
474 // sequence really made three of them.
475 const ran = ['clean', 'bites', 'dead']
476 .map(w => path.join(DEV, `ran-${w}.txt`))
477 .map(f => { try { return fs.readFileSync(f, 'utf8').trim(); } catch (e) { return ''; } });
478 check('12 all three runs really happened, each leaving this run\'s nonce on disk',
479 ran.length === 3 && ran.every(v => v === NONCE),
480 `clean=${ran[0] === NONCE} bites=${ran[1] === NONCE} dead=${ran[2] === NONCE}`);
481 // The exit status is the tri-state: 0 proved, 1 the code failed, 2 unproven.
482 check('13 a sequence holding a dead break does not exit as proved',
483 !!r.ended && r.ended.exit === 2,
484 r.ended ? `exit ${r.ended.exit}` : 'no ending');
485}
486
487// ── 7d. The control: every break biting IS reported as proved ───────
488//
489// Without this, check 7 would pass just as well on a verb that called everything
490// dead. Both halves are needed and only both together mean anything.
491
492{
493 const r = await verify({ name: 'fixlive', breaks: 'all' }, 120000);
494 const t = trailerOf(r.out);
495 check('7d a verifier whose breaks all bite is reported as proved',
496 /2 breaks confirmed red/.test(t) && /0 breaks proved nothing/.test(t),
497 t || r.out.slice(0, 300));
498 check('13b and it exits as proved',
499 !!r.ended && r.ended.exit === 0, r.ended ? `exit ${r.ended.exit}` : 'no ending');
500}
501
502// ── 10. Whose code is this -- ENFORCED, not merely reported ─────────
503//
504// REVIEW.md 1.21, and three checks because one would not do it. 10 is the
505// untracked case. 10c is the edited case, which membership cannot see at all:
506// `git ls-files --error-unmatch` exits 0 for a file with a comment appended while
507// `git diff --quiet HEAD` exits 1. And 10b is what keeps both honest -- a file
508// that IS the commit must still run, so a verb that refused everything would go
509// red here rather than reading as a pass.
510
511{
512 const r = await verify({ name: 'fixloose', breaks: 'none' });
513 const why = r.refused ? r.refused.reason : '';
514 check('10 a verifier git does not know is REFUSED, not run',
515 !!r.refused && /not this repository's committed code/.test(why),
516 r.refused ? why.slice(0, 140)
517 : `no refusal; report=${(r.out.split('\n')[0] || '').slice(0, 120)}`);
518 // A refusal a model cannot converge on costs the run anyway, so the sentence
519 // has to carry the fenced route, the tool that takes it, and what fenced
520 // running cannot do -- or a daimon reads the fence as its script being broken.
521 check('10a and the refusal hands over the fenced way to run it anyway',
522 /\["node","dev\/verify_fixloose\.mjs"\]/.test(why) && /'run'/.test(why)
523 && /browser/.test(why) && /commit the file/.test(why),
524 why.slice(0, 220) || 'no refusal');
525}
526{
527 const r = await verify({ name: 'fixclean', breaks: 'none' });
528 check('10b and one that IS the commit is not slandered',
529 !r.refused && /byte for byte the commit's/.test(r.out),
530 r.refused ? r.refused.reason.slice(0, 140) : (r.out.split('\n')[0] || '').slice(0, 160));
531}
532{
533 const r = await verify({ name: 'fixedit', breaks: 'none' });
534 check('10c an edit made after the commit is seen, which the index cannot show',
535 !!r.refused && /not what the commit holds/.test(r.refused.reason || ''),
536 r.refused ? r.refused.reason.slice(0, 140)
537 : `RAN -- the check followed the index; report=${(r.out.split('\n')[0] || '').slice(0, 100)}`);
538}
539
540// ── 14. The journal holds the node command, not a verb ──────────────
541
542{
543 const where = BREAK === 'wrongjournal' ? path.join(BASE, 'empty') : JOURNAL;
544 if (BREAK === 'wrongjournal') fs.mkdirSync(where, { recursive: true });
545 const files = fs.existsSync(where)
546 ? fs.readdirSync(where).filter(f => f.endsWith('.jsonl') || f.includes('journal'))
547 : [];
548 const text = files.map(f => {
549 try { return fs.readFileSync(path.join(where, f), 'utf8'); } catch (e) { return ''; }
550 }).join('\n');
551 check('14 the journal records the real node command line',
552 /verify_fixdead\.mjs/.test(text) && /"?fence:none"?/.test(text),
553 files.join(', ') || 'no journal files found');
554}
555
556// ── The summary ─────────────────────────────────────────────────────
557
558console.log(`\n${ok.length} ok, ${bad.length} failed`);
559if (BREAK) {
560 console.log(bad.length
561 ? `\nbreak '${BREAK}' produced failures, as it must.`
562 : `\nBREAK '${BREAK}' CHANGED NOTHING — the check it targets is not proving anything.`);
563 process.exit(bad.length ? 0 : 1);
564}
565process.exit(bad.length ? 1 : 0);