export type ProductionKind = 'TechDraw' | 'Spreadsheet' | 'Draft' | 'Plot'
export type ProductionArtifact = { id: string; kind: ProductionKind; version: number; dependencies: string[]; payload: Record<string, unknown> }
export type ProductionClosure = { complete: boolean; missingKinds: ProductionKind[]; missingDependencies: string[]; cycles: string[][]; artifacts: number }
export type ProductionDocumentSnapshot = { id: string; label: string; artifacts: ProductionArtifact[]; version: number }
export type ProductionDocumentApi = { add(input: Omit<ProductionArtifact, 'version'> & { version?: number }): ProductionArtifact; update(id: string, patch: Partial<Pick<ProductionArtifact, 'dependencies' | 'payload'>>): ProductionArtifact; closure(): ProductionClosure; snapshot(): ProductionDocumentSnapshot; save(): string; load(serialized: string): ProductionDocumentSnapshot }
const kinds: ProductionKind[] = ['TechDraw', 'Spreadsheet', 'Draft', 'Plot']
const clone = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T
const validKind = (value: string): value is ProductionKind => kinds.includes(value as ProductionKind)
export const createProductionDocument = (id = 'document', label = 'Production document'): ProductionDocumentApi => { const artifacts = new Map<string, ProductionArtifact>(); let version = 0; const add = (input: Omit<ProductionArtifact, 'version'> & { version?: number }) => { if (!input.id.trim() || artifacts.has(input.id)) throw new RangeError(`Production artifact already exists: ${input.id}`); if (!validKind(input.kind)) throw new RangeError(`Unsupported production artifact kind: ${input.kind}`); const artifact: ProductionArtifact = { id: input.id, kind: input.kind, version: input.version ?? 1, dependencies: [...new Set(input.dependencies)], payload: clone(input.payload) }; if (!Number.isSafeInteger(artifact.version) || artifact.version < 1) throw new RangeError('Production artifact version must be positive.'); artifacts.set(artifact.id, artifact); version += 1; return clone(artifact) }; const update = (artifactId: string, patch: Partial<Pick<ProductionArtifact, 'dependencies' | 'payload'>>) => { const artifact = artifacts.get(artifactId); if (!artifact) throw new RangeError(`Production artifact does not exist: ${artifactId}`); if (patch.dependencies) artifact.dependencies = [...new Set(patch.dependencies)]; if (patch.payload) artifact.payload = clone(patch.payload); artifact.version += 1; version += 1; return clone(artifact) }; const closure = (): ProductionClosure => { const present = new Set([...artifacts.values()].map((artifact) => artifact.kind)); const missingKinds = kinds.filter((kind) => !present.has(kind)); const missingDependencies: string[] = []; for (const artifact of artifacts.values()) for (const dependency of artifact.dependencies) if (!artifacts.has(dependency)) missingDependencies.push(`${artifact.id}->${dependency}`); const cycles: string[][] = []; const visiting = new Set<string>(); const visited = new Set<string>(); const walk = (id: string, path: string[]) => { if (visiting.has(id)) { const start = path.indexOf(id); cycles.push(path.slice(start).concat(id)); return }; if (visited.has(id)) return; visiting.add(id); const artifact = artifacts.get(id); for (const dependency of artifact?.dependencies ?? []) if (artifacts.has(dependency)) walk(dependency, [...path, dependency]); visiting.delete(id); visited.add(id) }; for (const artifact of artifacts.values()) walk(artifact.id, [artifact.id]); return { complete: missingKinds.length === 0 && missingDependencies.length === 0 && cycles.length === 0, missingKinds, missingDependencies: [...new Set(missingDependencies)].sort(), cycles, artifacts: artifacts.size } }; const snapshot = (): ProductionDocumentSnapshot => ({ id, label, artifacts: [...artifacts.values()].map(clone), version }); const save = () => `${JSON.stringify(snapshot(), null, 2)}\n`; const load = (serialized: string) => { let parsed: unknown; try { parsed = JSON.parse(serialized) } catch { throw new Error('Production document is not valid JSON.') }; if (!parsed || typeof parsed !== 'object' || !Array.isArray((parsed as { artifacts?: unknown }).artifacts)) throw new Error('Production document artifacts are missing.'); const source = parsed as ProductionDocumentSnapshot; artifacts.clear(); for (const artifact of source.artifacts) add(artifact); version = source.version; return snapshot() }; return { add, update, closure, snapshot, save, load } }
