|
| 1 | +#!/usr/bin/env node |
| 2 | +// One command a judge can paste. Runs every gate CI runs, in the same order, |
| 3 | +// with zero credentials and zero network. Optional toolchains SKIP loudly |
| 4 | +// rather than passing quietly β a gate that cannot run must never look green. |
| 5 | +// |
| 6 | +// Exit 0 only if every REQUIRED gate passed. |
| 7 | + |
| 8 | +import { spawnSync } from 'node:child_process'; |
| 9 | +import { readFileSync } from 'node:fs'; |
| 10 | + |
| 11 | +const B = '\x1b[1m', D = '\x1b[2m', G = '\x1b[32m', R = '\x1b[31m', Y = '\x1b[33m', X = '\x1b[0m'; |
| 12 | +const results = []; |
| 13 | + |
| 14 | +function has(cmd, args = ['--version']) { |
| 15 | + return spawnSync(cmd, args, { stdio: 'ignore' }).status === 0; |
| 16 | +} |
| 17 | + |
| 18 | +function gate({ name, required = true, skipIf, run, detail }) { |
| 19 | + if (skipIf) { |
| 20 | + results.push({ name, state: 'SKIP', detail: skipIf, required }); |
| 21 | + return; |
| 22 | + } |
| 23 | + const t = Date.now(); |
| 24 | + const ok = run(); |
| 25 | + results.push({ |
| 26 | + name, |
| 27 | + state: ok ? 'PASS' : 'FAIL', |
| 28 | + detail: `${detail} Β· ${((Date.now() - t) / 1000).toFixed(2)}s`, |
| 29 | + required, |
| 30 | + }); |
| 31 | +} |
| 32 | + |
| 33 | +const sh = (cmd, args) => |
| 34 | + spawnSync(cmd, args, { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' }); |
| 35 | + |
| 36 | +console.log(`\n${B}gavel Β· verify${X} ${D}every CI gate, no credentials, no network${X}\n`); |
| 37 | + |
| 38 | +// ββ 1 Β· the decision surface is pure ββββββββββββββββββββββββββββββββββββββββ |
| 39 | +// Invariant I10. assemble.mjs is injected verbatim into a sandboxed code node, |
| 40 | +// so an impure call would fail at runtime on-chain. Asserted here and in CI. |
| 41 | +gate({ |
| 42 | + name: 'Decision surface is pure', |
| 43 | + detail: 'src/assemble.mjs β no import/eval/fetch/Date.now/Math.random/process.env', |
| 44 | + run: () => { |
| 45 | + const s = readFileSync(new URL('../src/assemble.mjs', import.meta.url), 'utf8'); |
| 46 | + const banned = [ |
| 47 | + /^\s*import\s/m, /\bimport\s*\(/, /\beval\s*\(/, /\bnew\s+Function\s*\(/, |
| 48 | + /\bfetch\s*\(/, /\bMath\.random\s*\(/, /\bDate\.now\s*\(/, /\bprocess\.env/, |
| 49 | + ]; |
| 50 | + const hit = banned.filter((r) => r.test(s)); |
| 51 | + if (hit.length) console.error(` ${R}impure:${X} ${hit.map(String).join(', ')}`); |
| 52 | + return hit.length === 0; |
| 53 | + }, |
| 54 | +}); |
| 55 | + |
| 56 | +// ββ 2 Β· the test suite ββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| 57 | +gate({ |
| 58 | + name: 'JS test suite', |
| 59 | + detail: 'node --test', |
| 60 | + run: () => { |
| 61 | + const r = sh('node', ['--test']); |
| 62 | + const m = /^# pass (\d+)/m.exec(r.stdout ?? ''); |
| 63 | + if (m) console.log(` ${D}${m[1]} tests passed${X}`); |
| 64 | + if (r.status !== 0) console.error(r.stdout?.slice(-1500) ?? ''); |
| 65 | + return r.status === 0; |
| 66 | + }, |
| 67 | +}); |
| 68 | + |
| 69 | +// ββ 3 Β· the published numbers re-derive from committed data βββββββββββββββββ |
| 70 | +// This is the one that matters most: it proves the README's figures were |
| 71 | +// measured rather than asserted. |
| 72 | +gate({ |
| 73 | + name: 'Published survey figures re-derive', |
| 74 | + detail: 'survey/rederive.py β every published figure vs 1.3 MB of committed responses', |
| 75 | + skipIf: has('python3') ? null : 'python3 not found β install it to check the survey numbers', |
| 76 | + run: () => { |
| 77 | + const r = sh('python3', ['survey/rederive.py']); |
| 78 | + const n = (r.stdout?.match(/^\s*ok\s/gm) ?? []).length; |
| 79 | + if (n) console.log(` ${D}${n} figures re-derived${X}`); |
| 80 | + if (r.status !== 0) console.error(r.stdout?.slice(-1200) ?? ''); |
| 81 | + return r.status === 0; |
| 82 | + }, |
| 83 | +}); |
| 84 | + |
| 85 | +// ββ 4 Β· the rehearsal token βββββββββββββββββββββββββββββββββββββββββββββββββ |
| 86 | +// Optional: needs foundry. Dependency-free otherwise β no forge-std, no lib/. |
| 87 | +gate({ |
| 88 | + name: 'Solidity tests', |
| 89 | + required: false, |
| 90 | + detail: 'forge test β MockUSDC, 100% coverage on all four metrics', |
| 91 | + skipIf: has('forge') ? null : 'foundry not installed β optional, JS gates cover the decision surface', |
| 92 | + run: () => sh('forge', ['test']).status === 0, |
| 93 | +}); |
| 94 | + |
| 95 | +// ββ report ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ |
| 96 | +const pad = Math.max(...results.map((r) => r.name.length)); |
| 97 | +console.log(''); |
| 98 | +for (const r of results) { |
| 99 | + const tag = r.state === 'PASS' ? `${G}PASS${X}` : r.state === 'FAIL' ? `${R}FAIL${X}` : `${Y}SKIP${X}`; |
| 100 | + console.log(` ${tag} ${r.name.padEnd(pad)} ${D}${r.detail}${X}`); |
| 101 | +} |
| 102 | + |
| 103 | +const failed = results.filter((r) => r.state === 'FAIL' && r.required); |
| 104 | +const skipped = results.filter((r) => r.state === 'SKIP'); |
| 105 | +console.log(''); |
| 106 | +if (failed.length) { |
| 107 | + console.log(`${R}${B}VERIFY FAILED${X} β ${failed.length} required gate(s) did not pass.\n`); |
| 108 | + process.exit(1); |
| 109 | +} |
| 110 | +console.log( |
| 111 | + `${G}${B}VERIFY PASSED${X}` + |
| 112 | + (skipped.length ? ` ${Y}(${skipped.length} optional gate skipped)${X}` : '') + |
| 113 | + `\n${D}Nothing above touched the network or read a credential.${X}\n`, |
| 114 | +); |
0 commit comments