igneum/tools/ci/launch-gates-check.mjs
igneum-labs ff0d0dde61 Scrub: the founder list leaves the repository in every encoding; the never-push founder check decodes base64, hex and .b64 blobs too (7 October 2026, 21:4x UK)
tools/ci/founder-strings.b64 was the founder check's pattern list base64-encoded: a grep could not read it, any reader of the public host could (git.igneum.network was public from 21:26 UK; read off at 21:36). The list now lives only in a private file (~/.config/igneum/founder-strings on the Mac, /srv/discord-hooks/founder-strings on build-1 for the Discord guard; $IGNEUM_FOUNDER_STRINGS overrides) and site/forbidden-strings.txt carries no encoded copy. Readers: founder-strings-check.sh (skips with a line where the file is absent; the Mac's hook is the guard), site/scrub.mjs and launch-gates-check.mjs (the private file's patterns added where it exists), discord-hooks.mjs (three locations; the test writes a fixture list and loads the module after it), fresh-repo.sh (the private file; drops tools/ci/founder-strings.b64 from every commit; rewrites the base64 of every list regex out of every blob and scans for it).

The check's second pass (main's addition): every base64 literal of 24 characters or more, every hex literal of 24 or more and every *.b64 file is decoded and scanned, so no encoding hides a term again; the self-test plants each fixture sample in plain text, in a .b64 file, as a base64 literal and as a hex literal, each caught and named, and a tree without the list skips with its line.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 20:40:36 +00:00

144 lines
9.1 KiB
JavaScript

#!/usr/bin/env node
// The launch-pack gate (mission item 10, docs/analysis/mission/mission.md 2.10; the rule-row rule of CLAUDE.md,
// 6 October 2026: a rule row closes only with its check). Three things, each an exit 1 with the line:
// 1. every row of the "Launch gates" table in docs/plans/testnet-go.md has a non-empty Check cell, and every repository
// path the cell names in backticks exists (a gate whose check names a script that is not there is not a gate);
// 2. the text handed to the site lane (docs/plans/launch-pack.md between <!-- handoff:start --> and <!-- handoff:end -->)
// and the public income table (docs/analysis/income-tiers.md) carry none of the served-page patterns of
// site/forbidden-strings.txt nor the export patterns of tools/ci/forbidden-strings.txt, and no em dash;
// 3. the eight regulatory sentences in the handoff are the eight of docs/analysis/mission/future.md section 8.3, each
// numbered, in order, none dropped (the handoff may rephrase; each must cite its number).
// node tools/ci/launch-gates-check.mjs # exit 1 on any failure
// node tools/ci/launch-gates-check.mjs --self-test # a gate row without a check fails; a handoff with the founder's name fails
import { existsSync, readFileSync, mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path, { join } from 'node:path';
import { homedir } from 'node:os';
import { fileURLToPath } from 'node:url';
const HERE = path.dirname(fileURLToPath(import.meta.url));
const ROOT = process.env.LAUNCH_GATES_ROOT || path.join(HERE, '..', '..');
const GO = 'docs/plans/testnet-go.md';
const PACK = 'docs/plans/launch-pack.md';
const INCOME = 'docs/analysis/income-tiers.md';
// the private founder list (~/.config/igneum/founder-strings or $IGNEUM_FOUNDER_STRINGS; perl regex, a tab, a sample per row; # comments);
// absent here: no founder patterns from this reader (the Mac's pre-push hook carries the file and is the guard)
function founderPatternsFromFile() {
try {
const f = process.env.IGNEUM_FOUNDER_STRINGS || join(homedir(), '.config', 'igneum', 'founder-strings');
return readFileSync(f, 'utf8').split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#')).map((l) => new RegExp(l.split('\t')[0], 'i'));
} catch { return []; }
}
function patterns(root) {
const read = f => { try { return readFileSync(path.join(root, f), 'utf8'); } catch { return ''; } };
const lines = [...read('site/forbidden-strings.txt').split('\n'), ...read('tools/ci/forbidden-strings.txt').split('\n')];
return lines.map(l => l.trim()).filter(l => l && !l.startsWith('#')).map(l => new RegExp(l)).concat(root === process.cwd() || root === ROOT ? founderPatternsFromFile() : []); // plus the private founder list for the real tree
}
export function gateRows(text) {
const start = text.indexOf('## Launch gates');
if (start < 0) return null;
const section = text.slice(start);
const rows = [];
let header = null;
for (const line of section.split('\n')) {
if (!line.startsWith('|')) { if (header && rows.length) break; continue; }
const cells = line.split('|').slice(1, -1).map(c => c.trim());
if (!header) { header = cells.map(c => c.toLowerCase()); continue; }
if (cells.every(c => /^-+$/.test(c))) continue;
const row = {}; header.forEach((h, i) => { row[h] = cells[i] || ''; });
rows.push(row);
}
return rows;
}
export function checkGates(root, problems) {
const text = readFileSync(path.join(root, GO), 'utf8');
const rows = gateRows(text);
if (!rows) { problems.push(`${GO}: no "## Launch gates" section`); return; }
if (!rows.length) { problems.push(`${GO}: the Launch gates table has no rows`); return; }
for (const r of rows) {
const id = r.gate || r['#'] || '(no id)';
const check = r.check || '';
if (!check || /^(owed|none|tbd|n\/a)$/i.test(check)) problems.push(`${GO}: gate ${id} has no check`);
for (const m of check.matchAll(/`([^`]+)`/g)) {
const ref = m[1].split(/\s/)[0];
if (/^(tools|docs|site|infra|packaging|proving|app)\//.test(ref) && !existsSync(path.join(root, ref))) problems.push(`${GO}: gate ${id} names ${ref}, which does not exist`);
}
}
return rows.length;
}
export function handoff(root) {
const text = readFileSync(path.join(root, PACK), 'utf8');
const a = text.indexOf('<!-- handoff:start -->'), b = text.indexOf('<!-- handoff:end -->');
if (a < 0 || b < 0 || b < a) return null;
return text.slice(a, b);
}
export function checkText(root, problems) {
const pats = patterns(root);
const h = handoff(root);
if (h === null) { problems.push(`${PACK}: no handoff block between <!-- handoff:start --> and <!-- handoff:end -->`); return; }
const bodies = [[PACK + ' (handoff)', h]];
try { bodies.push([INCOME, readFileSync(path.join(root, INCOME), 'utf8')]); } catch { problems.push(`${INCOME}: missing`); }
for (const [name, body] of bodies) {
body.split('\n').forEach((line, i) => {
if (line.includes('\u2014')) problems.push(`${name}:${i + 1}: an em dash`);
for (const re of pats) if (re.test(line)) problems.push(`${name}:${i + 1}: forbidden pattern ${re.source}`);
});
}
// the eight regulatory sentences, numbered 1 to 8 in order
const nums = [...h.matchAll(/\(8\.3 sentence (\d)[;)]/g)].map(m => Number(m[1]));
for (let k = 1; k <= 8; k++) if (!nums.includes(k)) problems.push(`${PACK} (handoff): regulatory sentence ${k} of future.md 8.3 is missing its "(8.3 sentence ${k})" mark`);
const ordered = nums.filter((v, i, arr) => arr.indexOf(v) === i);
if (ordered.join(',') !== '1,2,3,4,5,6,7,8') problems.push(`${PACK} (handoff): the eight sentences are not in order (${ordered.join(',')})`);
if (!/not legal advice/i.test(h)) problems.push(`${PACK} (handoff): the label "not legal advice" is missing`);
}
function run(root) {
const problems = [];
const n = checkGates(root, problems);
checkText(root, problems);
return { problems, gates: n || 0 };
}
function selfTest() {
const fx = mkdtempSync(path.join(tmpdir(), 'launch-gates-'));
try {
for (const d of ['docs/plans', 'docs/analysis', 'site', 'tools/ci', 'tools/launch']) mkdirSync(path.join(fx, d), { recursive: true });
writeFileSync(path.join(fx, 'site/forbidden-strings.txt'), '(?i)\\bfoundername\\b\n'.replace('(?i)', '')); // a made-up name as a plain pattern (the private list is not read for a fixture root)
writeFileSync(path.join(fx, 'tools/ci/forbidden-strings.txt'), '/Users/\n');
writeFileSync(path.join(fx, 'tools/launch/x.mjs'), '');
const sentences = Array.from({ length: 8 }, (_, i) => `${i + 1}. sentence (8.3 sentence ${i + 1})`).join('\n');
const good = `# go\n\n## Launch gates\n\n| Gate | What | Check |\n|---|---|---|\n| G1 | a | \`tools/launch/x.mjs\` runs |\n`;
const goodPack = `<!-- handoff:start -->\n${sentences}\nnot legal advice\n<!-- handoff:end -->\n`;
writeFileSync(path.join(fx, GO), good); writeFileSync(path.join(fx, PACK), goodPack); writeFileSync(path.join(fx, INCOME), 'clean\n');
let r = run(fx);
if (r.problems.length) throw new Error(`self-test: the clean fixture failed: ${r.problems.join('; ')}`);
// a gate without a check
writeFileSync(path.join(fx, GO), good + '| G2 | b | |\n');
r = run(fx); if (!r.problems.some(p => /G2 has no check/.test(p))) throw new Error('self-test: a gate row without a check passed');
// a check naming a missing script
writeFileSync(path.join(fx, GO), good + '| G3 | c | `tools/launch/missing.mjs` |\n');
r = run(fx); if (!r.problems.some(p => /does not exist/.test(p))) throw new Error('self-test: a check naming a missing script passed');
writeFileSync(path.join(fx, GO), good);
// the founder's name in the handoff, an em dash, a missing sentence
writeFileSync(path.join(fx, PACK), goodPack.replace('1. sentence', '1. foundername says'));
r = run(fx); if (!r.problems.some(p => /forbidden pattern .*foundername/i.test(p))) throw new Error('self-test: the founder\'s name in the handoff passed');
writeFileSync(path.join(fx, PACK), goodPack.replace('2. sentence', '2. a \u2014 dash'));
r = run(fx); if (!r.problems.some(p => /em dash/.test(p))) throw new Error('self-test: an em dash in the handoff passed');
writeFileSync(path.join(fx, PACK), goodPack.replace('(8.3 sentence 5)', ''));
r = run(fx); if (!r.problems.some(p => /sentence 5 of future.md 8.3 is missing/.test(p))) throw new Error('self-test: a dropped regulatory sentence passed');
process.stdout.write('self-test passed: a gate without a check, a missing script, the founder\'s name, an em dash and a dropped sentence each fail; the clean fixture passes\n');
return 0;
} finally { rmSync(fx, { recursive: true, force: true }); }
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
if (process.argv.includes('--self-test')) process.exit(selfTest());
const { problems, gates } = run(ROOT);
if (problems.length) { for (const p of problems) process.stderr.write(`${p}\n`); process.exit(1); }
process.stdout.write(`launch gates: ${gates} gate rows, each with its check; the handoff text and the income table carry no forbidden pattern; the eight regulatory sentences are present in order\n`);
}