Spec 01 1.4.6: the constants table gains ACCEPT_TAG, LANES, the (c'') window-cap literal and the first-refused reading of MAX_SATURATED. tools/ci/spec-constants-check.mjs joins the gate: every "Constant | Value | Where" table of spec 01 against the crate's pub const items (an expression over constants evaluated, a byte string compared, a pending row skipped while the constant is absent), known-failed fixture first. igneum-pow/tests/spec_readback.rs derives every pinned id of the "Seed | Attempt | Id | Note" table through the crate and draws each class v4 row with its era to the stated attempt; a must-differ row asserts another id (box suite on the branch: 64 + 7 + 4 + 19 + 2 + 7 + 2 passed, build-remote rc 0). tools/ledger-page.mjs: both entry heading forms render (M1 and AP-F8-1; the single-letter form dropped every in-house pass row from /ledger), the pass gets its own section, a known-failed self-test in the gate. docs/fud-ledger.md drops the USD 50,000 prize clauses and "paid independent cryptanalysis" (decision item 1 of 6 October: no bounty; the in-house rule of 7 October), so the regenerated site/ledger.html carries neither. checks.txt regenerated (73 checks). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
138 lines
9 KiB
JavaScript
138 lines
9 KiB
JavaScript
#!/usr/bin/env node
|
|
// Spec read-back, the constants half (adv-accept-3 finding 3, ledger AP-F8-5, 7 October 2026): every table in
|
|
// docs/spec/01-lottery-hash.md whose header row is `| Constant | Value | Where |` names Rust constants of the igneum-pow
|
|
// crate with the value the text relies on, and this check fails when any value differs from the crate's `pub const`.
|
|
// So the public text and the shipped rule cannot drift apart unseen again (the spec at 017e7037 described a rule that
|
|
// mined a different program on 264 of 400 epochs). The ids half is igneum-pow/tests/spec_readback.rs (a cargo test).
|
|
//
|
|
// node tools/ci/spec-constants-check.mjs exit 1 listing every row that disagrees, is absent or does not parse
|
|
// node tools/ci/spec-constants-check.mjs --self-test a fixture with one wrong value, one absent constant and one pending
|
|
// row must fail on the first two and pass the third; the clean fixture passes
|
|
//
|
|
// Row forms. Constant: `module::NAME` (one file under igneum-pow/src) or `NAME` in backticks (every file searched). Value:
|
|
// the Rust literal as it reads (integers with or without underscores, a decimal with a dot, a byte string in double quotes).
|
|
// Where: free text; a row whose Where contains "pending" is skipped while the constant is absent from the crate (a class on
|
|
// a branch that lands code and text together) and compared once it is present. A constant defined as an expression of other
|
|
// constants (`ACCEPT_UNITS * LANES`) is evaluated. One literal that is not a const is read by name: `accept::distinct_ratio_pass`
|
|
// with the window cap `.min(N)` inside that function.
|
|
import { readFileSync, readdirSync, mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
const SPEC = 'docs/spec/01-lottery-hash.md';
|
|
const SRC = 'igneum-pow/src';
|
|
|
|
// the literals inside functions the spec names as constants: name -> [file, function, regex with one capture]
|
|
const LITERALS = {
|
|
'accept::distinct_ratio_pass': ['accept.rs', 'distinct_ratio_pass', /\.min\((\d+)\)/],
|
|
};
|
|
|
|
function tables(md) {
|
|
// every table whose header is exactly Constant | Value | Where; rows until the first non-table line
|
|
const out = []; const lines = md.split('\n');
|
|
for (let i = 0; i < lines.length; i++) {
|
|
if (!/^\|\s*Constant\s*\|\s*Value\s*\|\s*Where\s*\|\s*$/.test(lines[i])) continue;
|
|
const rows = []; let j = i + 1;
|
|
if (j < lines.length && /^\|\s*-+\s*\|/.test(lines[j])) j++;
|
|
for (; j < lines.length && /^\|/.test(lines[j]); j++) {
|
|
const cells = lines[j].replace(/^\||\|$/g, '').split('|').map(c => c.trim());
|
|
if (cells.length < 3) continue;
|
|
rows.push({ line: j + 1, constant: cells[0].replace(/`/g, '').trim(), value: cells[1].replace(/`/g, '').trim(), where: cells.slice(2).join('|') });
|
|
}
|
|
out.push(rows); i = j;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function constMap(srcDir) {
|
|
// name -> { file, raw } for every `pub const NAME: T = <expr>;` under igneum-pow/src (one line each)
|
|
const map = new Map(); const files = readdirSync(srcDir).filter(f => f.endsWith('.rs'));
|
|
for (const f of files) {
|
|
const text = readFileSync(join(srcDir, f), 'utf8');
|
|
for (const m of text.matchAll(/^pub const ([A-Z][A-Z0-9_]*)\s*:\s*[^=]+=\s*([^;]+);/gm)) {
|
|
const name = m[1]; const raw = m[2].trim();
|
|
const key = f.replace(/\.rs$/, '') + '::' + name;
|
|
map.set(key, { file: f, raw }); if (!map.has(name)) map.set(name, { file: f, raw, bare: true });
|
|
}
|
|
}
|
|
return { map, files, srcDir };
|
|
}
|
|
|
|
function evalRust(raw, consts, depth = 0) {
|
|
// a number, a byte string, or an expression over numbers and other constants
|
|
if (depth > 8) throw new Error('constant expression too deep: ' + raw);
|
|
const s = raw.replace(/\s+as\s+(u8|u16|u32|u64|usize|i32|i64|f64)/g, '').trim();
|
|
const bs = /^b?"((?:[^"\\]|\\.)*)"$/.exec(s); if (bs) return bs[1];
|
|
const expr = s.replace(/[A-Z][A-Z0-9_]*/g, name => {
|
|
const c = consts.get(name); if (!c) throw new Error('unknown identifier ' + name + ' in ' + raw);
|
|
const v = evalRust(c.raw, consts, depth + 1); if (typeof v !== 'number') throw new Error(name + ' is not a number');
|
|
return '(' + v + ')';
|
|
}).replace(/_/g, '').replace(/(\d)(u8|u16|u32|u64|usize|i32|i64|f64)\b/g, '$1');
|
|
if (!/^[\d\s().+\-*/<>]+$/.test(expr)) throw new Error('cannot evaluate ' + raw);
|
|
// eslint-disable-next-line no-new-func
|
|
return Number(Function('"use strict"; return (' + expr + ');')());
|
|
}
|
|
|
|
function parseSpecValue(v) {
|
|
const bs = /^"((?:[^"\\]|\\.)*)"$/.exec(v); if (bs) return bs[1];
|
|
const n = Number(v.replace(/_/g, '')); if (v.trim() === '' || Number.isNaN(n)) throw new Error('not a number or a quoted string: ' + v);
|
|
return n;
|
|
}
|
|
|
|
function check(rootDir, specRel = SPEC, srcRel = SRC) {
|
|
const md = readFileSync(join(rootDir, specRel), 'utf8');
|
|
const { map } = constMap(join(rootDir, srcRel));
|
|
const fails = []; let rows = 0, pending = 0;
|
|
for (const table of tables(md)) for (const r of table) {
|
|
rows++;
|
|
try {
|
|
const want = parseSpecValue(r.value);
|
|
if (LITERALS[r.constant]) {
|
|
const [file, fn, re] = LITERALS[r.constant];
|
|
const text = readFileSync(join(rootDir, srcRel, file), 'utf8');
|
|
const start = text.indexOf('fn ' + fn + '('); if (start < 0) throw new Error('no fn ' + fn + ' in ' + file);
|
|
const body = text.slice(start, text.indexOf('\n}', start)); const m = re.exec(body);
|
|
if (!m) throw new Error('no literal matching ' + re + ' inside ' + fn);
|
|
if (Number(m[1]) !== want) fails.push(`${specRel}:${r.line}: ${r.constant} reads ${r.value} in the spec, ${m[1]} in ${file} fn ${fn}`);
|
|
continue;
|
|
}
|
|
const c = map.get(r.constant);
|
|
if (!c) {
|
|
if (/pending/i.test(r.where)) { pending++; continue; }
|
|
fails.push(`${specRel}:${r.line}: ${r.constant} is not a pub const of ${srcRel} (and the row is not marked pending)`); continue;
|
|
}
|
|
const got = evalRust(c.raw, map);
|
|
const same = typeof want === 'string' ? want === got : Math.abs(want - got) < 1e-12;
|
|
if (!same) fails.push(`${specRel}:${r.line}: ${r.constant} reads ${r.value} in the spec, ${JSON.stringify(got)} in ${c.file} (${c.raw})`);
|
|
} catch (e) { fails.push(`${specRel}:${r.line}: ${r.constant}: ${e.message}`); }
|
|
}
|
|
if (rows === 0) fails.push(`${specRel}: no table headed | Constant | Value | Where | found`);
|
|
return { fails, rows, pending };
|
|
}
|
|
|
|
function selfTest() {
|
|
const d = mkdtempSync(join(tmpdir(), 'spec-constants-')); const fails = [];
|
|
try {
|
|
mkdirSync(join(d, 'docs', 'spec'), { recursive: true }); mkdirSync(join(d, SRC), { recursive: true });
|
|
writeFileSync(join(d, SRC, 'accept.rs'), 'pub const ACCEPT_UNITS: usize = 64;\npub const ACCEPT_HASHES: usize = ACCEPT_UNITS * LANES;\npub const MAX_SATURATED: u32 = 164;\npub const MIN_DISTINCT_RATIO_V4: f64 = 0.98;\npub const ACCEPT_TAG: &[u8] = b"igneum-accept/";\npub fn distinct_ratio_pass(x: u64) -> u64 {\n let w = (x as u64).min(2);\n w\n}\n');
|
|
writeFileSync(join(d, SRC, 'generator.rs'), 'pub const LANES: usize = 32;\npub const MIN_DISTINCT_SUM: u64 = 245_760;\n');
|
|
const clean = '# spec\n\n| Constant | Value | Where |\n|---|---|---|\n| accept::ACCEPT_UNITS | 64 | (c) |\n| accept::ACCEPT_HASHES | 2048 | |\n| `MIN_DISTINCT_SUM` | 245760 | |\n| accept::MIN_DISTINCT_RATIO_V4 | 0.98 | |\n| accept::ACCEPT_TAG | "igneum-accept/" | |\n| accept::distinct_ratio_pass | 2 | the window cap |\n| accept::MIN_DISTINCT_RATIO_V5 | 0.995 | class v5, pending |\n\ntext\n';
|
|
writeFileSync(join(d, SPEC), clean);
|
|
const ok = check(d); if (ok.fails.length || ok.rows !== 7 || ok.pending !== 1) fails.push('the clean fixture failed: ' + JSON.stringify(ok));
|
|
writeFileSync(join(d, SPEC), clean.replace('| 64 |', '| 63 |').replace('| `MIN_DISTINCT_SUM` |', '| `MIN_DISTINCT_SUMM` |'));
|
|
const bad = check(d);
|
|
if (!bad.fails.some(f => /ACCEPT_UNITS reads 63/.test(f))) fails.push('a wrong value was not caught: ' + JSON.stringify(bad.fails));
|
|
if (!bad.fails.some(f => /MIN_DISTINCT_SUMM is not a pub const/.test(f))) fails.push('an absent constant was not caught: ' + JSON.stringify(bad.fails));
|
|
if (bad.fails.length !== 2) fails.push('the known-failed fixture had ' + bad.fails.length + ' failures, wanted 2: ' + JSON.stringify(bad.fails));
|
|
} finally { rmSync(d, { recursive: true, force: true }); }
|
|
if (fails.length) { for (const f of fails) console.error('self-test failed: ' + f); return 1; }
|
|
console.log('self-test passed: a wrong value and an absent constant are caught and named; a pending row and the clean fixture pass; an expression over constants evaluates');
|
|
return 0;
|
|
}
|
|
|
|
if (process.argv.includes('--self-test')) process.exit(selfTest());
|
|
const r = check(root);
|
|
if (r.fails.length) { for (const f of r.fails) console.error('spec-constants: ' + f); process.exit(1); }
|
|
console.log(`spec-constants: ${r.rows} rows of ${SPEC} agree with the crate's pub const items (${r.pending} pending)`);
|