Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_changecapture.mjs

5.8 KiB, 1 run

created by r2519314175:263, 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_changecapture.mjs — --capture then --from builds the same three trees as a
2// direct run, so the answer can be kept out of the tree that builds the engine.
3//
4// WHY THIS EXISTS. `dev/reflux_change.mjs` reads the working tree twice -- `laneChange()`
5// and `splitChange`'s `#[cfg(test)]` scan -- and the engine staleguard runs 163 lines
6// later. A run with the change still in the tree therefore either dies at the guard or
7// forces a rebuild that compiles THE ANSWER into the engine the daimon speaks through,
8// which for a `src/tools.rs` change hands the daimon the API it is being asked to invent.
9// `--capture` writes the split down; the lane restores HEAD; `--from` runs clean.
10//
11// The claim that needs proving is that the two paths are the SAME MEASUREMENT. A capture
12// that quietly produced a different `work/` would be worse than no capture at all, because
13// every number after it would be about a tree nobody looked at.
14//
15// It applies a patch of its own and puts the tree back with `git checkout --`, and it
16// REFUSES to start on a dirty worktree so that "put it back" can never mean "throw the
17// lane's work away".
18import fs from 'node:fs';
19import os from 'node:os';
20import path from 'node:path';
21import crypto from 'node:crypto';
22import { spawnSync } from 'node:child_process';
23import { fileURLToPath } from 'node:url';
24
25const HERE = path.dirname(fileURLToPath(import.meta.url));
26const ROOT = path.join(HERE, '..');
27const PATCH = path.join(os.homedir(), '.cache/daimond/lane-ag-tasks/task1_insert_before.patch');
28const CAP = path.join(os.homedir(), '.cache/daimond/lane-ag-tasks/capture.json');
29const TREES = path.join(os.homedir(), '.cache/daimond-change/trees');
30const GRANT = path.join(os.homedir(), '.cache/daimond-change/work');
31
32const git = (...a) => spawnSync('git', ['-C', ROOT, ...a], { encoding: 'utf8' });
33const bad = [];
34const say = (ok, what) => { console.log(` ${ok ? 'ok ' : 'FAIL'} ${what}`); if (!ok) bad.push(what); };
35
36/// Every file's path and content, as one hash. Build artefacts are excluded: a target
37/// directory differs between two runs for reasons that are nothing to do with the split.
38function treeHash(dir) {
39 const h = crypto.createHash('sha256');
40 const walk = (d, rel) => {
41 let ents;
42 try { ents = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
43 for (const e of ents.sort((a, b) => a.name < b.name ? -1 : 1)) {
44 if (e.name === '.git' || e.name === 'target' || e.name === '.cargo') continue;
45 const p = path.join(d, e.name), r = rel ? `${rel}/${e.name}` : e.name;
46 if (e.isDirectory()) walk(p, r);
47 else if (e.isFile()) { h.update(r); h.update(fs.readFileSync(p)); }
48 }
49 };
50 walk(dir, '');
51 return h.digest('hex');
52}
53
54function run(args, label) {
55 const r = spawnSync('node', [path.join(ROOT, 'dev/reflux_change.mjs'), ...args],
56 { cwd: ROOT, encoding: 'utf8', env: { ...process.env } });
57 const out = (r.stdout || '') + (r.stderr || '');
58 if (r.status !== 0) console.log(out.split('\n').slice(-6).join('\n'));
59 return { ok: r.status === 0, out, label };
60}
61
62const dirty = git('status', '--porcelain').stdout.trim();
63if (dirty) {
64 console.log('verify_changecapture: REFUSED — this worktree has uncommitted changes, and this '
65 + 'check puts the tree back with `git checkout --`. It will not do that to somebody\'s work.');
66 process.exit(2);
67}
68if (!fs.existsSync(PATCH)) {
69 console.log(`verify_changecapture: SKIPPED — no fixture patch at ${PATCH}`);
70 process.exit(0);
71}
72
73// ── 1. The direct path, with the change in the tree ──
74if (git('apply', '--whitespace=nowarn', PATCH).status !== 0) {
75 console.log('verify_changecapture: the fixture patch does not apply to this HEAD; it was '
76 + 'taken at an older one. Retake it or delete it.');
77 process.exit(2);
78}
79const direct = run(['--world', '2', '--dry', '--apply-answer'], 'direct');
80say(direct.ok, 'the direct path passes with the change in the tree');
81say(/PASS\s+\d+ test\(s\), 0 failing/.test(direct.out), 'the direct path is green on the answer');
82const dMeasure = treeHash(path.join(TREES, 'measure'));
83const dWork = treeHash(path.join(GRANT, 'repo'));
84
85// ── 2. Capture, restore, and run from the capture ──
86const cap = run(['--world', '2', '--capture', CAP], 'capture');
87say(cap.ok && fs.existsSync(CAP), 'the split can be captured to a file');
88git('checkout', '--', '.');
89say(!git('status', '--porcelain').stdout.trim(), 'the worktree is back at HEAD before the engine is built');
90
91const from = run(['--world', '2', '--dry', '--apply-answer', '--from', CAP], 'from');
92say(from.ok, 'the captured path passes against a clean worktree');
93say(/PASS\s+\d+ test\(s\), 0 failing/.test(from.out), 'the captured path is green on the answer');
94const fMeasure = treeHash(path.join(TREES, 'measure'));
95const fWork = treeHash(path.join(GRANT, 'repo'));
96
97// ── 3. THE CLAIM ──
98say(dMeasure === fMeasure, `measure/ is the same tree either way (${dMeasure.slice(0, 12)})`);
99say(dWork === fWork, `work/repo is the same tree either way (${dWork.slice(0, 12)})`);
100
101// ── 4. And the two refusals that keep --from honest ──
102git('apply', '--whitespace=nowarn', PATCH);
103const onDirty = run(['--world', '2', '--dry', '--from', CAP], 'dirty');
104say(!onDirty.ok && /CLEAN worktree/.test(onDirty.out), '--from refuses a dirty worktree');
105git('checkout', '--', '.');
106const moved = JSON.parse(fs.readFileSync(CAP, 'utf8'));
107moved.head = '0'.repeat(40);
108fs.writeFileSync(CAP + '.moved', JSON.stringify(moved));
109const onMoved = run(['--world', '2', '--dry', '--from', CAP + '.moved'], 'moved');
110say(!onMoved.ok && /captured at/.test(onMoved.out), '--from refuses a capture taken at another HEAD');
111fs.rmSync(CAP + '.moved', { force: true });
112
113console.log(bad.length
114 ? `\nverify_changecapture: ${bad.length} check(s) failed.`
115 : '\nverify_changecapture: both paths build the same three trees, and --from refuses a '
116 + 'dirty tree and a moved HEAD.');
117process.exit(bad.length ? 1 : 0);