[verified] feat: add semantic diagram lifecycle

This commit is contained in:
2026-09-03 19:11:50 +00:00
parent 5146b14d43
commit db4129168b
37 changed files with 1626 additions and 58 deletions
@@ -0,0 +1,35 @@
import { lstatSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
import { basename, dirname, extname, resolve } from "node:path";
import * as yaml from "js-yaml";
import type { DiagramIRV2 } from "../../model/diagram-ir.js";
import { normalizeDiagramIR, validateDiagramIR } from "../../model/diagram-ir.js";
import { importDrawioToIR } from "./import-drawio.js";
export interface LifecycleActionOptions { spec?: string; base?: string; strict?: boolean; prune?: boolean; dryRun?: boolean; fail?: string; views?: string; kind?: string; property?: string[]; from?: string; to?: string }
export function loadStructured(path: string): unknown { return yaml.load(readFileSync(path, "utf8"), { schema: yaml.JSON_SCHEMA }); }
export function loadIR(path: string): DiagramIRV2 { return /\.(drawio|xml)$/i.test(path) ? importDrawioToIR(path).ir : normalizeDiagramIR(validateDiagramIR(loadStructured(path))); }
export function structuredText(value: unknown, path: string): string { return extname(path).toLowerCase() === ".json" ? `${JSON.stringify(value, null, 2)}\n` : yaml.dump(value, { noRefs: true, sortKeys: false, lineWidth: 120 }); }
function canonicalExisting(path: string): string {
const absolute = resolve(path);
try { return realpathSync(absolute); } catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return absolute;
throw error;
}
}
export function assertOutputSafe(path: string, inputs: string[] = []): string {
const output = resolve(path);
try { if (lstatSync(output).isSymbolicLink()) throw new Error("Refusing to overwrite a symbolic link"); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
const canonicalOutput = canonicalExisting(output);
if (inputs.some((input) => canonicalExisting(input) === canonicalOutput)) throw new Error("Refusing to overwrite an input path (including traversal aliases)");
return output;
}
export function atomicWrite(path: string, content: string, inputs: string[] = []): string {
const output = assertOutputSafe(path, inputs);
const temporary = resolve(dirname(output), `.${basename(output)}.${process.pid}.${Date.now()}.tmp`);
try { writeFileSync(temporary, content, { encoding: "utf8", flag: "wx" }); renameSync(temporary, output); } finally { rmSync(temporary, { force: true }); }
return output;
}