import { PatchValidationError } from "./errors.ts"; import { normalizePlanning, validatePlanningDependencies, validateTransitionPlanning } from "./planning.ts"; import { activeShellTransitionIds, liveRefinements, isNodeInsideRefinement, refinementChainForNode, refinementForParent, validateRefinements, } from "./refinement.ts"; import { canonicalJson, patchRequestHash, snapshotHash } from "./snapshot.ts"; import { redactText } from "../security/redact.ts"; import { MAIN_TOKEN_ID, type ApplyPatchResult, type Arc, type AttachEvidenceOp, type CommittedBatch, type DomainEvent, type GraphPatch, type GraphPatchOp, type Place, type Transition, type TransitionRefinement, type WorkflowState, } from "./types.ts"; function required(value: T, key: K): NonNullable { const candidate = value[key]; if (candidate === undefined || candidate === null || candidate === "") { throw new PatchValidationError(`${String(value["op" as K] ?? "operation")}: ${String(key)} is required`); } return candidate as NonNullable; } function getPlace(state: WorkflowState, id: string): Place { const place = state.places[id]; if (!place) throw new PatchValidationError(`Place not found: ${id}`); return place; } function getTransition(state: WorkflowState, id: string): Transition { const transition = state.transitions[id]; if (!transition) throw new PatchValidationError(`Transition not found: ${id}`); return transition; } function getRefinement(state: WorkflowState, id: string): TransitionRefinement { const refinement = state.refinements[id]; if (!refinement) throw new PatchValidationError(`Refinement not found: ${id}`); return refinement; } function inputArcs(state: WorkflowState, transitionId: string): Arc[] { return Object.values(state.arcs).filter((arc) => arc.toId === transitionId && (arc.kind === "flow" || arc.kind === "read")); } function outputArcs(state: WorkflowState, transitionId: string): Arc[] { return Object.values(state.arcs).filter( (arc) => arc.fromId === transitionId && (arc.kind === "success" || arc.kind === "failure"), ); } function flowInput(state: WorkflowState, transitionId: string): Arc { const flows = inputArcs(state, transitionId).filter((arc) => arc.kind === "flow"); if (flows.length !== 1) { throw new PatchValidationError(`Transition ${transitionId} must have exactly one flow input; found ${flows.length}`); } return flows[0]!; } function successOutput(state: WorkflowState, transitionId: string): Arc { const success = outputArcs(state, transitionId).filter((arc) => arc.kind === "success"); if (success.length !== 1) { throw new PatchValidationError(`Transition ${transitionId} must have exactly one success output; found ${success.length}`); } return success[0]!; } function failureOutput(state: WorkflowState, transitionId: string): Arc | undefined { const failure = outputArcs(state, transitionId).filter((arc) => arc.kind === "failure"); if (failure.length > 1) { throw new PatchValidationError(`Transition ${transitionId} may have at most one failure output; found ${failure.length}`); } return failure[0]; } function ensureEvidenceExists( state: WorkflowState, evidenceIds: string[] | undefined, options: { required?: boolean } = {}, ): void { if (options.required && (!evidenceIds || evidenceIds.length === 0)) { throw new PatchValidationError("At least one evidenceId is required"); } for (const evidenceId of evidenceIds ?? []) { if (!state.evidence[evidenceId]) throw new PatchValidationError(`Evidence not found: ${evidenceId}`); } } function attachEvidenceRefs(target: Place | Transition, evidenceIds: string[] | undefined): void { for (const evidenceId of evidenceIds ?? []) { if (!target.evidenceIds.includes(evidenceId)) target.evidenceIds.push(evidenceId); } } function validateLeafContract(transition: Transition): void { const missing: string[] = []; if (!transition.contract.whyNow) missing.push("whyNow"); if (transition.contract.expectedEvidence.length === 0) missing.push("expectedEvidence"); if (!transition.contract.exitCondition) missing.push("exitCondition"); if (!transition.contract.failureCondition) missing.push("failureCondition"); if (missing.length > 0) { throw new PatchValidationError(`Transition ${transition.id} is not actionable; missing ${missing.join(", ")}`); } } function validateShellContract(transition: Transition): void { const missing: string[] = []; if (!transition.contract.scope) missing.push("scope"); if (!transition.contract.exitCondition) missing.push("exitCondition"); if (missing.length > 0) { throw new PatchValidationError(`Shell transition ${transition.id} is incomplete; missing ${missing.join(", ")}`); } } function readConditionsSatisfied(state: WorkflowState, transitionId: string): void { for (const arc of inputArcs(state, transitionId).filter((candidate) => candidate.kind === "read")) { const place = getPlace(state, arc.fromId); const satisfied = place.type === "Gate" ? place.gateState === "open" : place.state === "satisfied"; if (!satisfied) throw new PatchValidationError(`Read condition is not satisfied: ${place.id}`); } } function validateGraph(state: WorkflowState): void { if (!state.places[state.token.placeId]) { throw new PatchValidationError(`Token ${state.token.id} points to missing place ${state.token.placeId}`); } validateRefinements(state); const shellIds = new Set(activeShellTransitionIds(state)); const leafId = state.reservation?.transitionId; if (state.reservation) { if (state.reservation.tokenId !== state.token.id || state.token.id !== MAIN_TOKEN_ID) { throw new PatchValidationError("Reservation tokenId must match the single execution token"); } const leaf = state.transitions[state.reservation.transitionId]; if (!leaf || leaf.status !== "active") { throw new PatchValidationError("Reservation must belong to the active leaf transition"); } const leafInput = flowInput(state, leaf.id); if (state.reservation.inputPlaceId !== leafInput.fromId) { throw new PatchValidationError(`Reservation input does not match active leaf ${leaf.id}`); } if (state.token.placeId !== state.reservation.inputPlaceId) { throw new PatchValidationError("Reserved token must remain marked at the active leaf input Place"); } if (state.token.status !== "active") { throw new PatchValidationError("A reserved execution token must be active"); } if (refinementForParent(state, leaf.id)) { throw new PatchValidationError(`Refined shell ${leaf.id} cannot hold the executable reservation`); } if (leaf.planning.commitment !== "committed") { throw new PatchValidationError(`Active leaf ${leaf.id} must be committed`); } } for (const transition of Object.values(state.transitions)) { validateTransitionPlanning(state, transition); const inputs = inputArcs(state, transition.id); const outputs = outputArcs(state, transition.id); const flows = inputs.filter((arc) => arc.kind === "flow"); const success = outputs.filter((arc) => arc.kind === "success"); const failure = outputs.filter((arc) => arc.kind === "failure"); if (flows.length > 1) throw new PatchValidationError(`Transition ${transition.id} has multiple flow inputs`); if (success.length > 1) throw new PatchValidationError(`Transition ${transition.id} has multiple success outputs`); if (failure.length > 1) throw new PatchValidationError(`Transition ${transition.id} has multiple failure outputs`); if (transition.status === "active") { flowInput(state, transition.id); successOutput(state, transition.id); if (transition.id === leafId) validateLeafContract(transition); else if (shellIds.has(transition.id)) validateShellContract(transition); else throw new PatchValidationError(`Active transition ${transition.id} is neither an ancestor shell nor the leaf`); } } for (const arc of Object.values(state.arcs)) { const fromPlace = state.places[arc.fromId]; const fromTransition = state.transitions[arc.fromId]; const toPlace = state.places[arc.toId]; const toTransition = state.transitions[arc.toId]; if (arc.kind === "flow" || arc.kind === "read") { if (!fromPlace || !toTransition || fromTransition || toPlace) { throw new PatchValidationError(`Arc ${arc.id} must connect Place → Transition`); } } else if (!fromTransition || !toPlace || fromPlace || toTransition) { throw new PatchValidationError(`Arc ${arc.id} must connect Transition → Place`); } } } export function validateWorkflowState(state: WorkflowState): void { validateGraph(state); } function event( kind: string, opIndex: number, eventIndex: number, payload: Record, ): DomainEvent { return { kind, opIndex, eventIndex, payload }; } function pushEvent( events: DomainEvent[], kind: string, opIndex: number, payload: Record, ): void { events.push(event(kind, opIndex, events.length, payload)); } function activationChain(state: WorkflowState, transitionId: string): TransitionRefinement[] { return refinementChainForNode(state, transitionId); } function activateLeaf(state: WorkflowState, transitionId: string, tokenId: string, opIndex: number): DomainEvent[] { if (tokenId !== MAIN_TOKEN_ID || state.token.id !== tokenId) throw new PatchValidationError(`Unknown token: ${tokenId}`); if (state.reservation) throw new PatchValidationError(`Token is already reserved by ${state.reservation.transitionId}`); const leaf = getTransition(state, transitionId); if (leaf.status !== "planned") throw new PatchValidationError(`Transition ${transitionId} is ${leaf.status}`); if (refinementForParent(state, transitionId)) { throw new PatchValidationError(`Transition ${transitionId} is a refinement shell; activate a leaf inside its subnet`); } if (leaf.planning.commitment !== "committed") { throw new PatchValidationError(`Transition ${transitionId} must be committed before activation`); } if (leaf.planning.requiresRefinement) { throw new PatchValidationError(`Directional transition ${transitionId} requires refinement and cannot activate as a leaf`); } const events: DomainEvent[] = []; const chain = activationChain(state, transitionId); for (const [depth, refinement] of chain.entries()) { const shell = getTransition(state, refinement.parentTransitionId); if (refinement.status === "active") { if (shell.status !== "active") { throw new PatchValidationError(`Active refinement ${refinement.id} has inactive shell ${shell.id}`); } continue; } if (refinement.status !== "draft" || (shell.status !== "planned" && shell.status !== "active")) { throw new PatchValidationError(`Refinement ${refinement.id} cannot be entered from ${refinement.status}`); } if (shell.planning.commitment !== "committed") { throw new PatchValidationError(`Shell ${shell.id} must be committed before entry`); } validatePlanningDependencies(state, shell); validateShellContract(shell); const shellInput = flowInput(state, shell.id); successOutput(state, shell.id); failureOutput(state, shell.id); readConditionsSatisfied(state, shell.id); const enteringNewShell = shell.status === "planned"; if (enteringNewShell && state.token.placeId !== shellInput.fromId) { throw new PatchValidationError(`Token is at ${state.token.placeId}; shell ${shell.id} requires ${shellInput.fromId}`); } shell.status = "active"; refinement.status = "active"; const fromPlaceId = state.token.placeId; state.token.placeId = refinement.entryPlaceId; if (enteringNewShell) { pushEvent(events, "transition.activated", opIndex, { transitionId: shell.id, tokenId, inputPlaceId: shellInput.fromId, depth, derived: true, causeOpIndex: opIndex, }); } pushEvent(events, "refinement.entered", opIndex, { refinementId: refinement.id, transitionId: shell.id, tokenId, fromPlaceId, toPlaceId: refinement.entryPlaceId, depth, derived: true, causeOpIndex: opIndex, }); pushEvent(events, "token.moved", opIndex, { tokenId, fromPlaceId, toPlaceId: refinement.entryPlaceId, transitionId: shell.id, refinementId: refinement.id, depth, derived: true, causeOpIndex: opIndex, }); } validatePlanningDependencies(state, leaf); validateLeafContract(leaf); const flow = flowInput(state, leaf.id); successOutput(state, leaf.id); failureOutput(state, leaf.id); readConditionsSatisfied(state, leaf.id); if (state.token.placeId !== flow.fromId) { throw new PatchValidationError(`Token is at ${state.token.placeId}; transition ${leaf.id} requires ${flow.fromId}`); } leaf.status = "active"; state.reservation = { transitionId: leaf.id, tokenId, inputPlaceId: flow.fromId }; pushEvent(events, "transition.activated", opIndex, { transitionId: leaf.id, tokenId, inputPlaceId: flow.fromId, depth: chain.length, derived: false, causeOpIndex: opIndex, }); return events; } function matchingExitRefinement( state: WorkflowState, placeId: string, outcome: "success" | "failure", ): TransitionRefinement | undefined { return liveRefinements(state) .filter( (refinement) => refinement.status === "active" && (outcome === "success" ? refinement.successExitPlaceId === placeId : refinement.failureExitPlaceId === placeId), ) .sort( (a, b) => refinementChainForNode(state, b.parentTransitionId).length - refinementChainForNode(state, a.parentTransitionId).length, )[0]; } // complexity-note(score=2, seen=pi): Recursive token movement, parent lifecycle, and DomainEvent ordering are one atomic contract. // refactor-next: If firing gains more outcomes, extract a transaction-plan object before mutating state instead of extending parallel branches here. function unwindRefinements( state: WorkflowState, outcome: "success" | "failure", tokenId: string, opIndex: number, events: DomainEvent[], ): void { let depth = 1; while (true) { const refinement = matchingExitRefinement(state, state.token.placeId, outcome); if (!refinement) return; const parent = getTransition(state, refinement.parentTransitionId); if (parent.status !== "active") { throw new PatchValidationError(`Refinement ${refinement.id} exit has inactive parent ${parent.id}`); } const fromPlaceId = state.token.placeId; let toPlaceId: string; let canContinue = true; if (outcome === "success") { toPlaceId = successOutput(state, parent.id).toId; parent.status = "completed"; } else { const failure = failureOutput(state, parent.id); if (failure) { toPlaceId = failure.toId; } else { toPlaceId = flowInput(state, parent.id).fromId; canContinue = false; } parent.status = "failed"; } refinement.status = "completed"; refinement.outcome = outcome; state.token.placeId = toPlaceId; pushEvent(events, "refinement.exited", opIndex, { refinementId: refinement.id, transitionId: parent.id, tokenId, fromPlaceId, toPlaceId, outcome, depth, derived: true, causeOpIndex: opIndex, }); pushEvent(events, outcome === "success" ? "transition.completed" : "transition.failed", opIndex, { transitionId: parent.id, tokenId, outputPlaceId: toPlaceId, refinementId: refinement.id, outcome, depth, derived: true, causeOpIndex: opIndex, }); pushEvent(events, "token.moved", opIndex, { tokenId, fromPlaceId, toPlaceId, transitionId: parent.id, refinementId: refinement.id, outcome, depth, derived: true, causeOpIndex: opIndex, }); if (!canContinue) return; depth += 1; } } function applyOperation( state: WorkflowState, op: GraphPatchOp, opIndex: number, now: string, trustedHumanEvidenceIds: ReadonlySet, ): DomainEvent[] { switch (op.op) { case "set_root_intent": { const rootIntent = required(op, "rootIntent"); state.rootIntent = rootIntent; return [event("root_intent.set", opIndex, 0, { rootIntent })]; } case "create_place": { const placeId = required(op, "placeId"); if (state.places[placeId] || state.transitions[placeId]) throw new PatchValidationError(`ID already exists: ${placeId}`); const placeType = required(op, "placeType"); const label = required(op, "label"); if (op.placeState !== undefined && op.placeState !== "unknown") { throw new PatchValidationError(`New Place ${placeId} must start unknown; use evidence plus set_place_state`); } if (placeType === "Gate" && op.gateState !== undefined && op.gateState !== "closed") { throw new PatchValidationError(`New Gate ${placeId} must start closed; use evidence plus mark_gate`); } state.places[placeId] = { id: placeId, type: placeType, label, state: "unknown", ...(placeType === "Gate" ? { gateState: "closed" as const } : {}), evidenceIds: [], }; return [event("place.created", opIndex, 0, { placeId, placeType, label })]; } case "create_transition": { const transitionId = required(op, "transitionId"); if (state.places[transitionId] || state.transitions[transitionId]) { throw new PatchValidationError(`ID already exists: ${transitionId}`); } const transitionType = required(op, "transitionType"); const intent = required(op, "intent"); const planning = normalizePlanning(required(op, "planning"), "declared"); state.transitions[transitionId] = { id: transitionId, type: transitionType, intent, status: "planned", planning, contract: { ...(op.whyNow ? { whyNow: op.whyNow } : {}), ...(op.scope ? { scope: op.scope } : {}), expectedEvidence: [...(op.expectedEvidence ?? [])], evidenceSelectors: [...(op.evidenceSelectors ?? [])], allowedEpisodeKinds: [...(op.allowedEpisodeKinds ?? [])], ...(op.exitCondition ? { exitCondition: op.exitCondition } : {}), ...(op.failureCondition ? { failureCondition: op.failureCondition } : {}), }, evidenceIds: [], }; return [ event("transition.created", opIndex, 0, { transitionId, transitionType, intent, commitment: planning.commitment, }), ]; } case "connect": { const arcId = required(op, "arcId"); if (state.arcs[arcId]) throw new PatchValidationError(`Arc already exists: ${arcId}`); const fromId = required(op, "fromId"); const toId = required(op, "toId"); const arcKind = required(op, "arcKind"); let transition: Transition; if (arcKind === "flow" || arcKind === "read") { getPlace(state, fromId); transition = getTransition(state, toId); } else { transition = getTransition(state, fromId); getPlace(state, toId); } if (transition.status !== "planned") { throw new PatchValidationError(`Topology of ${transition.id} cannot change after activation`); } state.arcs[arcId] = { id: arcId, fromId, toId, kind: arcKind }; return [event("arc.created", opIndex, 0, { arcId, fromId, toId, arcKind })]; } case "set_place_state": { const placeId = required(op, "placeId"); const place = getPlace(state, placeId); if (place.type === "Gate") throw new PatchValidationError(`Use mark_gate for Gate ${placeId}`); const placeState = required(op, "placeState"); ensureEvidenceExists(state, op.evidenceIds, { required: true }); const before = place.state; place.state = placeState; attachEvidenceRefs(place, op.evidenceIds); return [event("place.state_set", opIndex, 0, { placeId, before, placeState, evidenceIds: op.evidenceIds ?? [] })]; } case "set_transition_contract": { const transitionId = required(op, "transitionId"); const transition = getTransition(state, transitionId); const activeLeafScopeChange = transition.status === "active" && state.reservation?.transitionId === transitionId && op.scope !== undefined && op.whyNow === undefined && op.expectedEvidence === undefined && op.evidenceSelectors === undefined && op.allowedEpisodeKinds === undefined && op.exitCondition === undefined && op.failureCondition === undefined; if (transition.status !== "planned" && !activeLeafScopeChange) { throw new PatchValidationError(`Transition contract is immutable after activation; ${transitionId} is ${transition.status}`); } const hasChange = op.whyNow !== undefined || op.scope !== undefined || op.expectedEvidence !== undefined || op.evidenceSelectors !== undefined || op.allowedEpisodeKinds !== undefined || op.exitCondition !== undefined || op.failureCondition !== undefined; if (!hasChange) throw new PatchValidationError("set_transition_contract requires at least one contract field"); if (op.expectedEvidence !== undefined && transition.evidenceIds.length > 0) { const removed = transition.contract.expectedEvidence.filter((item) => !op.expectedEvidence!.includes(item)); if (removed.length > 0) { throw new PatchValidationError(`Cannot remove expected evidence already referenced by Evidence: ${removed.join(", ")}`); } } const before = structuredClone(transition.contract); if (op.whyNow !== undefined) transition.contract.whyNow = op.whyNow; if (op.scope !== undefined) transition.contract.scope = op.scope; if (op.expectedEvidence !== undefined) transition.contract.expectedEvidence = [...op.expectedEvidence]; if (op.evidenceSelectors !== undefined) transition.contract.evidenceSelectors = [...op.evidenceSelectors]; if (op.allowedEpisodeKinds !== undefined) transition.contract.allowedEpisodeKinds = [...op.allowedEpisodeKinds]; if (op.exitCondition !== undefined) transition.contract.exitCondition = op.exitCondition; if (op.failureCondition !== undefined) transition.contract.failureCondition = op.failureCondition; return [event("transition.contract_set", opIndex, 0, { transitionId, before, after: transition.contract })]; } case "set_transition_planning": { const transitionId = required(op, "transitionId"); const transition = getTransition(state, transitionId); if (transition.status !== "planned") { throw new PatchValidationError(`Transition planning is immutable after activation; ${transitionId} is ${transition.status}`); } const planningPatch = required(op, "planning"); if (Object.keys(planningPatch).length === 0) { throw new PatchValidationError("set_transition_planning requires at least one planning field"); } ensureEvidenceExists(state, op.evidenceIds); const before = structuredClone(transition.planning); const planningSource = (op.evidenceIds ?? []).some( (evidenceId) => trustedHumanEvidenceIds.has(evidenceId) && state.evidence[evidenceId]?.type === "human_statement", ) ? "human" : "declared"; const merged = normalizePlanning( { commitment: planningPatch.commitment ?? transition.planning.commitment, dependsOn: planningPatch.dependsOn ?? transition.planning.dependsOn, ...(planningPatch.reconsiderWhen !== undefined ? { reconsiderWhen: planningPatch.reconsiderWhen } : transition.planning.reconsiderWhen ? { reconsiderWhen: transition.planning.reconsiderWhen } : {}), ...(planningPatch.refinementTrigger !== undefined ? { refinementTrigger: planningPatch.refinementTrigger } : transition.planning.refinementTrigger ? { refinementTrigger: transition.planning.refinementTrigger } : {}), }, planningSource, transition.planning.requiresRefinement, ); if (canonicalJson(before) === canonicalJson(merged)) { throw new PatchValidationError(`set_transition_planning does not change ${transitionId}`); } transition.planning = merged; attachEvidenceRefs(transition, op.evidenceIds); return [ event("transition.planning_set", opIndex, 0, { transitionId, before, after: merged, reason: required(op, "reason"), evidenceIds: op.evidenceIds ?? [], }), ]; } case "refine_transition": { const refinementId = required(op, "refinementId"); if (state.refinements[refinementId]) throw new PatchValidationError(`Refinement already exists: ${refinementId}`); const parentTransitionId = required(op, "parentTransitionId"); const parent = getTransition(state, parentTransitionId); if (parent.status !== "planned" && parent.status !== "active") { throw new PatchValidationError(`Only planned or active transitions can be refined; ${parentTransitionId} is ${parent.status}`); } if (refinementForParent(state, parentTransitionId)) { throw new PatchValidationError(`Transition ${parentTransitionId} already has a live refinement`); } if (parent.status === "active") { const deepeningActiveLeaf = state.reservation?.transitionId === parentTransitionId; const replacingActiveRefinement = Object.values(state.refinements).some( (refinement) => refinement.parentTransitionId === parentTransitionId && refinement.status === "superseded" && refinement.supersededBy === refinementId, ); if (!deepeningActiveLeaf && !replacingActiveRefinement) { throw new PatchValidationError( `Active transition ${parentTransitionId} can only refine its executable leaf or install a declared replacement`, ); } if (deepeningActiveLeaf) delete state.reservation; } ensureEvidenceExists(state, op.evidenceIds); const nodeIds = [...required(op, "nodeIds")]; const arcIds = [...required(op, "arcIds")]; for (const nodeId of nodeIds) { if (!state.places[nodeId] && !state.transitions[nodeId]) { throw new PatchValidationError(`Refinement node not found: ${nodeId}`); } } for (const arcId of arcIds) { if (!state.arcs[arcId]) throw new PatchValidationError(`Refinement arc not found: ${arcId}`); } state.refinements[refinementId] = { id: refinementId, parentTransitionId, nodeIds, arcIds, entryPlaceId: required(op, "entryPlaceId"), successExitPlaceId: required(op, "successExitPlaceId"), ...(op.failureExitPlaceId ? { failureExitPlaceId: op.failureExitPlaceId } : {}), status: "draft", }; attachEvidenceRefs(parent, op.evidenceIds); return [ event("transition.refined", opIndex, 0, { refinementId, parentTransitionId, nodeIds, arcIds, reason: required(op, "reason"), evidenceIds: op.evidenceIds ?? [], }), ]; } case "supersede_refinement": { const oldRefinementId = required(op, "oldRefinementId"); const newRefinementId = required(op, "newRefinementId"); if (oldRefinementId === newRefinementId) throw new PatchValidationError("A refinement cannot supersede itself"); const oldRefinement = getRefinement(state, oldRefinementId); if (oldRefinement.status !== "draft" && oldRefinement.status !== "active") { throw new PatchValidationError( `Only draft or active refinements can be superseded; ${oldRefinementId} is ${oldRefinement.status}`, ); } ensureEvidenceExists(state, op.evidenceIds); const displacedTransitionIds: string[] = []; const displacedRefinementIds: string[] = []; if (oldRefinement.status === "active") { const parent = getTransition(state, oldRefinement.parentTransitionId); if (parent.status !== "active") { throw new PatchValidationError(`Active refinement ${oldRefinementId} has inactive parent ${parent.id}`); } const reservation = state.reservation; if (!reservation || !isNodeInsideRefinement(state, reservation.transitionId, oldRefinementId)) { throw new PatchValidationError(`Active refinement ${oldRefinementId} does not own the executable leaf`); } for (const transition of Object.values(state.transitions)) { if (transition.status === "active" && isNodeInsideRefinement(state, transition.id, oldRefinementId)) { transition.status = "superseded"; displacedTransitionIds.push(transition.id); } } const nestedActiveRefinements = liveRefinements(state).filter( (refinement) => refinement.id !== oldRefinementId && refinement.status === "active" && isNodeInsideRefinement(state, refinement.parentTransitionId, oldRefinementId), ); for (const refinement of nestedActiveRefinements) { refinement.status = "superseded"; displacedRefinementIds.push(refinement.id); } delete state.reservation; } oldRefinement.status = "superseded"; oldRefinement.supersededBy = newRefinementId; return [ event("refinement.superseded", opIndex, 0, { oldRefinementId, newRefinementId, parentTransitionId: oldRefinement.parentTransitionId, reason: required(op, "reason"), evidenceIds: op.evidenceIds ?? [], displacedTransitionIds, displacedRefinementIds, }), ]; } case "activate_transition": return activateLeaf(state, required(op, "transitionId"), required(op, "tokenId"), opIndex); case "complete_transition": { const transitionId = required(op, "transitionId"); const tokenId = required(op, "tokenId"); const transition = getTransition(state, transitionId); if (!state.reservation || state.reservation.transitionId !== transitionId || state.reservation.tokenId !== tokenId) { throw new PatchValidationError(`Transition ${transitionId} does not hold token ${tokenId}`); } ensureEvidenceExists(state, op.evidenceIds, { required: true }); attachEvidenceRefs(transition, op.evidenceIds); const fromPlaceId = state.reservation.inputPlaceId; const output = successOutput(state, transitionId); state.token.placeId = output.toId; transition.status = "completed"; delete state.reservation; const events: DomainEvent[] = []; pushEvent(events, "transition.completed", opIndex, { transitionId, tokenId, outputPlaceId: output.toId, outcome: "success", depth: 0, derived: false, causeOpIndex: opIndex, evidenceIds: op.evidenceIds ?? [], }); pushEvent(events, "token.moved", opIndex, { tokenId, fromPlaceId, toPlaceId: output.toId, transitionId, outcome: "success", depth: 0, derived: false, causeOpIndex: opIndex, }); unwindRefinements(state, "success", tokenId, opIndex, events); return events; } case "fail_transition": { const transitionId = required(op, "transitionId"); const tokenId = required(op, "tokenId"); const transition = getTransition(state, transitionId); if (!state.reservation || state.reservation.transitionId !== transitionId || state.reservation.tokenId !== tokenId) { throw new PatchValidationError(`Transition ${transitionId} does not hold token ${tokenId}`); } ensureEvidenceExists(state, op.evidenceIds, { required: true }); attachEvidenceRefs(transition, op.evidenceIds); const fromPlaceId = state.reservation.inputPlaceId; const failure = failureOutput(state, transitionId); state.token.placeId = failure?.toId ?? fromPlaceId; transition.status = "failed"; delete state.reservation; const events: DomainEvent[] = []; pushEvent(events, "transition.failed", opIndex, { transitionId, tokenId, outputPlaceId: state.token.placeId, outcome: "failure", depth: 0, derived: false, causeOpIndex: opIndex, evidenceIds: op.evidenceIds ?? [], }); pushEvent(events, "token.moved", opIndex, { tokenId, fromPlaceId, toPlaceId: state.token.placeId, transitionId, outcome: "failure", depth: 0, derived: false, causeOpIndex: opIndex, }); if (failure) unwindRefinements(state, "failure", tokenId, opIndex, events); return events; } case "retire_transition": { const transitionId = required(op, "transitionId"); const transition = getTransition(state, transitionId); if (transition.status !== "planned") { throw new PatchValidationError(`Only planned transitions can be retired; ${transitionId} is ${transition.status}`); } transition.status = "retired"; return [event("transition.retired", opIndex, 0, { transitionId, reason: required(op, "reason") })]; } case "supersede_transition": { const oldTransitionId = required(op, "oldTransitionId"); const newTransitionId = required(op, "newTransitionId"); if (oldTransitionId === newTransitionId) throw new PatchValidationError("A transition cannot supersede itself"); const oldTransition = getTransition(state, oldTransitionId); const newTransition = getTransition(state, newTransitionId); const supersedingActiveLeaf = oldTransition.status === "active" && state.reservation?.transitionId === oldTransitionId; if (oldTransition.status !== "planned" && oldTransition.status !== "failed" && !supersedingActiveLeaf) { throw new PatchValidationError(`Transition ${oldTransitionId} cannot be superseded from ${oldTransition.status}`); } if (oldTransition.status === "active" && !supersedingActiveLeaf) { throw new PatchValidationError(`Only the active executable leaf can be superseded; ${oldTransitionId} is a shell`); } if (newTransition.status !== "planned") { throw new PatchValidationError(`Replacement transition ${newTransitionId} must be planned`); } if (supersedingActiveLeaf) { ensureEvidenceExists(state, op.evidenceIds, { required: true }); attachEvidenceRefs(oldTransition, op.evidenceIds); delete state.reservation; } oldTransition.status = "superseded"; oldTransition.supersededBy = newTransitionId; return [ event("transition.superseded", opIndex, 0, { oldTransitionId, newTransitionId, reason: required(op, "reason"), evidenceIds: op.evidenceIds ?? [], releasedReservation: supersedingActiveLeaf, }), ]; } case "attach_evidence": return [applyAttachEvidence(state, op, opIndex, now)]; case "mark_gate": { const placeId = required(op, "placeId"); const place = getPlace(state, placeId); if (place.type !== "Gate") throw new PatchValidationError(`Place ${placeId} is not a Gate`); ensureEvidenceExists(state, op.evidenceIds, { required: true }); const gateState = required(op, "gateState"); const before = place.gateState; place.gateState = gateState; attachEvidenceRefs(place, op.evidenceIds); return [event("gate.marked", opIndex, 0, { placeId, before, gateState, evidenceIds: op.evidenceIds ?? [] })]; } } } function applyAttachEvidence(state: WorkflowState, op: AttachEvidenceOp, opIndex: number, now: string): DomainEvent { const targetId = required(op, "targetId"); const evidenceInput = required(op, "evidence"); if (state.evidence[evidenceInput.evidenceId]) { throw new PatchValidationError(`Evidence already exists: ${evidenceInput.evidenceId}`); } const target = state.places[targetId] ?? state.transitions[targetId]; if (!target) throw new PatchValidationError(`Evidence target not found: ${targetId}`); state.evidence[evidenceInput.evidenceId] = { ...evidenceInput, selectors: [...(evidenceInput.selectors ?? [])], summary: redactText(evidenceInput.summary), source: redactText(evidenceInput.source), observedAt: evidenceInput.observedAt ?? now, redacted: true, }; target.evidenceIds.push(evidenceInput.evidenceId); return event("evidence.attached", opIndex, 0, { targetId, evidenceId: evidenceInput.evidenceId, type: evidenceInput.type, }); } function isEvidenceOnly(patch: GraphPatch): boolean { return patch.ops.every((op) => op.op === "attach_evidence"); } function sanitizeDurablePatch(patch: GraphPatch): GraphPatch { return { ...patch, ops: patch.ops.map((op) => { if (op.op !== "attach_evidence") return structuredClone(op); return { ...op, evidence: { ...op.evidence, summary: redactText(op.evidence.summary), source: redactText(op.evidence.source), redacted: true, }, }; }), }; } export function applyGraphPatch(current: WorkflowState, patch: GraphPatch, now = new Date().toISOString()): ApplyPatchResult { if (patch.schemaVersion !== 2) { return { status: "rejected", state: current, currentRevision: current.revision, error: `Unsupported GraphPatch schema version: ${String(patch.schemaVersion)}`, }; } const durablePatch = sanitizeDurablePatch(patch); const requestHash = patchRequestHash(durablePatch); const existing = current.appliedPatches[patch.idempotencyKey]; if (existing) { if (existing.requestHash !== requestHash) { return { status: "rejected", state: current, currentRevision: current.revision, error: `Idempotency key ${patch.idempotencyKey} was reused with different patch content`, }; } return { status: "already_applied", state: current, revision: existing.revision, snapshotHash: existing.snapshotHash, }; } if (durablePatch.baseRevision !== current.revision && !isEvidenceOnly(durablePatch)) { return { status: "conflict", state: current, currentRevision: current.revision, error: `Stale structural patch: base revision ${patch.baseRevision}, current revision ${current.revision}`, }; } const draft = structuredClone(current); const events: DomainEvent[] = []; try { for (const [index, op] of durablePatch.ops.entries()) { if (op.op !== "set_transition_contract") continue; if (current.transitions[op.transitionId]?.status !== "active") continue; const refinesActiveLeaf = durablePatch.ops .slice(index + 1) .some( (candidate) => candidate.op === "refine_transition" && candidate.parentTransitionId === op.transitionId, ); if (!refinesActiveLeaf) { throw new PatchValidationError( `Active transition ${op.transitionId} contract can only gain scope while it is refined in the same patch`, ); } } const trustedHumanEvidenceIds = new Set(durablePatch.trustedHumanEvidenceIds ?? []); for (const [index, op] of durablePatch.ops.entries()) { const operationEvents = applyOperation(draft, op, index, now, trustedHumanEvidenceIds); for (const operationEvent of operationEvents) { events.push({ ...operationEvent, eventIndex: events.filter((item) => item.opIndex === index).length }); } } validateGraph(draft); for (const refinement of Object.values(draft.refinements)) { if (!refinement.supersededBy) continue; const replacement = draft.refinements[refinement.supersededBy]; if (!replacement) { throw new PatchValidationError( `Superseded refinement ${refinement.id} references missing replacement ${refinement.supersededBy}`, ); } if (replacement.parentTransitionId !== refinement.parentTransitionId) { throw new PatchValidationError( `Replacement refinement ${replacement.id} must refine the same parent ${refinement.parentTransitionId}`, ); } if (replacement.status === "superseded") { throw new PatchValidationError(`Replacement refinement ${replacement.id} is already superseded`); } } draft.revision = current.revision + 1; draft.appliedPatches[patch.idempotencyKey] = { patchId: patch.patchId, revision: draft.revision, snapshotHash: "pending", requestHash, }; const hash = snapshotHash(draft); draft.appliedPatches[patch.idempotencyKey]!.snapshotHash = hash; const batch: CommittedBatch = { patch: durablePatch, revision: draft.revision, events, snapshotHash: hash, committedAt: now, }; return { status: "applied", state: draft, batch }; } catch (error) { return { status: "rejected", state: current, currentRevision: current.revision, error: error instanceof Error ? error.message : String(error), }; } }