/* RFC-1105: share-path-rewrite migrator — rewrites stale `@warpgogol/werkstatt-shared` specifiers carrying the dissolved `share/` segment in workpiece authored sources to the post-dissolution top-level subpaths. Idempotent: specifiers without the `/share` segment are untouched. Does not move or delete files — specifier rewrite only. Does not resolve the bare `…/share` barrel specifier — logged as a warning for manual repair (no default target exists after dissolution). Does not touch `@warpgogol/werkstatt-site/share/*` — different package, live namespace. RFC-1105: initial migrator — explicit destination table + identity promotion for domain dirs, warn-only for removed/bare specifiers. */ import path from "node:path"; import { collectFiles } from "@warpgogol/werkstatt-shared/node/fs"; import type { Migrator, SternsystemData, MigrationContext } from "./types.ts"; import { createDefaultIO, type DirEntry, type WorkspaceIO, } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; export const RFC_1105_MIGRATOR_ID = "rfc-1105"; const SCAN_EXTENSIONS = new Set([".ts", ".tsx", ".astro", ".md", ".mdx", ".mjs", ".js"]); const SKIP_DIRS = new Set([ "node_modules", "dist", ".astro", ".cache", ".turbo", ".wrangler", ".git", ]); const SPECIFIER_RE = /@warpgogol\/werkstatt-shared\/share(\/[A-Za-z0-9_.\-/]*)?/g; const SPECIFIER_DETECT_RE = /@warpgogol\/werkstatt-shared\/share/; /** * Non-identity destinations — longest-prefix match wins. * Mirrors the RFC-1105 destination map exactly. */ const EXPLICIT_MAP: ReadonlyArray = [ // Node-only exiles (must precede the `semantic` identity rule) ["semantic/markdown-twin-provenance", "node/semantic/markdown-twin-provenance"], ["semantic/derived-prices-loader", "node/semantic/derived-prices-loader"], ["fs", "node/fs"], ["import-scan", "node/import-scan"], ["dev-props-validator", "node/dev-props-validator"], // Browser-only ["scripts", "client-scripts"], ["counter-utils", "client-scripts/counter-utils"], // Merges into existing top-level dirs ["content-reference", "content/content-reference"], ["content", "content"], ["onboarding-yaml", "onboarding"], // Loose files grouped into new domains ["check-hints", "checks/check-hints"], ["runtime-context", "runtime/runtime-context"], ["shared-context", "runtime/shared-context"], ["rfc0042-utils", "sections/rfc0042-utils"], ["walk-files", "stack/walk-files"], ["run-tool", "stack/run-tool"], ["stack-checks", "stack/stack-checks"], ["attribution-display", "attribution/attribution-display"], ["material-credits", "attribution/material-credits"], ["offer-capacity", "offers/offer-capacity"], ["formula-eval", "offers/formula-eval"], ["string-utils", "text/string-utils"], ["text-normalize", "text/text-normalize"], ["text-position", "text/text-position"], ["wrap-inline-numbers", "text/wrap-inline-numbers"], ["css-value-normalize", "text/css-value-normalize"], ["entitlement", "policy/entitlement"], ["feature-policy", "policy/feature-policy"], ["visibility", "policy/visibility"], ["page", "page"], ["redirects", "redirects"], ["image-provider", "image-provider"], ]; /** Domain dirs promoted mechanically: `share/` → ``. */ const PROMOTED_DIRS = new Set([ "agent", "content-discipline", "i18n", "knowledge", "legal", "middleware", "pbt", "remediation", "routes", "schemas", "semantic", "slug", "typography", "types", ]); /** * Maps a `share/…` subpath (without the leading `share/`) to its new subpath, * or null when no mechanical target exists (bare barrel, tests, configs). */ export function mapShareSubpath(subpath: string): string | null { if (subpath === "" || subpath === "index") return null; // Test files, configs and type-declaration helpers are not exported post-move. if ( subpath.startsWith("tests/") || subpath.endsWith(".test") || subpath.endsWith(".pbt.test") || subpath.endsWith(".snapshot.test") || subpath === "vitest.config" || subpath === "env.d" ) { return null; } for (const [from, to] of EXPLICIT_MAP) { if (subpath === from) return to; if (subpath.startsWith(`${from}/`)) return to + subpath.slice(from.length); } const first = subpath.split("/")[0]; if (first !== undefined && PROMOTED_DIRS.has(first)) { // Normalize trailing `/index` to the canonical barrel subpath. return subpath.endsWith("/index") ? subpath.slice(0, -"/index".length) : subpath; } return null; } async function rewriteFile( filePath: string, ctx: MigrationContext, io: WorkspaceIO = createDefaultIO().io, ): Promise { let content: string; try { content = await io.readFile(filePath); } catch { return false; } if (!SPECIFIER_DETECT_RE.test(content)) return false; let unresolved = 0; const rewritten = content.replace(SPECIFIER_RE, (match, sub: string | undefined) => { const subpath = (sub ?? "").replace(/^\//, ""); const mapped = mapShareSubpath(subpath); if (mapped === null) { unresolved += 1; return match; } return `@warpgogol/werkstatt-shared/${mapped}`; }); if (unresolved > 0) { ctx.logger.info( `[rfc-1105] WARNING ${filePath}: ${unresolved} share specifier(s) have no mechanical target ` + `(bare barrel, tests, or configs) — repair manually`, ); } if (rewritten === content) return false; await io.writeFile(filePath, rewritten); return true; } export const rfc1105Migrator: Migrator = { id: RFC_1105_MIGRATOR_ID, fromVersion: "6.230.0", toVersion: "6.231.0", description: "Rewrite `@warpgogol/werkstatt-shared` specifiers carrying the dissolved `share/` segment to post-dissolution top-level subpaths in workpiece authored sources (RFC-1105 share namespace dissolution). Bare barrel and test/config specifiers are logged for manual repair.", transform: async ( data: SternsystemData, ctx: MigrationContext, io: WorkspaceIO = createDefaultIO().io, ) => { const files: string[] = await collectFiles(path.join(data.rootPath, "src"), { extensions: [...SCAN_EXTENSIONS], ignore: (name) => SKIP_DIRS.has(name) || name.startsWith("-") || name.startsWith("old-"), }); // Root-level config files can also carry specifiers (astro.config.mjs, etc.). let rootEntries: DirEntry[] = []; try { rootEntries = await io.readdir(data.rootPath); } catch { rootEntries = []; } for (const entry of rootEntries) { if (entry.isFile && SCAN_EXTENSIONS.has(path.extname(entry.name))) { files.push(path.join(data.rootPath, entry.name)); } } let rewritten = 0; for (const file of files) { if (await rewriteFile(file, ctx)) rewritten += 1; } ctx.logger.info( `[rfc-1105] share-path-rewrite: ${rewritten} file(s) rewritten, ${files.length} scanned`, ); return data; }, };