import { readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"; import path from "node:path"; import { parseAuthoredDocument, resolveGovernedPath, SystemDocumentError, validateBundleCandidate, } from "./documents.ts"; import type { ProjectIndex } from "./project-index.ts"; export type PlanStatus = "draft" | "approved"; const FRONTMATTER = /^(---\r?\n)([\s\S]*?)(\r?\n---(?:\r?\n|$))/; const STATUS_LINE = /^status:[^\r\n]*$/gm; export function planStatusSource(source: string, status: PlanStatus): string { const block = FRONTMATTER.exec(source); if (!block) throw new SystemDocumentError( "status.frontmatter", "Plan requires one leading YAML frontmatter block", ); const open = block[1] ?? ""; const body = block[2] ?? ""; const close = block[3] ?? ""; const matches = body.match(STATUS_LINE) ?? []; if (matches.length !== 1) throw new SystemDocumentError( "status.line", `Plan frontmatter requires exactly one status line; found ${matches.length}`, ); const updated = body.replace(STATUS_LINE, `status: ${status}`); return `${open}${updated}${close}${source.slice(block[0].length)}`; } export function writePlanStatus( index: ProjectIndex, id: string, status: PlanStatus, ): void { const document = index.documents.find( (candidate) => candidate.metadata.id === id, ); if (!document) throw new SystemDocumentError( "status.missing-item", `No System document: ${id}`, ); if (document.metadata.type !== "plan") throw new SystemDocumentError( "status.item-type", `Only plans carry a status: ${id}`, ); if (document.metadata.status === status) return; const target = resolveGovernedPath(index.root, document.path); const markdown = planStatusSource(readFileSync(target, "utf8"), status); const diagnostics = validateBundleCandidate( index.documents.map((candidate) => ({ path: candidate.path, markdown: candidate.markdown, })), { path: document.path, markdown }, index.prefix, ); if (diagnostics.length > 0) throw new SystemDocumentError( "status.invalid", `Plan would become invalid: ${diagnostics.map((item) => item.message).join("; ")}`, ); const written = parseAuthoredDocument(markdown, index.prefix).metadata; if (written.type !== "plan" || written.status !== status) throw new SystemDocumentError( "status.invalid", `Plan status did not change to ${status}: ${id}`, ); const temporary = path.join( path.dirname(target), `.status-${process.pid}-${Date.now()}.md`, ); writeFileSync(temporary, markdown, { encoding: "utf8", flag: "wx" }); try { renameSync(temporary, target); } finally { rmSync(temporary, { force: true }); } }