import { createHash } from "node:crypto"; import type { Binding, Conformance, DependencyBinding, PersistentAttachment, WorkspaceRevision } from "./types.js"; import { validateWorkspaceRevision } from "./validation.js"; /** Content hashing is independent of JSON object insertion order, not array order. */ const compareText = (a: string, b: string) => a < b ? -1 : a > b ? 1 : 0; export const canonicalJson = (value: unknown): string => { if (value === null || typeof value === "string" || typeof value === "boolean") return JSON.stringify(value); if (typeof value === "number" && Number.isFinite(value)) return JSON.stringify(value); if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; if (typeof value === "object" && value !== null) { if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new Error("Expected a plain JSON object"); return `{${Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([a], [b]) => compareText(a, b)) .map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`; } throw new Error(`Cannot hash non-JSON value: ${typeof value}`); }; export const contentDigest = (value: unknown) => `sha256:${createHash("sha256").update(canonicalJson(value)).digest("hex")}`; const semantic = (value: unknown): unknown => { if (Array.isArray(value)) return value.map(semantic); if (value && typeof value === "object") return Object.fromEntries(Object.entries(value) .filter(([key, entry]) => entry !== undefined && key !== "displayName" && key !== "documentation") .map(([key, entry]) => [key, semantic(entry)])); return value; }; const sorted = (entries: readonly T[], key: (entry: T) => string) => [...entries].sort((a, b) => compareText(key(a), key(b))); export const conformanceIdentity = (entry: Conformance): string => entry.id ?? `legacy:${entry.atomId}:${entry.interfaceRevisionId}`; export type StorageContract = { id: string; ownerId: string; kind: "state" | "edge"; digest: string; definition: unknown }; export const storageContracts = (workspace: WorkspaceRevision): StorageContract[] => { const result: StorageContract[] = []; const add = (attachment: PersistentAttachment, ownerId: string) => { const definition = semantic(attachment); result.push({ id: attachment.id, ownerId, kind: attachment.kind, definition, digest: contentDigest({ ownerId, definition }) }); }; for (const attachment of workspace.sharedAttachments) add(attachment, "legacy:workspace"); for (const conformance of workspace.conformances) for (const attachment of conformance.privateAttachments) add(attachment, conformanceIdentity(conformance)); return sorted(result, (entry) => entry.id); }; type GraphNode = { value: unknown; dependencies: Set; reviewProviders: Set }; export type RuntimeContract = { groupId: string; packageId: string; packageRevisionId: string; digest: string; reviewProviders: string[]; dependencies: Array<{ id: string; digest: string }> }; /** Build only outbound execution dependencies. Incoming callers never retain or invalidate a provider. */ export const runtimeContracts = (workspace: WorkspaceRevision): RuntimeContract[] => { const nodes = new Map(); const node = (key: string, value: unknown) => { const result = { value: semantic(value), dependencies: new Set(), reviewProviders: new Set() }; nodes.set(key, result); return result; }; const conformanceKey = (atom: string, iface: string) => `conformance:${atom}:${iface}`; for (const storage of storageContracts(workspace)) node(`attachment:${storage.id}`, storage); for (const iface of workspace.interfaceImports) node(`interface:${iface.revisionId}`, { ...iface, members: sorted(iface.members, (entry) => entry.id).map((entry) => ({ ...entry, operations: sorted(entry.operations, (operation) => operation.id) })), }); for (const pkg of workspace.packageImports) node(`package:${pkg.revisionId}`, { ...pkg, semanticMajor: pkg.semanticMajor ?? 1, exports: sorted(pkg.exports, (entry) => entry.id).map((entry) => ({ ...entry, dependencyPorts: sorted(entry.dependencyPorts, (port) => port.id) })), }); const dependency = (parent: GraphNode, binding: DependencyBinding, atomId: string, reviews?: Set) => { if (binding.kind === "state") parent.dependencies.add(`attachment:${binding.slotId}`); if (binding.kind === "edge") parent.dependencies.add(`attachment:${binding.edgeTypeId}`); if (binding.kind === "constructor") { parent.dependencies.add(`constructor:${binding.atomId}`); const ctor = workspace.constructors.find((entry) => entry.atomId === binding.atomId); const pkg = workspace.packageImports.find((entry) => entry.revisionId === ctor?.packageRevisionId); if (pkg) reviews?.add(pkg.packageId); } if (binding.kind !== "constructor" && binding.via) parent.dependencies.add(`attachment:${binding.via.edgeTypeId}`); if (binding.kind === "interface") { parent.dependencies.add(`interface:${binding.interfaceRevisionId}`); // An edge traversal may select any matching target. Conservatively include every possible witness. for (const conformance of workspace.conformances) { if (conformance.interfaceRevisionId === binding.interfaceRevisionId && (binding.via || conformance.atomId === atomId)) { parent.dependencies.add(conformanceKey(conformance.atomId, conformance.interfaceRevisionId)); reviews?.add(conformanceIdentity(conformance)); for (const operation of conformance.operationBindings) { if (operation.binding.kind !== "package") continue; const revisionId = operation.binding.packageRevisionId; const pkg = workspace.packageImports.find((entry) => entry.revisionId === revisionId); if (pkg) reviews?.add(pkg.packageId); } } } } }; const binding = (parent: GraphNode, value: Binding, atomId: string, context: unknown) => { if (value.kind !== "package") { dependency(parent, value, atomId); return; } const packageNode = nodes.get(`package:${value.packageRevisionId}`)!; parent.dependencies.add(`package:${value.packageRevisionId}`); const normalized = { ...value, dependencies: sorted(value.dependencies, (entry) => entry.portId) }; const key = `binding:${contentDigest({ atomId, context, value: semantic(normalized) })}`; const bound = node(key, { atomId, context, binding: normalized }); packageNode.dependencies.add(key); for (const port of value.dependencies) dependency(bound, port.binding, atomId, packageNode.reviewProviders); }; for (const conformance of workspace.conformances) { const parent = node(conformanceKey(conformance.atomId, conformance.interfaceRevisionId), { id: conformanceIdentity(conformance), semanticMajor: conformance.semanticMajor ?? 1, atomId: conformance.atomId, interfaceRevisionId: conformance.interfaceRevisionId, operations: sorted(conformance.operationBindings, (entry) => entry.operationId), materializations: sorted(conformance.relationshipMaterializations, (entry) => entry.memberId), }); parent.dependencies.add(`interface:${conformance.interfaceRevisionId}`); for (const attachment of conformance.privateAttachments) parent.dependencies.add(`attachment:${attachment.id}`); for (const operation of conformance.operationBindings) binding(parent, operation.binding, conformance.atomId, { conformanceId: conformanceIdentity(conformance), semanticMajor: conformance.semanticMajor ?? 1, operationId: operation.operationId, }); for (const materialization of conformance.relationshipMaterializations) { parent.dependencies.add(`constructor:${materialization.constructorAtomId}`); parent.dependencies.add(`attachment:${materialization.edgeTypeId}`); } } for (const constructor of workspace.constructors) { const parent = node(`constructor:${constructor.atomId}`, constructor); binding(parent, { kind: "package", ...constructor }, constructor.atomId, { constructor: constructor.atomId }); } const counts = new Map(); for (const pkg of workspace.packageImports) counts.set(pkg.packageId, (counts.get(pkg.packageId) ?? 0) + 1); return sorted(workspace.packageImports.map((pkg): RuntimeContract => { const visited = new Set(); const walk = (key: string) => { if (visited.has(key)) return; const entry = nodes.get(key); if (!entry) throw new Error(`Unresolved execution dependency ${key}`); visited.add(key); for (const target of entry.dependencies) walk(target); }; walk(`package:${pkg.revisionId}`); const dependencies = [...visited].sort().map((id) => ({ id, digest: contentDigest(nodes.get(id)!.value) })); return { groupId: counts.get(pkg.packageId) === 1 ? pkg.packageId : `${pkg.packageId}#${pkg.revisionId}`, packageId: pkg.packageId, packageRevisionId: pkg.revisionId, digest: contentDigest(dependencies), reviewProviders: [...nodes.get(`package:${pkg.revisionId}`)!.reviewProviders].sort(), dependencies }; }), (entry) => entry.groupId); }; export type EvolutionReview = { requirementDigest: string; decision: "changed" | "accepted-unchanged"; rationale: string; agentId: string }; export type ReviewRequirement = { consumerId: string; providerId: string; oldMajor: number; newMajor: number; requirementDigest: string }; export type RuntimeAction = { groupId: string; action: "keep" | "start" | "replace" | "retire"; previous?: RuntimeContract; candidate?: RuntimeContract; reasons: string[] }; export type EvolutionReport = { schemaVersion: 1; baselineDigest: string | null; candidateDigest: string; checkerVersion: string; runtimeActions: RuntimeAction[]; storageChanges: Array<{ id: string; kind: "add" | "remove" | "change"; previous?: StorageContract; candidate?: StorageContract }>; reviews: Array; packageChecks: Array<{ groupId: string; contractDigest: string }>; blockers: string[]; }; export const planEvolution = (baseline: WorkspaceRevision | null, candidate: WorkspaceRevision, options: { reviews?: EvolutionReview[]; allowLegacy?: boolean } = {}): EvolutionReport => { const issues = validateWorkspaceRevision(candidate); if (issues.length) throw new Error(`Invalid candidate workspace:\n${issues.map((entry) => `${entry.path}: ${entry.message}`).join("\n")}`); if (baseline && baseline.workspaceId !== candidate.workspaceId) throw new Error("Cannot evolve a different workspace"); const candidateDigest = contentDigest(candidate); const checkerVersion = "quixos-evolution-v1"; const blockers: string[] = []; if (!options.allowLegacy) { if (candidate.sharedAttachments.length) blockers.push("Assign legacy workspace-shared attachments to explicit conformance owners"); for (const entry of candidate.conformances) if (!entry.id) blockers.push(`Conformance ${entry.atomId} as ${entry.interfaceRevisionId} requires an authored ID`); } const previousRuntimes = new Map((baseline ? runtimeContracts(baseline) : []).map((entry) => [entry.groupId, entry])); const nextRuntimes = new Map(runtimeContracts(candidate).map((entry) => [entry.groupId, entry])); const runtimeActions: RuntimeAction[] = [...new Set([...previousRuntimes.keys(), ...nextRuntimes.keys()])].sort().map((groupId) => { const previous = previousRuntimes.get(groupId), next = nextRuntimes.get(groupId); const before = new Map(previous?.dependencies.map((entry) => [entry.id, entry.digest])); const after = new Map(next?.dependencies.map((entry) => [entry.id, entry.digest])); const reasons = [...new Set([...before.keys(), ...after.keys()])].sort().filter((id) => before.get(id) !== after.get(id)); return { groupId, action: !previous ? "start" : !next ? "retire" : previous.digest === next.digest ? "keep" : "replace", ...(previous ? { previous } : {}), ...(next ? { candidate: next } : {}), reasons }; }); const beforeStorage = new Map((baseline ? storageContracts(baseline) : []).map((entry) => [entry.id, entry])); const afterStorage = new Map(storageContracts(candidate).map((entry) => [entry.id, entry])); const storageChanges: EvolutionReport["storageChanges"] = []; for (const id of [...new Set([...beforeStorage.keys(), ...afterStorage.keys()])].sort()) { const previous = beforeStorage.get(id), next = afterStorage.get(id); if (previous?.digest !== next?.digest) storageChanges.push({ id, kind: !previous ? "add" : !next ? "remove" : "change", ...(previous ? { previous } : {}), ...(next ? { candidate: next } : {}) }); } const providers = (workspace: WorkspaceRevision) => [ ...workspace.packageImports.map((entry) => ({ id: entry.packageId as string, revision: entry.revisionId as string, major: entry.semanticMajor ?? 1, node: `package:${entry.revisionId}`, digest: contentDigest(entry) })), ...workspace.conformances.map((entry) => ({ id: conformanceIdentity(entry), revision: contentDigest(entry), major: entry.semanticMajor ?? 1, node: `conformance:${entry.atomId}:${entry.interfaceRevisionId}`, digest: contentDigest(entry) })), ]; const oldProviders = baseline ? providers(baseline) : []; const reviews: EvolutionReport["reviews"] = []; for (const provider of providers(candidate)) { const old = oldProviders.filter((entry) => entry.id === provider.id); if (old.length > 1) { blockers.push(`Ambiguous semantic-major lineage for ${provider.id}`); continue; } if (!old[0] || old[0].major === provider.major) continue; if (provider.major < old[0].major) blockers.push(`Semantic major decreases for ${provider.id}`); for (const consumer of nextRuntimes.values()) { if (consumer.packageId === provider.id || !consumer.reviewProviders.includes(provider.id)) continue; const requirement = { consumerId: consumer.groupId, providerId: provider.id, oldMajor: old[0].major, newMajor: provider.major }; const requirementDigest = contentDigest({ ...requirement, oldProvider: old[0].digest, newProvider: provider.digest, consumer: consumer.digest, checkerVersion }); const accepted = (options.reviews ?? []).some((entry) => entry.requirementDigest === requirementDigest && ["changed", "accepted-unchanged"].includes(entry.decision) && entry.rationale.trim() && entry.agentId.trim()); reviews.push({ ...requirement, requirementDigest, accepted }); if (!accepted) blockers.push(`Semantic-major review required: ${consumer.groupId} consumes ${provider.id}`); } } return { schemaVersion: 1, baselineDigest: baseline ? contentDigest(baseline) : null, candidateDigest, checkerVersion, runtimeActions, storageChanges, reviews, packageChecks: runtimeActions.filter((entry) => entry.candidate && entry.action !== "keep") .map((entry) => ({ groupId: entry.groupId, contractDigest: entry.candidate!.digest })), blockers }; };