[verified] feat: add semantic diagram lifecycle
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { readFileSync } from "node:fs";
|
||||
import * as yaml from "js-yaml";
|
||||
|
||||
import { diagramIRToDrawio } from "../../authoring/ir-to-drawio.js";
|
||||
import { normalizeDiagramIR, validateDiagramIR } from "../../model/diagram-ir.js";
|
||||
import { atomicWrite } from "../../services/semantic-lifecycle/lifecycle-io.js";
|
||||
|
||||
export function run(filePath: string, _pageIndex = 0, outputPath?: string): Record<string, unknown> {
|
||||
if (!outputPath) throw new Error("build requires --output <path.drawio>");
|
||||
@@ -11,8 +11,7 @@ export function run(filePath: string, _pageIndex = 0, outputPath?: string): Reco
|
||||
const ir = validateDiagramIR(parsed);
|
||||
const normalized = normalizeDiagramIR(ir);
|
||||
const xml = diagramIRToDrawio(normalized);
|
||||
const resolvedOutput = resolve(outputPath);
|
||||
writeFileSync(resolvedOutput, xml, "utf8");
|
||||
const resolvedOutput = atomicWrite(outputPath, xml, [filePath]);
|
||||
return {
|
||||
action: "build",
|
||||
file: filePath,
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
import { doctorReport } from "../../services/semantic-lifecycle/publishing.js";
|
||||
export function run(_filePath = "", _page = 0, _output?: string): Record<string, unknown> { return { action: "doctor", ...doctorReport() }; }
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { EditBatch } from "../../services/semantic-lifecycle/edit-batch.js";
|
||||
import { applyEditBatch } from "../../services/semantic-lifecycle/edit-batch.js";
|
||||
import { assertOutputSafe, atomicWrite, loadIR, loadStructured, structuredText, type LifecycleActionOptions } from "../../services/semantic-lifecycle/lifecycle-io.js";
|
||||
import { diagramIRToDrawio } from "../../authoring/ir-to-drawio.js";
|
||||
export function run(filePath: string, _page = 0, outputPath?: string, options: LifecycleActionOptions = {}): Record<string, unknown> {
|
||||
if (!options.spec) throw new Error("edit requires --spec <batch.yaml>");
|
||||
if (!options.dryRun && !outputPath) throw new Error("edit requires --output unless --dry-run is used");
|
||||
if (outputPath) assertOutputSafe(outputPath, [filePath, options.spec]);
|
||||
const source = loadIR(filePath); const batch = loadStructured(options.spec) as EditBatch;
|
||||
const result = applyEditBatch(source, batch, { dryRun: options.dryRun });
|
||||
let output: string | undefined;
|
||||
if (!options.dryRun && outputPath) {
|
||||
const content = /\.(drawio|xml)$/i.test(outputPath) ? diagramIRToDrawio(result.ir) : structuredText(result.ir, outputPath);
|
||||
output = atomicWrite(outputPath, content, [filePath, options.spec]);
|
||||
}
|
||||
return { action: "edit", output, applied: result.applied, dryRun: result.dryRun, preview: options.dryRun ? result.preview : undefined };
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { importDrawioToIR } from "../../services/semantic-lifecycle/import-drawio.js";
|
||||
import { atomicWrite, structuredText } from "../../services/semantic-lifecycle/lifecycle-io.js";
|
||||
export function run(filePath: string, _page = 0, outputPath?: string): Record<string, unknown> {
|
||||
if (!outputPath) throw new Error("import requires --output <model.yaml|model.json>");
|
||||
if (!/\.(?:ya?ml|json)$/i.test(outputPath)) throw new Error("import output must use a .yaml, .yml, or .json extension");
|
||||
const result = importDrawioToIR(filePath);
|
||||
const output = atomicWrite(outputPath, structuredText(result.ir, outputPath), [filePath]);
|
||||
return { action: "import", file: filePath, output, lossReport: result.lossReport };
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { queryDiagram } from "../../services/semantic-lifecycle/analysis.js";
|
||||
import { loadIR, type LifecycleActionOptions } from "../../services/semantic-lifecycle/lifecycle-io.js";
|
||||
export function run(filePath: string, _page = 0, _output?: string, options: LifecycleActionOptions = {}): Record<string, unknown> {
|
||||
const properties = Object.fromEntries((options.property ?? []).map((entry) => { const at = entry.indexOf("="); if (at < 1) throw new Error(`--property requires key=value: ${entry}`); const raw = entry.slice(at + 1); let value: unknown = raw; try { value = JSON.parse(raw); } catch {} return [entry.slice(0, at), value]; }));
|
||||
return { action: "query", ...queryDiagram(loadIR(filePath), { kind: options.kind, properties, from: options.from, to: options.to }) };
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { createStoryHtml } from "../../services/semantic-lifecycle/publishing.js";
|
||||
import { atomicWrite, loadIR, type LifecycleActionOptions } from "../../services/semantic-lifecycle/lifecycle-io.js";
|
||||
import { simulateFailure } from "../../services/semantic-lifecycle/analysis.js";
|
||||
export function run(filePath: string, _page = 0, outputPath?: string, options: LifecycleActionOptions = {}): Record<string, unknown> { if (!outputPath) throw new Error("story requires --output <story.html>"); const ir = loadIR(filePath); const scenario = options.fail ? simulateFailure(ir, options.fail) : undefined; const output = atomicWrite(outputPath, createStoryHtml(ir, { scenario }), [filePath]); return { action: "story", output, accessible: true, offline: true, scenario: scenario ? { failed: scenario.failed, impacted: scenario.impacted } : undefined }; }
|
||||
@@ -0,0 +1,10 @@
|
||||
import { diagramIRToDrawio } from "../../authoring/ir-to-drawio.js";
|
||||
import { assertOutputSafe, atomicWrite, loadIR, structuredText, type LifecycleActionOptions } from "../../services/semantic-lifecycle/lifecycle-io.js";
|
||||
import { syncDiagramIR } from "../../services/semantic-lifecycle/sync.js";
|
||||
export function run(filePath: string, _page = 0, outputPath?: string, options: LifecycleActionOptions = {}): Record<string, unknown> {
|
||||
if (!options.base || !options.spec) throw new Error("sync requires --base <base> and --spec <incoming>"); if (!options.dryRun && !outputPath) throw new Error("sync requires --output unless --dry-run is used");
|
||||
if (outputPath) assertOutputSafe(outputPath, [filePath, options.base, options.spec]);
|
||||
const result = syncDiagramIR(loadIR(options.base), loadIR(filePath), loadIR(options.spec), { prune: options.prune });
|
||||
let output: string | undefined; if (!options.dryRun && outputPath) output = atomicWrite(outputPath, /\.(drawio|xml)$/i.test(outputPath) ? diagramIRToDrawio(result.ir) : structuredText(result.ir, outputPath), [filePath, options.base, options.spec]);
|
||||
return { action: "sync", output, dryRun: options.dryRun === true, added: result.added, removed: result.removed, conflicts: result.conflicts, preview: options.dryRun ? result.ir : undefined };
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { POLICY_IDS, runPolicies } from "../../services/semantic-lifecycle/analysis.js";
|
||||
import { loadIR, loadStructured, type LifecycleActionOptions } from "../../services/semantic-lifecycle/lifecycle-io.js";
|
||||
export function run(filePath: string, _page = 0, _output?: string, options: LifecycleActionOptions = {}): Record<string, unknown> {
|
||||
let rules: readonly string[] = POLICY_IDS; if (options.spec) { const raw = loadStructured(options.spec); rules = Array.isArray(raw) ? raw.map(String) : ((raw as { rules?: unknown[] }).rules ?? []).map((item) => typeof item === "string" ? item : String((item as { id?: unknown }).id)); }
|
||||
const unknown = rules.filter((rule) => !(POLICY_IDS as readonly string[]).includes(rule));
|
||||
if (unknown.length) throw new Error(`Unknown policy identifier: ${unknown.join(", ")}`);
|
||||
const report = runPolicies(loadIR(filePath), rules); return { action: "test", ...report, failed: report.errors > 0 || (options.strict === true && report.warnings > 0) };
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { diagramIRToDrawio } from "../../authoring/ir-to-drawio.js";
|
||||
import type { DiagramIRV2 } from "../../model/diagram-ir.js";
|
||||
import { projectLinkedViews, type ViewName } from "../../services/semantic-lifecycle/analysis.js";
|
||||
import { atomicWrite, loadIR, type LifecycleActionOptions } from "../../services/semantic-lifecycle/lifecycle-io.js";
|
||||
export function run(filePath: string, _page = 0, outputPath?: string, options: LifecycleActionOptions = {}): Record<string, unknown> {
|
||||
if (!outputPath) throw new Error("views requires --output <views.drawio>");
|
||||
const source = loadIR(filePath);
|
||||
const names = options.views?.split(",").filter(Boolean) as ViewName[] | undefined;
|
||||
const allowed = new Set<ViewName>(["executive", "system", "deployment", "dataflow", "security"]);
|
||||
const unknown = names?.filter((name) => !allowed.has(name)) ?? [];
|
||||
if (unknown.length) throw new Error(`Unknown linked view: ${unknown.join(", ")}`);
|
||||
const views = projectLinkedViews(source, names);
|
||||
const ir: DiagramIRV2 = { version: 2, title: source.title, provenance: source.provenance, pages: views.map((view) => ({ id: view.id, title: view.title, nodes: view.nodes, edges: view.edges, layout: { type: "manual" }, properties: { linkedView: true, sourcePageIds: view.sourcePageIds, fallback: view.fallback, fallbackReason: view.fallbackReason, hint: view.hint } })) };
|
||||
const output = atomicWrite(outputPath, diagramIRToDrawio(ir), [filePath]);
|
||||
return { action: "views", output, views: views.map(({ id, fallback, fallbackReason, hint }) => ({ id, fallback, fallbackReason, hint })) };
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { simulateFailure } from "../../services/semantic-lifecycle/analysis.js";
|
||||
import { loadIR, type LifecycleActionOptions } from "../../services/semantic-lifecycle/lifecycle-io.js";
|
||||
export function run(filePath: string, _page = 0, _output?: string, options: LifecycleActionOptions = {}): Record<string, unknown> { if (!options.fail) throw new Error("what-if requires --fail <node-id>"); return { action: "what-if", ...simulateFailure(loadIR(filePath), options.fail) }; }
|
||||
Reference in New Issue
Block a user