learning_ai_common_plat/scripts/count-roadmap-progress.ts
saravanakumardb1 e72323b8db docs(roadmap): v3.2 — showcase-first tracker + auto-counter script
Adds §11 to the v3 cross-repo UX roadmap: a 202-item, machine-parsable
checklist that coding agents flip as they ship work, with the
`learning_ai_uxui_web` showcase as the canonical visual-iteration
surface ahead of any product adoption.

──────────────────────────────────────────────────────────────────
§11 — Showcase-first workflow & live progress tracker
──────────────────────────────────────────────────────────────────
\u00a711.0  The showcase-first rule (non-negotiable 7-step recipe)
       1. Scaffold in common_plat/packages/<name>/
       2. Vendor snapshot to learning_ai_uxui_web/src/lib/<name>-preview/
       3. Showcase route at src/app/showcase/<group>/<slug>/page.tsx
          + catalog entry in src/catalog/routes.ts
       4. MSW mock any backend dependency
       5. Smoke test (axe + visual-regression baseline)
       6. Publish to Gitea registry; delete preview; swap imports
       7. Adopt in ≥ 1 product — that PR closes the checklist row

\u00a711.1  Agent update protocol — flip `- [ ]` → `- [x]` inline with
       the commit; trail the short-SHA in parentheses; multi-step
       rows tracked sub-bullet by sub-bullet.

\u00a711.2  Live "Progress at a glance" block (auto-rewritten by the
       counter script below).

\u00a711.3  Wave 8 Rollout       — 18 items
\u00a711.4  Wave 9 Data          — 42 items
\u00a711.5  Wave 10 Shells       — 35 items
\u00a711.6  Wave 11 Adaptive     — 26 items
\u00a711.7  Wave 12 Mobile       — 26 items
\u00a711.8  Wave 13 Futurism     — 39 items
\u00a711.9  Cross-cutting        —  8 items
\u00a711.10 Customer-magnet demos —  8 items
       ───────────────────────────────
       TOTAL                   — 202 items

Every package row carries a paired "**Showcase:**` /showcase/...`"
route entry so agents know exactly where the demo lives. Cross-
referenced with the per-product upgrade matrix in §5.

Renumbered hygiene §11 → §12. Header bumped v3.1 → v3.2.

──────────────────────────────────────────────────────────────────
scripts/count-roadmap-progress.ts
──────────────────────────────────────────────────────────────────
TypeScript node script (tsx) that:
  - Parses the v3 doc, counts `- [ ]` and `- [x]` per §11.x section
  - Rewrites the §11.2 fenced block in place with live counts +
    bar charts + percentages
  - Updates the `· \`N / M\`` suffix on every `### 11.x Wave …`
    heading so per-wave totals stay accurate
  - Idempotent: re-runs are no-ops when nothing changed
  - Verified: emits `0 / 202 done` on initial run; "up to date"
    on second run

Wired as Wave 8.A.7 (also tracked at CC.8 — should land in pre-commit
hook so the §11.2 block can never drift from reality).

Mirror in copilot/learning_ai_uxui_web follows in a paired commit.
2026-05-27 15:24:46 -07:00

166 lines
5.8 KiB
TypeScript

#!/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)`
);
}