The owner's orders of 7 October 2026 (header, logos, footer, phone scene) and the Counter ASIC lane's public text (docs/plans/counter-asic-3-public-text-2026-10-07.md,b10c7ac2) with the disclosure prize held back until its publish word. Header: the one-screen home no longer hides items; the sheet carries exactly Litepaper, Miner, App, Wallet, Live devnet, Ledger and Download; 44 px bar links, 48 px sheet rows, the burger turns into an X; tools/ci/site-nav-check.mjs runs after every build and in the gate. Cards: the Windows four-pane, Apple and Tux marks from80c5618, white on the ember card, the card's own text colour on the mono cards (so they flip with the theme). Footer: the ledger page renders through tools/ledger-page.mjs and now underlines Ledger; the Reddit profile is the one the owner named. Renderer: narrow, narrowBreak 720, narrowWindow 30, narrowLanes 4, narrowMinNode 11; hairlines off except the selected chain; checkpoint labels as the percent; fps 60 on the home and /live mounts. Measured in headless chromium at 390 with 4x CPU throttling: 50 to 56 frames a second (the cap is 60). Evidence row 17 rewritten in the table's eight columns. The forbidden list gains "disclosure prize" and the word-bounded rig names. Tested with Playwright at 390 and 768 in both themes with real taps on every public page and the 404: 48 rows, 0 problems; captures in docs/plans/site-ui-4-shots/chrome. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
230 lines
17 KiB
JavaScript
230 lines
17 KiB
JavaScript
#!/usr/bin/env node
|
|
// Renders docs/fud-ledger.md as the public page site/ledger.html: every entry, the critic's words, what was done, the
|
|
// status and the date, with the count table on top. Nothing is dropped and nothing is softened; the only rewrites are
|
|
// the identity and provider terms of tools/ci/forbidden-strings.txt and the leak classes below (config and home paths,
|
|
// process ids, listen addresses, machine names, repository file paths), applied in place; the rendered page is then
|
|
// grepped against the same list and the render fails on a hit.
|
|
// Usage: node tools/ledger-page.mjs [docs/fud-ledger.md] [site/ledger.html]
|
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const root = join(here, '..');
|
|
const src = process.argv[2] || join(root, 'docs', 'fud-ledger.md');
|
|
const out = process.argv[3] || join(root, 'site', 'ledger.html');
|
|
|
|
const esc = s => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
|
|
// Identity and provider terms. The criticism keeps its force; the operational name goes.
|
|
const SCRUB = [
|
|
[/\bJosh's\b/g, "the owner's"], [/\bJosh\b/g, 'the owner'],
|
|
[/\bHetzner\b/g, 'the cloud provider'], [/\bGoDaddy\b/g, 'the US registrar'], [/\bVercel\b/g, 'the host'],
|
|
[/CLAUDE\.md/g, 'the project rules file'], [/\.claude\/agents\/?/g, 'the agent files '],
|
|
[/\/opt\/igneum[^\s`,;)]*/g, 'the prover host directory'], [/dl\.igneum\.network/g, 'the downloads host'],
|
|
[/log[-_]intake[-_]?key|LOG_INTAKE_KEY|intake[_-]?key/gi, 'the log key'], [/log-intake|LOG_INTAKE/g, 'the log intake'],
|
|
[/\bintake id\b/g, 'the upload id'], [/\+0100/g, 'a local-time offset'], [/\bBST\b/g, 'local time'],
|
|
[/\bTailscale\b|\bts\.net\b/g, 'the private network'], [/DESKTOP-[A-Z0-9]{7}/g, 'the PC'], [/MacBook/g, 'the laptop'],
|
|
[/\bhcloud\b/g, 'the cloud CLI'], [/igneum-seed[0-9]*/g, 'the seed node'], [/\/root\//g, '/<home>/'],
|
|
[/\/Users\/[^\s/`]+/g, '~'], [/C:\\Users\\[^\s\\`]+/g, '%USERPROFILE%'], [/~\/Desktop/g, '~/<folder>'],
|
|
[/192\.168\.\d+\.\d+/g, '<lan address>'], [/100\.\d+\.\d+\.\d+/g, '<private address>'],
|
|
// 6 October 2026 (the public-page leak found by the site audit): config and home paths, process ids, listen addresses,
|
|
// machine names and repository file paths never reach the page; the ledger id beside each entry is the reference
|
|
[/~\/\.config\/[^\s`,;)]*/g, 'a config file'], [/~\/[A-Za-z0-9_.-]+(?:\/[^\s`,;)]*)?/g, 'a home-directory file'],
|
|
[/\bpid \d+\b/g, 'the process'], [/--rpclisten=\S+/g, 'the RPC listen flag'], [/\b0\.0\.0\.0:\d+\b/g, 'the listen address'],
|
|
[/\bPC [12]\b('s)?/g, (m, p) => 'the Windows machine' + (p || '')], [/\bthe Mac's\b/g, "the Apple M5 Max's"], [/\bthe Mac\b/g, 'the Apple M5 Max'],
|
|
[/(?<![\w/:.@-])(?:docs|tools|app|packaging|infra|sim|proving|vendor|igneum|site|proto-[a-z]+|rusty-kaspa|igneum-node[a-z0-9-]*)\/[\w./+-]*\w(?::\d+(?:-\d+)?)?/g, 'a repository file'],
|
|
];
|
|
const scrub = s => SCRUB.reduce((t, [re, r]) => t.replace(re, r), s);
|
|
|
|
// Inline markdown: code spans and links only; the ledger uses nothing else inside a status line.
|
|
function inline(s) {
|
|
let t = esc(s);
|
|
t = t.replace(/`([^`]+)`/g, (m, c) => `<code>${c}</code>`);
|
|
t = t.replace(/\[([^\]]+)\]\((https?:[^)]+)\)/g, (m, a, u) => `<a href="${u}" rel="noopener">${a}</a>`);
|
|
return t;
|
|
}
|
|
|
|
const lines = readFileSync(src, 'utf8').split('\n');
|
|
const entries = [];
|
|
let cur = null;
|
|
for (const l of lines) {
|
|
const m = /^### ([A-Z]\d+)\. (.*)$/.exec(l);
|
|
if (m) { cur = { id: m[1], title: m[2].trim(), quote: '', status: '', answer: '' }; entries.push(cur); continue; }
|
|
if (!cur) continue;
|
|
const s = l.trim();
|
|
if (!cur.quote && s.startsWith('"')) cur.quote = s.replace(/^"|"$/g, '');
|
|
else if (!cur.status && s.startsWith('Status:')) cur.status = s.slice(7).trim();
|
|
else if (!cur.answer && s.startsWith('Answer:')) cur.answer = s.slice(7).trim();
|
|
}
|
|
|
|
const SECTION = { M: 'Mining and chips', F: 'Finality and attacks', P: 'Proving and the zkEVM', E: 'Economics and the coin', G: 'Governance and the founders', C: 'Comparisons', L: 'Legal and regulatory', X: 'Launch and operations', D: 'Builders' };
|
|
|
|
function bucket(status) {
|
|
const s = status.toLowerCase();
|
|
if (/^(fixed|rolled out|rule fixed|spec fixed|rule implemented|rule written|written|designed)/.test(s)) return 'Fixed or built';
|
|
if (s.startsWith('conceded')) return 'Conceded';
|
|
if (s.startsWith('answered by design')) return 'Answered by design';
|
|
if (/^(answered with evidence|measured|simulation half)/.test(s)) return 'Answered with evidence';
|
|
if (/^(closed|decided)/.test(s)) return 'Closed by rule or decided';
|
|
if (s.startsWith('open')) return 'Open';
|
|
return 'Other';
|
|
}
|
|
function statusWord(status) {
|
|
const m = /^([^(:.]+?)(?=\s*[(:.]|$)/.exec(status);
|
|
return (m ? m[1] : status).trim();
|
|
}
|
|
function dateOf(status) {
|
|
const m = /(\d{1,2} October 2026)/.exec(status);
|
|
return m ? m[1] : '3 October 2026';
|
|
}
|
|
|
|
const ORDER = ['Open', 'Conceded', 'Fixed or built', 'Closed by rule or decided', 'Answered with evidence', 'Answered by design', 'Other'];
|
|
const MEANING = {
|
|
'Open': 'Nothing has settled it yet. The entry names what will',
|
|
'Conceded': 'The critic is right. "Stated" means the public text says so; "not yet stated" means it does not yet',
|
|
'Fixed or built': 'A code, spec or text change answers it, with the commit or the page named',
|
|
'Closed by rule or decided': 'A consensus rule or a decision by the owner answers it, dated',
|
|
'Answered with evidence': 'A measurement or a simulation exists and is named',
|
|
'Answered by design': 'A design rule answers it; no measurement is possible yet',
|
|
'Other': 'A status outside the six above, read the line',
|
|
};
|
|
const counts = {};
|
|
for (const e of entries) { const b = bucket(e.status); counts[b] = (counts[b] || 0) + 1; }
|
|
|
|
const partial = name => readFileSync(join(root, 'site', 'partials', name), 'utf8').trim();
|
|
const HEAD = partial('head.html'), NAV = partial('nav.html'), FOOT = partial('footer.html');
|
|
// the ledger is a header item: the page underlines it like build.mjs's navFor does for the other pages
|
|
const NAV_LEDGER = NAV.replace(/(<a href="\/ledger" data-nav="ledger")/, '$1 aria-current="page"');
|
|
|
|
const intro = `This is every criticism the project expects, in the critic's words, with what was done about it and the date. ${entries.length} entries since 3 October 2026. Entries are never deleted; a status that changes keeps its history on the line. Where the critic was right the entry says Conceded. Where nothing has been done it says Open and names what settles it. The founder mined through the GPU years. Ethereum's move to proof of stake in September 2022 ended that income and the miners' place in that chain. This is one person building, with AI systems doing the engineering, the coin he wanted to exist for miners: GPU-mined, the miners are the provers, no founder allocation, every cost stated. Help is welcome and a team is wanted: cryptographers, node engineers, miners who will test. This ledger is the application form: pick an open row and write to <a href="mailto:hello@igneum.network">hello@igneum.network</a> with its id.`;
|
|
|
|
const countRows = ORDER.filter(b => counts[b]).map(b => `<tr><td class="num">${counts[b]}</td><td><button type="button" class="chip" data-filter="${esc(b)}">${esc(b)}</button></td><td>${esc(MEANING[b])}</td></tr>`).join('\n');
|
|
|
|
let body = '';
|
|
let lastSec = '';
|
|
for (const e of entries) {
|
|
const sec = SECTION[e.id[0]] || e.id[0];
|
|
if (sec !== lastSec) { body += `<h2 id="${e.id[0].toLowerCase()}">${esc(sec)}</h2>\n`; lastSec = sec; }
|
|
const b = bucket(e.status);
|
|
const word = statusWord(scrub(e.status));
|
|
const rest = scrub(e.status).slice(word.length).replace(/^[\s(:.]+/, '').trim();
|
|
body += `<article class="entry" id="${e.id}" data-bucket="${esc(b)}">
|
|
<div class="head"><span class="id">${e.id}</span><h3>${inline(scrub(e.title))}</h3><span class="date">${esc(dateOf(e.status))}</span></div>
|
|
<blockquote>${inline(scrub(e.quote))}</blockquote>
|
|
<div class="status"><span class="badge b-${b.toLowerCase().replace(/[^a-z]+/g, '-')}">${esc(word)}</span>${rest ? ` <span class="did">${inline(rest)}</span>` : ''}</div>
|
|
${e.answer ? `<details><summary>The answer as first written</summary><p>${inline(scrub(e.answer))}</p></details>` : ''}
|
|
</article>\n`;
|
|
}
|
|
|
|
const html = `<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
<title>Igneum ledger: every criticism, answered</title>
|
|
<meta name="description" content="Every criticism Igneum expects, in the critic's words, with what was done, the status and the date. ${entries.length} entries. Nothing deleted, nothing softened.">
|
|
<link rel="canonical" href="https://igneum.network/ledger">
|
|
<meta name="theme-color" content="#0C0C0E">
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:site_name" content="Igneum">
|
|
<meta property="og:title" content="Igneum ledger: every criticism, answered">
|
|
<meta property="og:description" content="${entries.length} criticisms in the critic's words, with what was done, the status and the date.">
|
|
<meta property="og:url" content="https://igneum.network/ledger">
|
|
<meta property="og:image" content="https://igneum.network/og-small.png?v=3">
|
|
<meta property="og:image:width" content="256">
|
|
<meta property="og:image:height" content="256">
|
|
<meta property="og:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
|
|
<meta name="twitter:card" content="summary">
|
|
<meta name="twitter:title" content="Igneum ledger: every criticism, answered">
|
|
<meta name="twitter:description" content="${entries.length} criticisms in the critic's words, with what was done, the status and the date.">
|
|
<meta name="twitter:image" content="https://igneum.network/og-small.png?v=3">
|
|
<meta name="twitter:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
|
|
<link rel="icon" href="/favicon.ico" sizes="48x48">
|
|
<link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32">
|
|
<link rel="icon" href="/icon-192.png" type="image/png" sizes="192x192">
|
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180">
|
|
<link rel="manifest" href="/site.webmanifest">
|
|
<!-- head:start -->
|
|
${HEAD}
|
|
<!-- head:end -->
|
|
<style>
|
|
.entry p,.entry li,.entry .status{overflow-wrap:anywhere}.entry code{white-space:normal;overflow-wrap:anywhere}
|
|
:root{--obsidian:#0C0C0E;--graphite:#16161A;--line:#2A2A30;--line-2:#3A3A42;--ember:#F2541B;--molten:#FFB35C;--bone:#F4F1EC;--ash:#9A9A9E;--ink-2:#C9C7C2;--max:1200px;--gutter:clamp(16px,4vw,32px);--sec:clamp(40px,6vw,72px)}
|
|
*{box-sizing:border-box}
|
|
body{margin:0;background:var(--obsidian);color:var(--bone);font-family:var(--f-sans);font-size:16px;line-height:1.55;-webkit-font-smoothing:antialiased;overflow-x:hidden}
|
|
a{color:var(--ember);text-decoration:none}a:hover{color:var(--molten)}
|
|
.wrap{max-width:var(--max);margin:0 auto;padding-inline:var(--gutter)}
|
|
.eyebrow{font-family:var(--f-mono);font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--ash)}
|
|
h1{font-family:var(--f-display);font-weight:900;font-size:clamp(30px,5vw,52px);line-height:1.08;margin:10px 0 18px}
|
|
main h2{font-family:var(--f-display);font-weight:700;font-size:clamp(20px,2.6vw,28px);text-wrap:balance;margin:var(--sec) 0 16px;padding-top:8px;border-top:1px solid var(--line);scroll-margin-top:84px}
|
|
h3{font-family:var(--f-sans);font-weight:600;font-size:17px;margin:0;flex:1 1 auto;min-width:0}
|
|
.intro{font-size:17px;color:var(--ink-2);max-width:76ch;margin:0 0 24px}
|
|
.tbl{overflow-x:auto;border:1px solid var(--line);border-radius:16px;background:var(--graphite);margin:0 0 20px}
|
|
table{border-collapse:collapse;width:100%;font-size:15px;min-width:520px}
|
|
th,td{padding:11px 14px;text-align:left;vertical-align:top;border-bottom:1px solid var(--line)}
|
|
th{font-family:var(--f-mono);font-size:12px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash);font-weight:500}
|
|
tr:last-child td{border-bottom:0}
|
|
td.num{font-family:var(--f-display);font-weight:700;font-size:22px;color:var(--ember);white-space:nowrap}
|
|
.chip{background:none;border:1px solid var(--line-2);color:var(--bone);border-radius:999px;padding:6px 12px;font:inherit;font-size:14px;cursor:pointer}
|
|
.chip:hover,.chip.on{border-color:var(--ember);color:var(--molten)}
|
|
.toolbar{display:flex;flex-wrap:wrap;gap:8px 14px;align-items:center;margin:0 0 8px;font-size:14px;color:var(--ash)}
|
|
.toolbar input{background:var(--graphite);border:1px solid var(--line-2);color:var(--bone);border-radius:10px;padding:8px 12px;font:inherit;font-size:14px;min-width:220px}
|
|
.sections{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:14px;margin:0 0 8px}
|
|
.entry{border:1px solid var(--line);border-radius:14px;background:var(--graphite);padding:16px 18px;margin:0 0 12px;scroll-margin-top:84px}
|
|
.entry.hide{display:none}
|
|
.head{display:flex;flex-wrap:wrap;align-items:baseline;gap:6px 12px}
|
|
.id{font-family:var(--f-mono);font-size:13px;color:var(--molten);flex:0 0 auto}
|
|
.date{font-family:var(--f-mono);font-size:12px;color:var(--ash);flex:0 0 auto}
|
|
blockquote{margin:10px 0;padding:10px 14px;border-left:3px solid var(--line-2);color:var(--ink-2);font-style:italic}
|
|
.status{font-size:15px;color:var(--ink-2)}
|
|
.badge{display:inline-block;font-family:var(--f-mono);font-size:12px;letter-spacing:.06em;text-transform:uppercase;padding:3px 8px;border-radius:6px;border:1px solid var(--line-2);color:var(--bone);margin-right:6px;vertical-align:middle}
|
|
.b-open{border-color:var(--ember);color:var(--ember)}.b-conceded{border-color:var(--molten);color:var(--molten)}.b-fixed-or-built{border-color:#5cb85c;color:#8fd48f}
|
|
code{font-family:var(--f-mono);font-size:13px;color:var(--bone);background:#0C0C0E;padding:1px 5px;border-radius:5px}
|
|
details{margin-top:8px;font-size:14px;color:var(--ash)}summary{cursor:pointer;color:var(--ash)}details p{margin:8px 0 0;color:var(--ink-2)}
|
|
.foot{margin-top:var(--sec)}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- nav:start -->
|
|
${NAV_LEDGER}
|
|
<!-- nav:end -->
|
|
<main id="main" class="wrap">
|
|
<header style="padding-block:clamp(40px,6vw,72px) 8px">
|
|
<div class="eyebrow">Ledger · ${entries.length} entries · regenerated from the repository</div>
|
|
<h1>Every criticism, answered or conceded</h1>
|
|
<p class="intro">${intro}</p>
|
|
</header>
|
|
<div class="tbl"><table>
|
|
<thead><tr><th>Count</th><th>Status</th><th>Meaning</th></tr></thead>
|
|
<tbody>
|
|
${countRows}
|
|
<tr><td class="num">${entries.length}</td><td><button type="button" class="chip" data-filter="">All</button></td><td>Every entry. The sections: ${Object.entries(SECTION).map(([k, v]) => `<a href="#${k.toLowerCase()}">${esc(v)}</a>`).join(', ')}</td></tr>
|
|
</tbody></table></div>
|
|
<div class="toolbar"><input type="search" id="q" placeholder="Search the ledger" aria-label="Search the ledger"><span id="shown"></span></div>
|
|
${body}
|
|
<p class="intro" style="margin-top:var(--sec)">Source: the project's criticism ledger, a file in the repository, rendered to this page at build time; the repository is published at the public testnet. A criticism that is not here, or that shows an entry is wrong, is added with credit if wanted: <a href="mailto:hello@igneum.network">hello@igneum.network</a> or <a href="https://github.com/igneum-network/spec/issues" rel="noopener">an issue on the specification repository</a>.</p>
|
|
</main>
|
|
<!-- footer:start -->
|
|
${FOOT}
|
|
<!-- footer:end -->
|
|
<script>
|
|
(function(){
|
|
var chips=document.querySelectorAll('.chip'),entries=document.querySelectorAll('.entry'),q=document.getElementById('q'),shown=document.getElementById('shown'),f='';
|
|
function apply(){var t=(q.value||'').toLowerCase(),n=0;entries.forEach(function(e){var ok=(!f||e.getAttribute('data-bucket')===f)&&(!t||e.textContent.toLowerCase().indexOf(t)>=0);e.classList.toggle('hide',!ok);if(ok)n++;});shown.textContent=n+' of '+entries.length+' shown';chips.forEach(function(c){c.classList.toggle('on',c.getAttribute('data-filter')===f);});}
|
|
chips.forEach(function(c){c.addEventListener('click',function(){f=c.getAttribute('data-filter')||'';apply();});});
|
|
q.addEventListener('input',apply);apply();
|
|
if(location.hash){var el=document.getElementById(location.hash.slice(1));if(el)el.scrollIntoView();}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|
|
`;
|
|
// hard stop (6 October 2026): the rendered page is grepped with the committed forbidden list plus the leak classes above
|
|
const forbid = readFileSync(join(root, 'tools', 'ci', 'forbidden-strings.txt'), 'utf8').split('\n').filter(l => l.trim() && !l.startsWith('#'))
|
|
.concat(['~/\\.config', '\\bpid \\d+\\b', '0\\.0\\.0\\.0:\\d+', '\\bPC [12]\\b', '\\bthe Mac\\b', '/Users/']);
|
|
const leaks = [];
|
|
for (const pat of forbid) { let re; try { re = new RegExp(pat, 'm'); } catch { continue; } const m = re.exec(html); if (m) leaks.push(`${pat}: ...${html.slice(Math.max(0, m.index - 40), m.index + 60).replace(/\s+/g, ' ')}...`); }
|
|
if (leaks.length) { console.error('ledger page: forbidden text would reach the public page:'); leaks.forEach(l => console.error(' ' + l)); process.exit(1); }
|
|
writeFileSync(out, html);
|
|
console.log(`${out}: ${entries.length} entries, counts ${JSON.stringify(counts)}`);
|