Phase 1 of the docs-by-domain reorg. Every snapshot now carries YAML frontmatter (domain + summary + tags); the auto-guess was hand-corrected against the full index (T6/interchain -> cross-chain, CDP/MBA/stablecoin -> fiatm, CitizenToken/Ballot -> governance, etc.). No file renamed/moved. build-snapshot-index.mjs now reads the frontmatter and emits a greppable INDEX.md: a top Quick-find table (No | domain | tags | summary) plus per-domain sections with summary + tags. 224/224 tagged, 8 domains, zero uncategorised. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
116 lines
6.3 KiB
JavaScript
116 lines
6.3 KiB
JavaScript
#!/usr/bin/env node
|
||
// build-snapshot-index.mjs — regenerate snapshots/INDEX.md from snapshot frontmatter.
|
||
//
|
||
// Each snapshot carries YAML frontmatter:
|
||
// ---
|
||
// domain: wallet|asm|fiatm|cross-chain|games-tote|governance|onramp|infra
|
||
// summary: "one concise line"
|
||
// tags: comma, separated, keywords (optional)
|
||
// ---
|
||
// Domain source, in priority order:
|
||
// 1. the explicit `domain:` tag (the durable path — every snapshot has one);
|
||
// 2. else keyword-bucketing of the filename slug (fallback for any stray
|
||
// untagged file — add a tag + rerun to fix it).
|
||
//
|
||
// Usage: node scripts/build-snapshot-index.mjs (run from repo root)
|
||
// The index is a DERIVED artifact — never hand-edit it; edit the tags + rerun.
|
||
import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||
import { join } from 'node:path';
|
||
|
||
const DIR = 'snapshots';
|
||
|
||
// Ordered domain buckets. `key` is the value the `domain:` tag must use.
|
||
// `kw` is the fallback slug-keyword bucketing for any untagged file.
|
||
const DOMAINS = [
|
||
{ key: 'wallet', title: 'Wallet UI (shells/app)', kw: ['wallet', 'shell', 'cosmogenesis', 'keystore', 'agora', 'artemision', 'hero', 'send', 'receive', 'sweep', 'pwa', 'ui', 'apps-tab', 'quickaction', 'onboarding', 'unlock', 'sign-message', 'connector', 'walletconnect', 'wizardconnect', 'cashconnect'] },
|
||
{ key: 'asm', title: 'ASM / Orbital Swap / Potidaea / Anthemus', kw: ['asm', 'swap', 'orbital', 'potidaea', 'anthemus', 'claim', 'reservoir', 'covenant', 'olympia', 'listing', 'sharepair', 'cauldron', 'clob', 'limit-order'] },
|
||
{ key: 'fiatm', title: 'FIATm / BCHm / MBA (backed currency)', kw: ['fiatm', 'bchm', 'mba', 'dea', 'dra', 'stablecoin', 'usdm', 'eurm', 'gbpm', 'jpym', 'inrm', 'arsm', 'trym', 'oracle', 'controller', 'dart', 'metal', 'dusd', 'paryon', 'peg', 'catalytic', 'backtest', 'elevation', 'equilibrium', 'redeem'] },
|
||
{ key: 'cross-chain', title: 'Cross-chain / interchain hub / T6', kw: ['cross-chain', 'crosschain', 'interchain', 'wrap', 'bridge', 'btc-', 'tron', 'ttrx', 'sepolia', 'deposit', 'lock-and-mint', 't6', 'htlc', 'ln-'] },
|
||
{ key: 'games-tote', title: 'Games / Tote / AnyHedge / Pari-mutuel', kw: ['tote', 'anyhedge', 'pari', 'prediction', 'football', 'game', 'checkers', 'event', 'oracle-tote', 'escrow'] },
|
||
{ key: 'governance', title: 'Governance (Athenae Politeia)', kw: ['voting', 'vote', 'athenae', 'politeia', 'ballot', 'citizen', 'complaint', 'application-covenant', 'sortition', 'plutocrat', 'elder', 'endorsement'] },
|
||
{ key: 'onramp', title: 'On-ramp / fiat rails', kw: ['onramp', 'on-ramp', 'ramp', 'provider'] },
|
||
{ key: 'infra', title: 'Infra / build / deploy / tooling', kw: ['deploy', 'beta', 'native', 'cli', 'test', 'consolidat', 'script', 'dashboard', 'registry', 'verify', 'migrat', 'refactor', 'adr', 'spec'] },
|
||
];
|
||
const MISC = { key: 'misc', title: 'Uncategorised (add a `domain:` tag)' };
|
||
const TITLE_OF = new Map([...DOMAINS, MISC].map((d) => [d.key, d.title]));
|
||
|
||
function frontmatter(text) {
|
||
const m = text.match(/^?---\r?\n([\s\S]*?)\r?\n---/);
|
||
if (!m) return {};
|
||
const body = m[1];
|
||
const domain = body.match(/^\s*domain:\s*([a-z0-9-]+)\s*$/im)?.[1]?.toLowerCase() ?? null;
|
||
let summary = body.match(/^\s*summary:\s*(.+?)\s*$/im)?.[1] ?? null;
|
||
if (summary) summary = summary.replace(/^"(.*)"$/s, '$1').replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
||
let tags = body.match(/^\s*tags:\s*(.*?)\s*$/im)?.[1] ?? '';
|
||
const tagList = tags.split(',').map((t) => t.trim()).filter(Boolean);
|
||
return { domain, summary, tags: tagList };
|
||
}
|
||
function h1(text, file) {
|
||
const m = text.match(/^#\s+(.+)$/m);
|
||
return (m ? m[1] : file.replace(/\.md$/, '')).trim().replace(/^Snapshot\s+\S+\s*[:—-]\s+/i, '');
|
||
}
|
||
function bucketOf(domain, file) {
|
||
if (domain && (TITLE_OF.has(domain))) return domain;
|
||
const slug = file.toLowerCase();
|
||
for (const d of DOMAINS) if (d.kw.some((k) => slug.includes(k))) return d.key;
|
||
return MISC.key;
|
||
}
|
||
const numOf = (file) => (file.match(/^(\d+)/)?.[1] ?? '');
|
||
const esc = (s) => s.replace(/\|/g, '\\|');
|
||
|
||
const files = readdirSync(DIR).filter((f) => f.endsWith('.md') && f !== 'INDEX.md').sort();
|
||
const groups = new Map([...DOMAINS, MISC].map((d) => [d.key, []]));
|
||
const all = [];
|
||
let tagged = 0;
|
||
for (const f of files) {
|
||
const text = readFileSync(join(DIR, f), 'utf8');
|
||
const fm = frontmatter(text);
|
||
if (fm.domain) tagged++;
|
||
const rec = {
|
||
file: f,
|
||
stem: f.replace(/\.md$/, ''),
|
||
num: numOf(f),
|
||
summary: fm.summary || h1(text, f),
|
||
tags: fm.tags || [],
|
||
domain: bucketOf(fm.domain, f),
|
||
};
|
||
groups.get(rec.domain).push(rec);
|
||
all.push(rec);
|
||
}
|
||
|
||
const L = [
|
||
'# Snapshots — index by domain',
|
||
'',
|
||
'> **Generated file — do not hand-edit.** Rebuild with `node scripts/build-snapshot-index.mjs`.',
|
||
'> Snapshots stay a flat, chronological changelog; this index is the domain view over them.',
|
||
'>',
|
||
'> **To place a snapshot:** set `domain: <wallet|asm|fiatm|cross-chain|games-tote|governance|onramp|infra>`',
|
||
'> (plus `summary:` and optional `tags:`) in its YAML frontmatter, then rerun this script.',
|
||
'> Untagged files fall back to filename-keyword bucketing.',
|
||
'>',
|
||
'> Duplicate snapshot numbers exist (0018, 0019, 0022, 0027, 0053, 0054, 0057, 0062, 0083, 0084,',
|
||
'> 0144, 0145, 0153, 0164) — they are distinct files, disambiguated below by slug.',
|
||
'',
|
||
`_${files.length} snapshots · ${tagged} tagged · ${[...groups].filter(([, v]) => v.length).length} domains · regenerate after adding one._`,
|
||
'',
|
||
'## Quick-find — all snapshots (Ctrl-F by №, domain, tag, or word)',
|
||
'',
|
||
'| № | domain | tags | summary |',
|
||
'| --- | --- | --- | --- |',
|
||
];
|
||
for (const r of all) {
|
||
L.push(`| [${r.num}](${r.file}) | ${r.domain} | ${esc(r.tags.join(' '))} | ${esc(r.summary)} |`);
|
||
}
|
||
L.push('');
|
||
for (const d of [...DOMAINS, MISC]) {
|
||
const items = groups.get(d.key);
|
||
if (!items.length) continue;
|
||
L.push(`## ${d.title} \`(${items.length})\``, '');
|
||
for (const r of items) {
|
||
const tags = r.tags.length ? ` · _${r.tags.join(', ')}_` : '';
|
||
L.push(`- [${r.stem}](${r.file}) — ${r.summary}${tags}`);
|
||
}
|
||
L.push('');
|
||
}
|
||
writeFileSync(join(DIR, 'INDEX.md'), L.join('\n'));
|
||
console.log(`Wrote ${DIR}/INDEX.md — ${files.length} snapshots (${tagged} tagged) across ${[...groups].filter(([, v]) => v.length).length} groups.`);
|