108 lines
3.5 KiB
TypeScript
108 lines
3.5 KiB
TypeScript
#!/usr/bin/env bun
|
|
|
|
import fs from "node:fs"
|
|
import path from "node:path"
|
|
|
|
const ROOT = path.resolve(import.meta.dir, "..")
|
|
const MANIFEST_PATH = path.join(ROOT, "core", "CORE-MANIFEST.json")
|
|
const CHECK = process.argv.includes("--check")
|
|
|
|
const REF_RE = /\B#(\d{3,5})\b/g
|
|
const PLACEHOLDER = "org-internal"
|
|
|
|
interface Mapping {
|
|
core: string
|
|
dogfood: string | null
|
|
sync: string
|
|
}
|
|
|
|
function fail(message: string): never {
|
|
console.error(`delink-core: ${message}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
if (!fs.existsSync(MANIFEST_PATH)) fail(`manifest not found: ${MANIFEST_PATH}`)
|
|
let mappings: Mapping[]
|
|
try {
|
|
mappings = (JSON.parse(fs.readFileSync(MANIFEST_PATH, "utf8")) as { mappings: Mapping[] }).mappings
|
|
} catch (e) {
|
|
fail(`manifest is not valid JSON: ${(e as Error).message}`)
|
|
}
|
|
|
|
function listTextFiles(absDir: string, base = ""): string[] {
|
|
const out: string[] = []
|
|
for (const entry of fs.readdirSync(absDir, { withFileTypes: true })) {
|
|
const rel = base ? `${base}/${entry.name}` : entry.name
|
|
if (entry.isDirectory()) out.push(...listTextFiles(path.join(absDir, entry.name), rel))
|
|
else if (/\.(md|yaml|json)$/.test(entry.name)) out.push(rel)
|
|
}
|
|
return out
|
|
}
|
|
|
|
const byteLocked = new Set<string>()
|
|
for (const m of mappings) {
|
|
if (m.sync !== "verbatim" && m.sync !== "verbatimDir") continue
|
|
if (m.sync === "verbatimDir") {
|
|
const dir = m.core.endsWith("/") ? m.core : `${m.core}/`
|
|
for (const f of listTextFiles(path.join(ROOT, dir))) byteLocked.add(`${dir}${f}`)
|
|
} else if (/\.(md|yaml|json)$/.test(m.core)) {
|
|
byteLocked.add(m.core)
|
|
}
|
|
}
|
|
|
|
function transformContent(content: string): { next: string; count: number } {
|
|
let count = 0
|
|
const next = content.replace(REF_RE, (match, _digits: string, offset: number, whole: string) => {
|
|
const before = whole.slice(Math.max(0, offset - 14), offset)
|
|
if (before.endsWith(`[${PLACEHOLDER} `)) return match
|
|
count++
|
|
return `[${PLACEHOLDER} #${_digits}]`
|
|
})
|
|
return { next, count }
|
|
}
|
|
|
|
export function transformContentForTest(content: string): { next: string; count: number } {
|
|
return transformContent(content)
|
|
}
|
|
|
|
const files = listTextFiles(path.join(ROOT, "core"))
|
|
const perFile: { file: string; count: number }[] = []
|
|
const deferredLocked: { file: string; count: number }[] = []
|
|
let total = 0
|
|
let replacedTotal = 0
|
|
for (const rel of files) {
|
|
const fullRel = `core/${rel}`
|
|
const content = fs.readFileSync(path.join(ROOT, "core", rel), "utf8")
|
|
const { next, count } = transformContent(content)
|
|
if (count === 0) continue
|
|
if (byteLocked.has(fullRel)) {
|
|
deferredLocked.push({ file: fullRel, count })
|
|
total += count
|
|
continue
|
|
}
|
|
perFile.push({ file: fullRel, count })
|
|
total += count
|
|
if (!CHECK) {
|
|
fs.writeFileSync(path.join(ROOT, "core", rel), next)
|
|
replacedTotal += count
|
|
}
|
|
}
|
|
|
|
if (CHECK) {
|
|
if (perFile.length > 0) {
|
|
console.error(`delink-core: ${perFile.length} file(s) still carry replaceable refs:`)
|
|
for (const p of perFile) console.error(` ${p.file}: ${p.count}`)
|
|
process.exit(1)
|
|
}
|
|
const lockedRefs = deferredLocked.reduce((a, b) => a + b.count, 0)
|
|
console.log(
|
|
`delink-core: check ok — 0 replaceable refs; ${deferredLocked.length} byte-locked verbatim mirror(s) carrying ${lockedRefs} refs deferred to dogfood-side delink (Inc 6b)`,
|
|
)
|
|
process.exit(0)
|
|
}
|
|
|
|
console.log(`delink-core: replaced ${replacedTotal} refs across ${perFile.length} file(s)`)
|
|
const top = [...perFile, ...deferredLocked].sort((a, b) => b.count - a.count).slice(0, 10)
|
|
for (const p of top) console.log(` ${p.file}: ${p.count}`)
|
|
process.exit(0)
|