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". |
| 18 | import fs from 'node:fs'; |
| 19 | import os from 'node:os'; |
| 20 | import path from 'node:path'; |
| 21 | import crypto from 'node:crypto'; |
| 22 | import { spawnSync } from 'node:child_process'; |
| 23 | import { fileURLToPath } from 'node:url'; |
| 24 | |
| 25 | const HERE = path.dirname(fileURLToPath(import.meta.url)); |
| 26 | const ROOT = path.join(HERE, '..'); |
| 27 | const PATCH = path.join(os.homedir(), '.cache/daimond/lane-ag-tasks/task1_insert_before.patch'); |
| 28 | const CAP = path.join(os.homedir(), '.cache/daimond/lane-ag-tasks/capture.json'); |
| 29 | const TREES = path.join(os.homedir(), '.cache/daimond-change/trees'); |
| 30 | const GRANT = path.join(os.homedir(), '.cache/daimond-change/work'); |
| 31 | |
| 32 | const git = (...a) => spawnSync('git', ['-C', ROOT, ...a], { encoding: 'utf8' }); |
| 33 | const bad = []; |
| 34 | const 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. |
| 38 | function 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 | |
| 54 | function 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 | |
| 62 | const dirty = git('status', '--porcelain').stdout.trim(); |
| 63 | if (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 | } |
| 68 | if (!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 ── |
| 74 | if (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 | } |
| 79 | const direct = run(['--world', '2', '--dry', '--apply-answer'], 'direct'); |
| 80 | say(direct.ok, 'the direct path passes with the change in the tree'); |
| 81 | say(/PASS\s+\d+ test\(s\), 0 failing/.test(direct.out), 'the direct path is green on the answer'); |
| 82 | const dMeasure = treeHash(path.join(TREES, 'measure')); |
| 83 | const dWork = treeHash(path.join(GRANT, 'repo')); |
| 84 | |
| 85 | // ── 2. Capture, restore, and run from the capture ── |
| 86 | const cap = run(['--world', '2', '--capture', CAP], 'capture'); |
| 87 | say(cap.ok && fs.existsSync(CAP), 'the split can be captured to a file'); |
| 88 | git('checkout', '--', '.'); |
| 89 | say(!git('status', '--porcelain').stdout.trim(), 'the worktree is back at HEAD before the engine is built'); |
| 90 | |
| 91 | const from = run(['--world', '2', '--dry', '--apply-answer', '--from', CAP], 'from'); |
| 92 | say(from.ok, 'the captured path passes against a clean worktree'); |
| 93 | say(/PASS\s+\d+ test\(s\), 0 failing/.test(from.out), 'the captured path is green on the answer'); |
| 94 | const fMeasure = treeHash(path.join(TREES, 'measure')); |
| 95 | const fWork = treeHash(path.join(GRANT, 'repo')); |
| 96 | |
| 97 | // ── 3. THE CLAIM ── |
| 98 | say(dMeasure === fMeasure, `measure/ is the same tree either way (${dMeasure.slice(0, 12)})`); |
| 99 | say(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 ── |
| 102 | git('apply', '--whitespace=nowarn', PATCH); |
| 103 | const onDirty = run(['--world', '2', '--dry', '--from', CAP], 'dirty'); |
| 104 | say(!onDirty.ok && /CLEAN worktree/.test(onDirty.out), '--from refuses a dirty worktree'); |
| 105 | git('checkout', '--', '.'); |
| 106 | const moved = JSON.parse(fs.readFileSync(CAP, 'utf8')); |
| 107 | moved.head = '0'.repeat(40); |
| 108 | fs.writeFileSync(CAP + '.moved', JSON.stringify(moved)); |
| 109 | const onMoved = run(['--world', '2', '--dry', '--from', CAP + '.moved'], 'moved'); |
| 110 | say(!onMoved.ok && /captured at/.test(onMoved.out), '--from refuses a capture taken at another HEAD'); |
| 111 | fs.rmSync(CAP + '.moved', { force: true }); |
| 112 | |
| 113 | console.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.'); |
| 117 | process.exit(bad.length ? 1 : 0); |