#!/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("")) { const before = next; next = next.replace( /\n?[\s\S]*?\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();