#!/usr/bin/env bun /** * delib-monorepo-refs — scrub genesis / monorepo / stage / OpenSpec metadata * from individual library README.md files so they read as standalone packages. * * Usage: * bun scripts/delib-monorepo-refs.ts # rewrite * bun scripts/delib-monorepo-refs.ts --dry-run # preview */ import { readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { CONFIG } from "./lib/config.js"; import { walkMd } from "./lib/doc-tree.js"; const ROOT = CONFIG.paths.root; const PACKAGES_ROOT = join(ROOT, CONFIG.paths.packagesRoot); const REPLACEMENTS: { pattern: RegExp; replacement: string; note?: string }[] = [ // 1. Strip OpenSpec link blocks (won't resolve on npm) { pattern: /\n?[\s\S]*?\n?/g, replacement: "\n", note: "OpenSpec block", }, // 2. Stand-alone "## OpenSpec" heading + following spec-link line { pattern: /\n?## OpenSpec\n\nCanonical capability spec: `?[^`\n]+`?\.[ \t]*\n?/g, replacement: "\n", note: "OpenSpec heading", }, // 3. Remove Stage-N.N prefix from H1 title lines { pattern: /^(# @suaveplan\/[\w-]+)\s+-\s+Stage-?\d+(?:\.\d+)?\s+/gim, replacement: "$1 — ", note: "Stage in H1", }, // 4. Remove Stage-N.N at start of sentences / lines (common prose prefix) { pattern: /([.\n]|^)\s*Stage-?\d+(?:\.\d+)?\s+(concrete|abstract|trivial|bridge|rolled|Better-Auth|contract|package|audit|fixed-principal|session resolver|JWT|service-to-service|in-memory|reference|HTTP|WebSocket|in-process|fixed-Principal|concrete Hono|concrete GraphQL|concrete WebAssembly|concrete Apollo|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: "$1$2", note: "Stage prefix", }, // 5. Remove orphaned "Stage-N.N" that stands alone mid-sentence { pattern: /\bStage-?\d+(?:\.\d+)?\b/gi, replacement: "", note: "orphaned Stage", }, // 6. "Genesis Stage N — …" sentences { pattern: /\bGenesis Stage \d+(?:\.\d+)?[^.]*\.\s*/gi, replacement: "", note: "Genesis Stage sentence", }, // 7. "Genesis-wave package …" sentences { pattern: /\bGenesis-wave package[^.]*\.\s*/gi, replacement: "", note: "Genesis-wave sentence", }, // 8. "Per design doc §N.N, genesis ships …" sentences { pattern: /\bPer design doc §\d+(?:\.\d+)*,\s+genesis\s+[^.]*\.\s*/gi, replacement: "", note: "design-doc genesis sentence", }, // 9. "the Suaveplan monorepo" / "for the Suaveplan monorepo" { pattern: /\bfor the Suaveplan monorepo\b/gi, replacement: "for Suaveplan", note: "monorepo ref", }, { pattern: /\bthe Suaveplan monorepo\b/gi, replacement: "Suaveplan", note: "monorepo ref", }, // 10. "the genesis monorepo" { pattern: /\bthe genesis monorepo\b/gi, replacement: "Suaveplan", note: "genesis monorepo", }, // 11. "genesis `@suaveplan/...`" -> just the package name { pattern: /\bgenesis\s+(`@suaveplan\/[^`]+`)/gi, replacement: "$1", note: "genesis pkg prefix", }, // 12. "for the `@suaveplan/api` plugin foundation" { pattern: /\bfor the `@suaveplan\/api` plugin foundation\b/gi, replacement: "for `@suaveplan/api`", note: "plugin foundation", }, { pattern: /\bthe `@suaveplan\/api` plugin foundation\b/gi, replacement: "`@suaveplan/api`", note: "plugin foundation", }, { pattern: /\bplugin foundation\b/gi, replacement: "plugin system", note: "plugin foundation", }, // 13. Loose "genesis" when clearly referring to the project (not a name) { pattern: /\bgenesis (fix|package|design|crypto|wave|monorepo)\b/gi, replacement: "$1", note: "genesis adjective", }, // 14. "openspec/changes/..." paths in prose { pattern: /`?\.\.\/\.\.\/\.\.\/\.\.\/openspec\/[^`\n]+`?/g, replacement: "the spec", note: "openspec path", }, // 15. "OpenSpec change" / "OpenSpec-tracked" { pattern: /\bOpenSpec change\b/gi, replacement: "spec change", note: "OpenSpec change", }, { pattern: /\bOpenSpec-tracked\b/gi, replacement: "tracked", note: "OpenSpec-tracked", }, // 16. "of the umbrella plan (openspec/changes/...)" { pattern: /\bof the umbrella plan\s*\([^)]+openspec[^)]+\)\b/gi, replacement: "of the design", note: "umbrella plan", }, // 17. "Stage N of the umbrella plan" { pattern: /\bStage \d+(?:\.\d+)?\s+of the umbrella plan\b/gi, replacement: "the design", note: "Stage umbrella plan", }, ]; function scrub(content: string): { next: string; hits: string[] } { let next = content; const hits: string[] = []; for (const { pattern, replacement, note } of REPLACEMENTS) { const before = next; next = next.replace(pattern, replacement); if (next !== before && note) { hits.push(note); } } // Collapse multiple blank lines left behind next = next.replace(/\n{3,}/g, "\n\n"); return { next, hits }; } function main(): void { const dryRun = process.argv.slice(2).includes("--dry-run"); const readmes = walkMd(PACKAGES_ROOT, { ignoreDirs: ["node_modules", "dist", "build", "coverage", ".turbo"], }).filter((p) => basename(p) === "README.md"); let changed = 0; let unchanged = 0; const report: string[] = []; for (const readmePath of readmes) { const content = readFileSync(readmePath, "utf-8"); const { next, hits } = scrub(content); if (next === content) { unchanged++; continue; } const rel = readmePath.replace(ROOT + "/", ""); const uniqueHits = [...new Set(hits)]; report.push(`${rel} (${uniqueHits.join(", ")})`); if (!dryRun) { writeFileSync(readmePath, next); } changed++; } console.log(""); console.log(`✏️ Changed: ${changed}`); console.log(`⏭️ Unchanged: ${unchanged}`); if (report.length > 0) { console.log(""); console.log("Files touched:"); for (const line of report) console.log(` ${line}`); } if (dryRun) { console.log(""); console.log("(Dry-run — no files were written.)"); } } function basename(p: string): string { return p.slice(p.lastIndexOf("/") + 1); } main();