#!/usr/bin/env tsx /** * count-roadmap-progress.ts — parse the v3 cross-repo UX roadmap doc and * regenerate the "§11.2 Progress at a glance" block with live counts. * * Usage: * pnpm dlx tsx scripts/count-roadmap-progress.ts \ * docs/UI_ROADMAP_2026_V3_CROSS_REPO.md * * Behaviour: * - Reads the given doc. * - Counts `- [ ]` (open) and `- [x]` / `- [X]` (closed) checkboxes in each * §11.x sub-section by parsing `### 11.N Wave …` headings. * - Rewrites the fenced block inside `### 11.2 Progress at a glance` * in-place so the visible counters match reality. * - Also rewrites the `· `0 / N`` suffix on every `### 11.x Wave …` * heading so per-wave totals stay accurate. * * Idempotent: re-running with no checkbox changes is a no-op. * Exit codes: 0 ok, 1 doc not found, 2 expected sections missing. * * Authored: Wave 8.A.7 of v3.2 roadmap. */ import { readFileSync, writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; const docPath = resolve(process.argv[2] ?? 'docs/UI_ROADMAP_2026_V3_CROSS_REPO.md'); let src: string; try { src = readFileSync(docPath, 'utf8'); } catch { console.error(`[count-roadmap-progress] cannot read ${docPath}`); process.exit(1); } interface Section { /** Heading label (e.g. "Wave 8 Rollout"). */ label: string; /** Regex matching the `### 11.x …` heading. */ headingRe: RegExp; /** Regex matching the next `### 11.x …` heading (acts as end-marker). */ nextHeadingRe: RegExp; } const sections: Section[] = [ { label: 'Wave 8 Rollout', headingRe: /^### 11\.3 Wave 8 .*$/m, nextHeadingRe: /^### 11\.4 /m }, { label: 'Wave 9 Data', headingRe: /^### 11\.4 Wave 9 .*$/m, nextHeadingRe: /^### 11\.5 /m }, { label: 'Wave 10 Shells', headingRe: /^### 11\.5 Wave 10 .*$/m, nextHeadingRe: /^### 11\.6 /m }, { label: 'Wave 11 Adaptive', headingRe: /^### 11\.6 Wave 11 .*$/m, nextHeadingRe: /^### 11\.7 /m, }, { label: 'Wave 12 Mobile', headingRe: /^### 11\.7 Wave 12 .*$/m, nextHeadingRe: /^### 11\.8 /m }, { label: 'Wave 13 Futurism', headingRe: /^### 11\.8 Wave 13 .*$/m, nextHeadingRe: /^### 11\.9 /m, }, { label: 'Cross-cutting', headingRe: /^### 11\.9 Cross-cutting .*$/m, nextHeadingRe: /^### 11\.10 /m, }, { label: 'Magnet demos', headingRe: /^### 11\.10 .*$/m, nextHeadingRe: /^---\s*$/m }, ]; function sliceSection(text: string, start: RegExp, end: RegExp): string | null { const startMatch = start.exec(text); if (!startMatch) return null; const after = text.slice(startMatch.index); const endMatch = end.exec(after); return endMatch ? after.slice(0, endMatch.index) : after; } function countChecks(block: string): { open: number; done: number } { const open = (block.match(/^- \[ \]/gm) ?? []).length; const done = (block.match(/^- \[[xX]\]/gm) ?? []).length; return { open, done }; } function bar(done: number, total: number, width = 10): string { if (total === 0) return '⬛'.repeat(width); const filled = Math.round((done / total) * width); return '🟩'.repeat(filled) + '⬛'.repeat(width - filled); } function pct(done: number, total: number): string { if (total === 0) return ' 0%'; return `${String(Math.round((done / total) * 100)).padStart(3)}%`; } interface Tally { label: string; done: number; total: number; } const tallies: Tally[] = []; let totalDone = 0; let totalAll = 0; for (const s of sections) { const block = sliceSection(src, s.headingRe, s.nextHeadingRe); if (block === null) { console.error(`[count-roadmap-progress] section not found: ${s.label}`); process.exit(2); } const { open, done } = countChecks(block); const total = open + done; tallies.push({ label: s.label, done, total }); totalDone += done; totalAll += total; } // Build the new "§11.2 Progress at a glance" fenced block. const lines: string[] = []; lines.push( `TOTAL ${String(totalDone).padStart(3)} / ${String(totalAll).padStart(3)} ${bar(totalDone, totalAll)} ${pct(totalDone, totalAll)}` ); lines.push('─'.repeat(45)); const labelWidth = Math.max(...tallies.map(t => t.label.length)); for (const t of tallies) { lines.push( `${t.label.padEnd(labelWidth)} ${String(t.done).padStart(3)} / ${String(t.total).padStart(3)} ${bar(t.done, t.total)} ${pct(t.done, t.total)}` ); } const newBlock = lines.join('\n'); // Locate the existing fenced block inside §11.2 and replace its body. const headerRe = /(### 11\.2 Progress at a glance\s*\n+```)([\s\S]*?)(```)/; const headerMatch = src.match(headerRe); if (!headerMatch) { console.error('[count-roadmap-progress] §11.2 fenced block not found'); process.exit(2); } const replaced = src.replace( headerRe, (_full, open, _body, close) => `${open}\n${newBlock}\n${close}` ); // Also rewrite the per-section `· \`N / M\`` suffix on §11.3 .. §11.10 headings. let withSuffixes = replaced; const headingSuffixMap: Array<[RegExp, Tally]> = [ [/^(### 11\.3 Wave 8 — Unblock & rollout) · `[^`]*`$/m, tallies[0]!], [/^(### 11\.4 Wave 9 — Data, content, search) · `[^`]*`$/m, tallies[1]!], [/^(### 11\.5 Wave 10 — Product surfaces & shells) · `[^`]*`$/m, tallies[2]!], [/^(### 11\.6 Wave 11 — Adaptive \/ ambient \/ multimodal) · `[^`]*`$/m, tallies[3]!], [/^(### 11\.7 Wave 12 — Mobile · i18n · sustainability) · `[^`]*`$/m, tallies[4]!], [/^(### 11\.8 Wave 13 — Futurism layer) · `[^`]*`$/m, tallies[5]!], [/^(### 11\.9 Cross-cutting) · `[^`]*`$/m, tallies[6]!], ]; for (const [re, t] of headingSuffixMap) { withSuffixes = withSuffixes.replace(re, (_match, head) => `${head} · \`${t.done} / ${t.total}\``); } if (withSuffixes === src) { console.log('[count-roadmap-progress] up to date — no changes'); } else { writeFileSync(docPath, withSuffixes, 'utf8'); console.log( `[count-roadmap-progress] rewrote §11.2 + per-wave headings (${totalDone} / ${totalAll} done)` ); }