Files
suaveplan/scripts/cleanup-md-monorepo-refs.ts
2026-05-27 23:47:53 +01:00

371 lines
27 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bun
/**
* cleanup-md-monorepo-refs — carefully scrub genesis/monorepo/stage metadata
* from library .md files so they read as standalone packages.
*
* Reads `.md-cleanup-todo.txt` (path|line1,line2,...), processes files whose
* matches are all in the first N lines, and removes them from the todo.
*
* Usage:
* bun scripts/cleanup-md-monorepo-refs.ts # process all
* bun scripts/cleanup-md-monorepo-refs.ts --first10 # only first-10-line files
* bun scripts/cleanup-md-monorepo-refs.ts --readme # only README.md files
* bun scripts/cleanup-md-monorepo-refs.ts --dry-run # preview
*/
import { readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
const TODO_PATH = ".md-cleanup-todo.txt";
interface TodoItem {
path: string;
lines: number[];
}
function parseTodo(content: string): TodoItem[] {
const items: TodoItem[] = [];
for (const line of content.split("\n")) {
const pipe = line.indexOf("|");
if (pipe === -1) continue;
const path = line.slice(0, pipe).trim();
const lines = line
.slice(pipe + 1)
.split(",")
.map((s) => parseInt(s.trim(), 10))
.filter((n) => !isNaN(n));
if (path && lines.length > 0) items.push({ path, lines });
}
return items;
}
function serializeTodo(items: TodoItem[]): string {
return items.map((i) => `${i.path}|${i.lines.join(",")}`).join("\n") + "\n";
}
function maxLine(lines: number[]): number {
return Math.max(...lines);
}
/**
* Very conservative replacements. Each function receives the full text and
* returns the scrubbed text.
*/
function scrub(text: string, filePath: string): { next: string; edits: string[] } {
const edits: string[] = [];
let next = text;
// 1. Remove OpenSpec link blocks safely
if (next.includes("<!-- begin:auto-spec-link -->")) {
const before = next;
next = next.replace(
/\n?<!-- begin:auto-spec-link -->[\s\S]*?<!-- end:auto-spec-link -->\n?/g,
"\n"
);
if (next !== before) edits.push("removed OpenSpec block");
}
// 2. Remove bare "## OpenSpec" heading + following spec-link paragraph
if (/\n## OpenSpec\n/.test(next)) {
const before = next;
next = next.replace(
/\n## OpenSpec\n\nCanonical capability spec:[^\n]*\n/g,
"\n"
);
if (next !== before) edits.push("removed OpenSpec heading");
}
// 3. Remove Stage-N prefix from H1 title lines (only if line starts with # @suaveplan/... - Stage-N ...)
// Use a regex that only touches spaces/tabs, never newlines.
const h1StagePattern =
/^(# @suaveplan\/[\w-]+)[ \t]+-[ \t]+Stage-?\d+(?:\.\d+)?[ \t]+/gim;
if (h1StagePattern.test(next)) {
next = next.replace(h1StagePattern, "$1 — ");
edits.push("removed Stage from H1");
}
// 4. Remove "Stage N — " or "Stage-N " at the very start of a paragraph (after blank line or start of file)
const stagePrefixPattern =
/(^|\n\n)[ \t]*Stage-?\d+(?:\.\d+)?[ \t]*(?:—|-)[ \t]*/gi;
if (stagePrefixPattern.test(next)) {
next = next.replace(stagePrefixPattern, "$1");
edits.push("removed Stage prefix from paragraph");
}
// 5. Replace specific monorepo phrases
const phraseReplacements: { pattern: RegExp; replacement: string; label: string }[] = [
{ pattern: /\bfor the Suaveplan monorepo\b/gi, replacement: "for Suaveplan", label: "monorepo phrase" },
{ pattern: /\bthe Suaveplan monorepo\b/gi, replacement: "Suaveplan", label: "monorepo phrase" },
{ pattern: /\bthe genesis monorepo\b/gi, replacement: "Suaveplan", label: "genesis monorepo" },
{ pattern: /\bgenesis `@suaveplan\//gi, replacement: "`@suaveplan/", label: "genesis pkg prefix" },
{ pattern: /\bfor the `@suaveplan\/api` plugin foundation\b/gi, replacement: "for `@suaveplan/api`", label: "plugin foundation" },
{ pattern: /\bthe `@suaveplan\/api` plugin foundation\b/gi, replacement: "`@suaveplan/api`", label: "plugin foundation" },
{ pattern: /\bplugin foundation\b/gi, replacement: "plugin system", label: "plugin foundation" },
{ pattern: /\bGenesis Stage \d+(?:\.\d+)?[^.]*\.\s*/gi, replacement: "", label: "Genesis Stage sentence" },
{ pattern: /\bGenesis-wave package[^.]*\.\s*/gi, replacement: "", label: "Genesis-wave sentence" },
{ pattern: /\bPer design doc §\d+(?:\.\d+)*,\s+genesis\s+[^.]*\.\s*/gi, replacement: "", label: "design-doc genesis sentence" },
{ pattern: /\bgenesis (fix|package|design|crypto|wave|port|rewrite|drops?|error|monorepo)\b/gi, replacement: "$1", label: "genesis adjective" },
{ pattern: /\bStage \d+(?:\.\d+)?\s+of the umbrella plan\b/gi, replacement: "the design", label: "umbrella plan" },
{ pattern: /\bof the umbrella plan\s*\([^)]*openspec[^)]*\)\b/gi, replacement: "of the design", label: "umbrella plan" },
{ pattern: /\bOpenSpec change\b/gi, replacement: "spec change", label: "OpenSpec change" },
{ pattern: /\bOpenSpec-tracked\b/gi, replacement: "tracked", label: "OpenSpec-tracked" },
{ pattern: /\bgenesis-only\b/gi, replacement: "", label: "genesis-only" },
{ pattern: /\bgenesis original\b/gi, replacement: "original", label: "genesis original" },
{ pattern: /\bgenesis-side\b/gi, replacement: "", label: "genesis-side" },
{ pattern: /\bgenesis `EventBus/gi, replacement: "`EventBus", label: "genesis EventBus" },
{ pattern: /\bgenesis `tsconfig/gi, replacement: "`tsconfig", label: "genesis tsconfig" },
{ pattern: /\bgenesis `exactOptionalPropertyTypes/gi, replacement: "`exactOptionalPropertyTypes", label: "genesis exactOptionalPropertyTypes" },
{ pattern: /\bgenesis error ecosystem/gi, replacement: "error ecosystem", label: "genesis error ecosystem" },
{ pattern: /\bgenesis dependency graph/gi, replacement: "dependency graph", label: "genesis dependency graph" },
{ pattern: /\bgenesis stack/gi, replacement: "stack", label: "genesis stack" },
{ pattern: /\bgenesis hardening/gi, replacement: "hardening", label: "genesis hardening" },
{ pattern: /\bgenesis family/gi, replacement: "family", label: "genesis family" },
{ pattern: /\bgenesis cache family/gi, replacement: "cache family", label: "genesis cache family" },
{ pattern: /\bgenesis audio family/gi, replacement: "audio family", label: "genesis audio family" },
{ pattern: /\bgenesis mesh/gi, replacement: "mesh", label: "genesis mesh" },
{ pattern: /\bthe monorepo\b/gi, replacement: "the project", label: "the monorepo" },
{ pattern: /\bthe rest of the monorepo\b/gi, replacement: "the rest of the project", label: "rest of monorepo" },
{ pattern: /\bmonorepo-wide\b/gi, replacement: "project-wide", label: "monorepo-wide" },
{ pattern: /\bmonorepo manifest\b/gi, replacement: "project manifest", label: "monorepo manifest" },
{ pattern: /\bmonorepo Python-tier story\b/gi, replacement: "Python tooling story", label: "monorepo Python" },
{ pattern: /\bbroader monorepo\b/gi, replacement: "broader project", label: "broader monorepo" },
{ pattern: /\bextends the monorepo/gi, replacement: "extends the project", label: "extends monorepo" },
{ pattern: /\bExtending the monorepo/gi, replacement: "Extending the project", label: "extends monorepo" },
{ pattern: /\bthe genesis `audio-decode-\*\*` family/gi, replacement: "the `audio-decode-*` family", label: "genesis audio-decode family" },
{ pattern: /\bthe first downstream consumer is `@suaveplan\/audio-ir-loader`\./gi, replacement: "", label: "first downstream consumer" },
{ pattern: /\bgenesis surface\b/gi, replacement: "surface", label: "genesis surface" },
{ pattern: /\bgenesis-canonical\b/gi, replacement: "canonical", label: "genesis-canonical" },
{ pattern: /\bgenesis preserves\b/gi, replacement: "preserves", label: "genesis preserves" },
{ pattern: /\bgenesis cause-chain\b/gi, replacement: "cause-chain", label: "genesis cause-chain" },
{ pattern: /\bgenesis codec plugin\b/gi, replacement: "codec plugin", label: "genesis codec plugin" },
{ pattern: /\bgenesis voice domain conventions\b/gi, replacement: "voice domain conventions", label: "genesis voice domain conventions" },
{ pattern: /\bgenesis voice\b/gi, replacement: "voice", label: "genesis voice" },
{ pattern: /\bgenesis rule\b/gi, replacement: "rule", label: "genesis rule" },
{ pattern: /\bgenesis-added\b/gi, replacement: "added", label: "genesis-added" },
{ pattern: /\bgenesis exposes\b/gi, replacement: "exposes", label: "genesis exposes" },
{ pattern: /\bgenesis WebSocket\b/gi, replacement: "WebSocket", label: "genesis WebSocket" },
{ pattern: /\bgenesis OpenSpec\b/gi, replacement: "OpenSpec", label: "genesis OpenSpec" },
{ pattern: /\bgenesis-wide convention\b/gi, replacement: "convention", label: "genesis-wide convention" },
{ pattern: /\bgenesis packages\b/gi, replacement: "packages", label: "genesis packages" },
{ pattern: /\bgenesis packages? that\b/gi, replacement: "packages that", label: "genesis packages that" },
{ pattern: /\bgenesis (\w+ing)\b/gi, replacement: "$1", label: "genesis ...ing" },
{ pattern: /\bGenesis includes it\b/gi, replacement: "It is included", label: "Genesis includes it" },
{ pattern: /\bGenesis treats it\b/gi, replacement: "It is treated", label: "Genesis treats it" },
{ pattern: /\bGenesis treats\b/gi, replacement: "The package treats", label: "Genesis treats" },
{ pattern: /\bGenesis's preferred\b/gi, replacement: "Preferred", label: "Genesis's preferred" },
{ pattern: /\bGenesis preserves\b/gi, replacement: "Preserves", label: "Genesis preserves" },
{ pattern: /\bGenesis closes\b/gi, replacement: "This package closes", label: "Genesis closes" },
{ pattern: /\bmonorepo with dozens of packages\b/gi, replacement: "package ecosystem", label: "monorepo with dozens" },
{ pattern: /\bmonorepo's shared code space\b/gi, replacement: "shared code space", label: "monorepo's shared code" },
{ pattern: /\bmonorepo\. Wraps\b/gi, replacement: ". Wraps", label: "monorepo. Wraps" },
{ pattern: /\bthe wider monorepo\b/gi, replacement: "the wider ecosystem", label: "wider monorepo" },
{ pattern: /\bopenspec-conformance\b/gi, replacement: "conformance", label: "openspec-conformance" },
{ pattern: /\bthe other genesis\b/gi, replacement: "the other", label: "the other genesis" },
{ pattern: /\bthe genesis (lint|docs-check|storage|lint profile|cause-chain|WebRTC|WebSocket|codec|voice|surface)\b/gi, replacement: "the $1", label: "the genesis ..." },
{ pattern: /\bgenesis (lint profile|docs-check rule|storage family|storage backend|HTTP-stat parser|tier package)\b/gi, replacement: "$1", label: "genesis ..." },
{ pattern: /\bgenesis-app\b/gi, replacement: "app", label: "genesis-app" },
{ pattern: /\bgenesis bans\b/gi, replacement: "the test suite bans", label: "genesis bans" },
{ pattern: /\bgenesis-tier\b/gi, replacement: "", label: "genesis-tier" },
{ pattern: /\bgenesis-specific\b/gi, replacement: "specific", label: "genesis-specific" },
{ pattern: /\bGenesis-specific\b/gi, replacement: "Specific", label: "Genesis-specific" },
{ pattern: /\bGenesis sits\b/gi, replacement: "This package sits", label: "Genesis sits" },
{ pattern: /\bgithub\.com\/SuavePlan\/genesis\b/gi, replacement: "github.com/SuavePlan/suaveplan", label: "github.com/SuavePlan/genesis" },
{ pattern: /\bgenesis (\w+ing)\b/gi, replacement: "$1", label: "genesis ...ing" },
{ pattern: /\bgenesis project rules\b/gi, replacement: "project rules", label: "genesis project rules" },
{ pattern: /\bgenesis convention\b/gi, replacement: "convention", label: "genesis convention" },
{ pattern: /\bgenesis 300-line hard cap\b/gi, replacement: "300-line hard cap", label: "genesis 300-line" },
{ pattern: /\bgenesis wrapper\b/gi, replacement: "wrapper", label: "genesis wrapper" },
{ pattern: /\bgenesis pre-1\.0\b/gi, replacement: "pre-1.0", label: "genesis pre-1.0" },
{ pattern: /\bgenesis 5-files-per-feature convention\b/gi, replacement: "5-files-per-feature convention", label: "genesis 5-files" },
{ pattern: /\bPre-Genesis consumer\b/gi, replacement: "Pre-migration consumer", label: "Pre-Genesis consumer" },
{ pattern: /\bPre-genesis consumer\b/gi, replacement: "Pre-migration consumer", label: "Pre-genesis consumer" },
{ pattern: /\bgenesis Zod import rule\b/gi, replacement: "Zod import rule", label: "genesis Zod import" },
{ pattern: /\bgenesis boundary\b/gi, replacement: "boundary", label: "genesis boundary" },
{ pattern: /\bgenesis tsconfig\b/gi, replacement: "tsconfig", label: "genesis tsconfig" },
{ pattern: /\bthis monorepo\b/gi, replacement: "this project", label: "this monorepo" },
{ pattern: /\bPer the genesis\b/gi, replacement: "Per the", label: "Per the genesis" },
{ pattern: /\bgenesis (error|cause-chain|lint|docs-check|storage|codec|voice|surface|WebRTC|WebSocket)\b/gi, replacement: "$1", label: "genesis generic" },
{ pattern: /\bGenesis (error|cause-chain|lint|docs-check|storage|codec|voice|surface|WebRTC|WebSocket)\b/gi, replacement: "$1", label: "Genesis generic" },
{ pattern: /\bgenesis\b/gi, replacement: "", label: "genesis fallback" },
{ pattern: /\bGenesis\b/gi, replacement: "", label: "Genesis fallback" },
{ pattern: /\bmonorepo\b/gi, replacement: "project", label: "monorepo fallback" },
{ pattern: /\bMonorepo\b/gi, replacement: "Project", label: "Monorepo fallback" },
{ pattern: /\bstage[- ]?\d+\b/gi, replacement: "", label: "stage fallback" },
{ pattern: /\bStage[- ]?\d+\b/gi, replacement: "", label: "Stage fallback" },
{ pattern: /\bopenspec\b/gi, replacement: "specification", label: "openspec fallback" },
{ pattern: /\bOpenspec\b/gi, replacement: "Specification", label: "Openspec fallback" },
{ pattern: /\bOpenSpec\b/gi, replacement: "OpenSpec", label: "OpenSpec keep" },
{ pattern: /\bplugin foundation\b/gi, replacement: "plugin", label: "plugin foundation" },
{ pattern: /\bon genesis\b/gi, replacement: "", label: "on genesis" },
{ pattern: /\bin genesis\b/gi, replacement: "", label: "in genesis" },
{ pattern: /\bhas not yet landed on genesis\b/gi, replacement: "has not yet shipped", label: "not landed on genesis" },
{ pattern: /\bpackages not yet in genesis\b/gi, replacement: "packages not yet shipped", label: "not yet in genesis" },
{ pattern: /\bsuaveplan main, v\d+\.\d+\.\d+\) plus genesis hardening/gi, replacement: "harded", label: "suaveplan main plus genesis" },
{ pattern: /\bgenesis improvement\b/gi, replacement: "improvement", label: "genesis improvement" },
{ pattern: /\bgenesis game type/gi, replacement: "game type", label: "genesis game type" },
{ pattern: /\bgenesis `Middleware`/gi, replacement: "`Middleware`", label: "genesis Middleware" },
{ pattern: /\bgenesis `storage-uri-plugin-bundle`/gi, replacement: "`storage-uri-plugin-bundle`", label: "genesis storage-uri" },
{ pattern: /\bGenesis-original\b/gi, replacement: "Original", label: "Genesis-original" },
{ pattern: /\bgenesis-original\b/gi, replacement: "original", label: "genesis-original" },
{ pattern: /\bgenesis bootstrap landing/gi, replacement: "bootstrap landing", label: "genesis bootstrap" },
{ pattern: /\bgenesis JSX output/gi, replacement: "JSX output", label: "genesis JSX" },
{ pattern: /\bduring genesis\b/gi, replacement: "", label: "during genesis" },
{ pattern: /\bthe genesis port/gi, replacement: "the port", label: "genesis port" },
{ pattern: /\bthe genesis rewrite/gi, replacement: "the rewrite", label: "genesis rewrite" },
{ pattern: /\bthis migration/gi, replacement: "this release", label: "this migration" },
{ pattern: /\bgenesis deps\b/gi, replacement: "dependencies", label: "genesis deps" },
{ pattern: /\bgenesis `([^`]+)`/gi, replacement: "`$1`", label: "genesis backtick" },
{ pattern: /\bgenesis API-plugin/gi, replacement: "API-plugin", label: "genesis API-plugin" },
{ pattern: /\bgenesis ship\b/gi, replacement: "ship", label: "genesis ship" },
{ pattern: /\binitial genesis ship\b/gi, replacement: "initial ship", label: "initial genesis ship" },
{ pattern: /\blanding on `?genesis`?\b/gi, replacement: "shipping", label: "landing on genesis" },
{ pattern: /\bmigrated into genesis\b/gi, replacement: "being migrated", label: "migrated into genesis" },
{ pattern: /\bgenesis plugin\b/gi, replacement: "plugin", label: "genesis plugin" },
{ pattern: /\bgenesis catalog\b/gi, replacement: "catalog", label: "genesis catalog" },
{ pattern: /\bgenesis SurrealDB repository\b/gi, replacement: "SurrealDB repository", label: "genesis SurrealDB" },
{ pattern: /\bgenesis sibling adapter\b/gi, replacement: "sibling adapter", label: "genesis sibling" },
{ pattern: /\bgenesis-native sibling\b/gi, replacement: "native sibling", label: "genesis-native sibling" },
{ pattern: /\bgenesis dehydrated-drive workflow\b/gi, replacement: "dehydrated-drive workflow", label: "genesis dehydrated-drive" },
{ pattern: /\bgenesis drive mesh\b/gi, replacement: "drive mesh", label: "genesis drive mesh" },
{ pattern: /\bgenesis migration\b/gi, replacement: "migration", label: "genesis migration" },
{ pattern: /\bgenesis network bootstrap\b/gi, replacement: "network bootstrap", label: "genesis network bootstrap" },
{ pattern: /\bgenesis license package\b/gi, replacement: "license package", label: "genesis license" },
{ pattern: /\bGenesis enterprise extras\b/gi, replacement: "Enterprise extras", label: "Genesis enterprise extras" },
{ pattern: /\bgenesis style\b/gi, replacement: "style", label: "genesis style" },
{ pattern: /\bgenesis-stage rebuild\b/gi, replacement: "rebuild", label: "genesis-stage rebuild" },
{ pattern: /\bgenesis determin/gi, replacement: "determin", label: "genesis determin" },
{ pattern: /\bgenesis foundation\b/gi, replacement: "foundation", label: "genesis foundation" },
{ pattern: /\bgenesis default/gi, replacement: "default", label: "genesis default" },
{ pattern: /\bgenesis found/gi, replacement: "found", label: "genesis found" },
{ pattern: /\bgenesis imports/gi, replacement: "imports", label: "genesis imports" },
{ pattern: /\bgenesis `EventBus/gi, replacement: "`EventBus", label: "genesis EventBus" },
{ pattern: /\bmonorepo root directory\b/gi, replacement: "project root directory", label: "monorepo root directory" },
{ pattern: /\bmonorepo sense\b/gi, replacement: "sense", label: "monorepo sense" },
{ pattern: /\bSuaveplan monorepo\./gi, replacement: "Suaveplan.", label: "Suaveplan monorepo." },
{ pattern: /\bStage-1 contracts only\b/gi, replacement: "contracts only", label: "Stage-1 contracts only" },
{ pattern: /\bStage-1 contract package:\b/gi, replacement: "Contract package:", label: "Stage-1 contract package:" },
{ pattern: /\bStage 7\b\./gi, replacement: ".", label: "Stage 7." },
{ pattern: /\bStage 1 \(Contracts\) of the API plugin system umbrella\b/gi, replacement: "Contracts of the API plugin system umbrella", label: "Stage 1 Contracts umbrella" },
{ pattern: /\bStage-\d+'s\b/gi, replacement: "", label: "Stage-N's" },
{ pattern: /\bStage-\d+ must\b/gi, replacement: "must", label: "Stage-N must" },
{ pattern: /\bStage \d+ can be declared complete\b/gi, replacement: "can be declared complete", label: "Stage N declared complete" },
{ pattern: /\bStage \d+ and plug behind\b/gi, replacement: "and plug behind", label: "Stage N plug behind" },
{ pattern: /\b`api-tenant-provider-memory` \(Stage \d+\)/gi, replacement: "`api-tenant-provider-memory`", label: "api-tenant-provider-memory Stage" },
{ pattern: /\bStage \d+\),/gi, replacement: "),", label: "Stage N)," },
{ pattern: /\bStage 1 \(this release\)\b/gi, replacement: "this release", label: "Stage 1 this release" },
{ pattern: /\bStage 1 defines\b/gi, replacement: "defines", label: "Stage 1 defines" },
{ pattern: /\bStage 1 ships\b/gi, replacement: "ships", label: "Stage 1 ships" },
{ pattern: /\bon genesis\b/gi, replacement: "", label: "on genesis" },
{ pattern: /\bgenesis onion-model/gi, replacement: "onion-model", label: "genesis onion-model" },
{ pattern: /\bthe genesis `([^`]+)`/gi, replacement: "the `$1`", label: "the genesis backtick" },
{ pattern: /\bpart of the genesis `([^`]+)`/gi, replacement: "part of the `$1`", label: "part of genesis backtick" },
{ pattern: /\bgenesis uses/gi, replacement: "uses", label: "genesis uses" },
{ pattern: /\b`genesis`\b/gi, replacement: "", label: "backtick genesis" },
{ pattern: /\bGenesis ships\b/gi, replacement: "Ships", label: "Genesis ships" },
{ pattern: /\bGenesis uses/gi, replacement: "Uses", label: "Genesis uses" },
{ pattern: /\bforward reference to Stage \d+\b/gi, replacement: "forward reference", label: "forward reference to Stage" },
{ pattern: /\bin subsequent Stage-\d+ changes?\b/gi, replacement: "in subsequent changes", label: "subsequent Stage changes" },
{ pattern: /\bStage-\d+(?:\.\d+)? (acceptance gate|scope|changes?)\b/gi, replacement: "$1", label: "Stage noun" },
{ pattern: /\bThe Stage-\d+(?:\.\d+)? /gi, replacement: "The ", label: "The Stage-N" },
{ pattern: /\b(Stage \d+(?:\.\d+)?) host\b/gi, replacement: "host", label: "Stage N host" },
{ pattern: /\bStage \d+(?:\.\d+)? (keystone|testing|precondition gate|consumer|DB adapter|tenant resolver|concrete|Apollo Server GraphQL engine adapter|contract package|service-to-service|fixed-principal|trivial session resolver|Better-Auth adapter|concrete JWT|concrete Hono|concrete `Router`|concrete in-memory|concrete WebAssembly|concrete WebSocket|concrete WebSocket broker|concrete Apollo|concrete GraphQL|concrete GraphQL Yoga|concrete static|concrete tenant|concrete database|concrete event|concrete job|concrete logger|concrete manifest|concrete migration|concrete OpenAPI|concrete rate-limit|concrete router|concrete saga|concrete sandbox|concrete secrets|concrete SSE|concrete WS|concrete worker|concrete WS broker|concrete WS Bun)\b/gi, replacement: "$2", label: "Stage N prefix" },
{ pattern: /\bStage \d+(?:\.\d+)? (of the|—|–|-)\s*/gi, replacement: "", label: "Stage N of" },
{ pattern: /\bin Stage \d+(?:\.\d+)?\b/gi, replacement: "", label: "in Stage N" },
{ pattern: /\b\(Stage \d+(?:\.\d+)?[^)]*\)/gi, replacement: "", label: "(Stage N)" },
{ pattern: /\bStage \d+\+\b/gi, replacement: "", label: "Stage N+" },
{ pattern: /\blive in @suaveplan\/[^ ]+ \(Stage \d+\)/gi, replacement: "", label: "live in pkg (Stage N)" },
{ pattern: /\bimplement(s|ed) the contracts here\.?\s*$/gim, replacement: "implements the contracts here.", label: "implements contracts" },
{ pattern: /\bthe migration change at `openspec\/archive\/[^`]+`/gi, replacement: "", label: "migration change openspec" },
{ pattern: /\b`openspec\/archive\/[^`]+`/gi, replacement: "", label: "openspec archive path" },
{ pattern: /\b`openspec\/[^`]+`/gi, replacement: "the spec", label: "openspec path" },
{ pattern: /\b`\.\.\/\.\.\/\.\.\/\.\.\/openspec\/[^`]+`/gi, replacement: "the spec", label: "relative openspec path" },
{ pattern: /\b`\.\.\/openspec\/[^`]+`/gi, replacement: "the spec", label: "relative openspec path" },
{ pattern: /\bsee repository root\b/gi, replacement: "see the repository", label: "see repository root" },
{ pattern: /\bVersion `0\.1\.0` during genesis\.\b/gi, replacement: "", label: "version during genesis" },
{ pattern: /\b`0\.1\.0` \(genesis-original\)\b/gi, replacement: "`0.1.0`", label: "genesis-original version" },
{ pattern: /\b`STAGE_\d+_RESERVED`\b/gi, replacement: "`RESERVED`", label: "STAGE_N_RESERVED" },
{ pattern: /\breserved for Stage \d+\b/gi, replacement: "reserved", label: "reserved for Stage" },
];
for (const { pattern, replacement, label } of phraseReplacements) {
const before = next;
next = next.replace(pattern, replacement);
if (next !== before) edits.push(label);
}
// Clean up double spaces and excess blank lines left behind
next = next.replace(/[ \t]+\n/g, "\n"); // trailing spaces
next = next.replace(/\n{3,}/g, "\n\n"); // 3+ blank lines -> 2
return { next, edits };
}
function main(): void {
const args = process.argv.slice(2);
const dryRun = args.includes("--dry-run");
const first10Only = args.includes("--first10");
const readmeOnly = args.includes("--readme");
const todoRaw = readFileSync(TODO_PATH, "utf-8");
let items = parseTodo(todoRaw);
if (readmeOnly) {
items = items.filter((i) => i.path.endsWith("README.md"));
}
if (first10Only) {
items = items.filter((i) => maxLine(i.lines) <= 10);
}
const remaining: TodoItem[] = [];
const processed: { path: string; edits: string[] }[] = [];
const errors: string[] = [];
for (const item of items) {
try {
const text = readFileSync(item.path, "utf-8");
const { next, edits } = scrub(text, item.path);
if (next !== text) {
if (!dryRun) writeFileSync(item.path, next);
processed.push({ path: item.path, edits: [...new Set(edits)] });
} else {
// No changes made — keep in todo for manual review
remaining.push(item);
}
} catch (err) {
errors.push(`${item.path}: ${err instanceof Error ? err.message : String(err)}`);
remaining.push(item);
continue;
}
}
// Rebuild todo with files that were NOT in this batch (original todo minus processed)
const originalItems = parseTodo(todoRaw);
const processedPaths = new Set(processed.map((p) => p.path));
const notProcessed = originalItems.filter((i) => !processedPaths.has(i.path));
if (!dryRun) {
writeFileSync(TODO_PATH, serializeTodo(notProcessed));
}
console.log("");
console.log(`Processed: ${processed.length}`);
console.log(`Remaining: ${notProcessed.length}`);
if (errors.length > 0) console.log(`Errors: ${errors.length}`);
if (processed.length > 0) {
console.log("");
console.log("Files changed:");
for (const { path, edits } of processed) {
console.log(` ${path} (${edits.join(", ")})`);
}
}
if (dryRun) {
console.log("");
console.log("(Dry-run — no files were written, todo not updated.)");
}
}
main();