237 lines
7.0 KiB
TypeScript
237 lines
7.0 KiB
TypeScript
#!/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?<!-- begin:auto-spec-link -->[\s\S]*?<!-- end:auto-spec-link -->\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();
|