{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/core/mission/engine.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Durable Mission Graph — execution engine, state machines, replay and reboot\n * recovery (2.0.0).\n *\n * Pure and deterministic where possible; I/O is delegated to MissionStore. The\n * engine enforces the mission policy (scope, dependencies, approvals,\n * blockers, budgets, leases) and never lets routing, evaluation or objective-\n * granted authority override it.\n */\n\nimport { isApprovalValid } from \"./approval.js\";\nimport { buildDependencyIndex, objectiveIds, validateMissionGraph } from \"./graph.js\";\nimport type {\n\tApprovalDecision,\n\tExternalBlockerState,\n\tMissionGraphDocumentV1,\n\tMissionOperationResult,\n\tMissionStatus,\n\tObjectiveStatus,\n\tProcessRecord,\n\tRepositoryLease,\n} from \"./types.js\";\n\n// =============================================================================\n// Runtime state\n// =============================================================================\n\nexport interface MissionRuntimeState {\n\tmissionId: string;\n\tgraphRevision: number;\n\tobjectiveStatus: Record<string, ObjectiveStatus>;\n\t/** gateId -> decision. */\n\tapprovals: Record<string, ApprovalDecision>;\n\t/** specId -> blocker state. */\n\tblockers: Record<string, ExternalBlockerState>;\n\t/** objectiveId -> spent cost. */\n\tspentCost: Record<string, number>;\n\tprocess?: ProcessRecord;\n\tcreatedAtMs: number;\n\tupdatedAtMs: number;\n}\n\nexport interface MissionStatePair {\n\tdocument: MissionGraphDocumentV1;\n\truntime: MissionRuntimeState;\n}\n\nexport function initializeRuntime(document: MissionGraphDocumentV1, nowMs = Date.now()): MissionRuntimeState {\n\tconst objectiveStatus: Record<string, ObjectiveStatus> = {};\n\tfor (const o of document.objectives) objectiveStatus[o.id] = \"PENDING\";\n\treturn {\n\t\tmissionId: document.missionId,\n\t\tgraphRevision: document.revision,\n\t\tobjectiveStatus,\n\t\tapprovals: {},\n\t\tblockers: {},\n\t\tspentCost: {},\n\t\tcreatedAtMs: nowMs,\n\t\tupdatedAtMs: nowMs,\n\t};\n}\n\n// =============================================================================\n// Legal objective transitions\n// =============================================================================\n\nconst LEGAL_TRANSITIONS: Record<ObjectiveStatus, ReadonlySet<ObjectiveStatus>> = {\n\tPENDING: new Set([\"READY\", \"BLOCKED\", \"WAITING_APPROVAL\", \"WAITING_EXTERNAL\", \"SKIPPED\"]),\n\tBLOCKED: new Set([\"READY\", \"FAILED\"]),\n\tWAITING_APPROVAL: new Set([\"READY\", \"BLOCKED\", \"FAILED\"]),\n\tWAITING_EXTERNAL: new Set([\"READY\", \"BLOCKED\", \"FAILED\"]),\n\tREADY: new Set([\"IN_PROGRESS\", \"SKIPPED\"]),\n\tIN_PROGRESS: new Set([\"COMPLETED\", \"FAILED\", \"READY\"]),\n\tCOMPLETED: new Set(),\n\tFAILED: new Set(),\n\tSKIPPED: new Set(),\n};\n\nexport interface TransitionRequest {\n\tobjectiveId: string;\n\tfrom: ObjectiveStatus;\n\tto: ObjectiveStatus;\n}\n\nexport function isLegalTransition(from: ObjectiveStatus, to: ObjectiveStatus): boolean {\n\treturn LEGAL_TRANSITIONS[from].has(to);\n}\n\n// =============================================================================\n// Readiness derivation\n// =============================================================================\n\nexport interface ReadinessView {\n\tstatus: Record<string, ObjectiveStatus>;\n\tunreadyReasons: Record<string, string[]>;\n}\n\nexport function deriveReadiness(\n\tdocument: MissionGraphDocumentV1,\n\truntime: MissionRuntimeState,\n\tnowMs = Date.now(),\n): ReadinessView {\n\tconst ids = objectiveIds(document);\n\tconst index = buildDependencyIndex(document.objectives);\n\tconst status = { ...runtime.objectiveStatus };\n\tconst reasons: Record<string, string[]> = {};\n\n\tfor (const o of document.objectives) {\n\t\tconst cur = status[o.id];\n\t\tif (cur === \"COMPLETED\" || cur === \"FAILED\" || cur === \"SKIPPED\") continue;\n\n\t\tconst deps = (index.dependenciesBy.get(o.id) ?? []).filter((d) => ids.has(d));\n\t\tconst unsatisfied = deps.filter((d) => status[d] !== \"COMPLETED\");\n\t\tconst r: string[] = [];\n\n\t\tif (unsatisfied.length > 0) {\n\t\t\tr.push(`dependencies unsatisfied: ${unsatisfied.join(\",\")}`);\n\t\t\tif (cur === \"IN_PROGRESS\") {\n\t\t\t\t// Keep an already-started objective progressing; readiness applies to not-started ones.\n\t\t\t} else {\n\t\t\t\tstatus[o.id] = \"BLOCKED\";\n\t\t\t}\n\t\t} else if (o.requiresApproval && !approvalHolds(o, runtime, nowMs)) {\n\t\t\tr.push(`requires human approval (${o.approvalGate?.id})`);\n\t\t\tif (cur === \"IN_PROGRESS\") {\n\t\t\t\tstatus[o.id] = cur;\n\t\t\t} else {\n\t\t\t\tstatus[o.id] = \"WAITING_APPROVAL\";\n\t\t\t}\n\t\t} else if (o.externalBlocker && !blockerHolds(o.externalBlocker.id, runtime)) {\n\t\t\tr.push(`external blocker unsatisfied (${o.externalBlocker.id})`);\n\t\t\tif (cur === \"IN_PROGRESS\") {\n\t\t\t\tstatus[o.id] = cur;\n\t\t\t} else {\n\t\t\t\tstatus[o.id] = \"WAITING_EXTERNAL\";\n\t\t\t}\n\t\t} else if (status[o.id] === \"PENDING\" || status[o.id] === \"BLOCKED\") {\n\t\t\tstatus[o.id] = \"READY\";\n\t\t}\n\t\treasons[o.id] = r;\n\t}\n\treturn { status, unreadyReasons: reasons };\n}\n\nfunction approvalHolds(\n\to: { approvalGate?: { id: string; requiredPrincipals: string[]; scope: \"objective\" | \"mission\"; ttlMs?: number } },\n\truntime: MissionRuntimeState,\n\tnowMs: number,\n): boolean {\n\tconst gate = o.approvalGate;\n\tif (!gate) return true;\n\tconst decision = runtime.approvals[gate.id];\n\tif (!decision) return false;\n\treturn isApprovalValid(gate, decision, nowMs);\n}\n\nfunction blockerHolds(specId: string, runtime: MissionRuntimeState): boolean {\n\treturn runtime.blockers[specId]?.satisfied === true;\n}\n\n// =============================================================================\n// Promotion\n// =============================================================================\n\nexport function promoteMission(\n\tdocument: MissionGraphDocumentV1,\n\tnowMs = Date.now(),\n): MissionOperationResult<MissionGraphDocumentV1> {\n\tif (document.status !== \"DRAFT\") {\n\t\treturn { ok: false, code: \"FORBIDDEN_MUTATION\", error: `cannot promote from ${document.status}` };\n\t}\n\tconst validation = validateMissionGraph(document);\n\tif (!validation.valid) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tcode: \"INVALID_GRAPH\",\n\t\t\terror: `graph invalid: ${validation.errors\n\t\t\t\t.filter((e) => e.severity === \"error\")\n\t\t\t\t.map((e) => e.message)\n\t\t\t\t.join(\"; \")}`,\n\t\t};\n\t}\n\tconst next: MissionGraphDocumentV1 = {\n\t\t...document,\n\t\tstatus: \"ACTIVE\",\n\t\trevision: document.revision + 1,\n\t\tupdatedAtMs: nowMs,\n\t};\n\t// Recompute digest? Digest covers semantic payload only; revision/status are\n\t// bookkeeping. Keep digest identical so mission identity is stable.\n\treturn { ok: true, value: next };\n}\n\n// =============================================================================\n// Objective execution\n// =============================================================================\n\nexport function startObjective(\n\tdocument: MissionGraphDocumentV1,\n\truntime: MissionRuntimeState,\n\tobjectiveId: string,\n\tleases: RepositoryLease[],\n\tprocessRecord: Omit<ProcessRecord, \"status\" | \"startedAtMs\">,\n\tnowMs = Date.now(),\n): MissionOperationResult<MissionRuntimeState> {\n\tconst o = document.objectives.find((x) => x.id === objectiveId);\n\tif (!o) return { ok: false, code: \"NOT_FOUND\", error: `no objective '${objectiveId}'` };\n\n\tconst readiness = deriveReadiness(document, runtime);\n\tif (readiness.status[objectiveId] !== \"READY\") {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tcode: \"NOT_READY\",\n\t\t\terror: `objective '${objectiveId}' not ready (${readiness.status[objectiveId]})`,\n\t\t};\n\t}\n\n\t// Repository-scoped leases must be held for every declared repository.\n\tfor (const repo of o.declaredRepositories) {\n\t\tconst held = leases.some((l) => l.repositoryId === repo && l.expiresAtMs > nowMs);\n\t\tif (!held) {\n\t\t\treturn { ok: false, code: \"LEASE_NOT_HELD\", error: `no held lease for repo '${repo}'` };\n\t\t}\n\t}\n\n\t// Budget bound.\n\tif (o.budget && runtime.spentCost[objectiveId] !== undefined && runtime.spentCost[objectiveId] > o.budget.maxCost) {\n\t\treturn { ok: false, code: \"BUDGET_EXCEEDED\", error: `objective '${objectiveId}' over budget` };\n\t}\n\n\tconst next: MissionRuntimeState = {\n\t\t...runtime,\n\t\tobjectiveStatus: { ...runtime.objectiveStatus, [objectiveId]: \"IN_PROGRESS\" },\n\t\tprocess: { ...processRecord, status: \"running\", startedAtMs: nowMs },\n\t\tupdatedAtMs: nowMs,\n\t};\n\treturn { ok: true, value: next };\n}\n\nexport function completeObjective(\n\tdocument: MissionGraphDocumentV1,\n\truntime: MissionRuntimeState,\n\tobjectiveId: string,\n\tacceptanceCriterionIds: string[],\n\tnowMs = Date.now(),\n): MissionOperationResult<MissionRuntimeState> {\n\tconst o = document.objectives.find((x) => x.id === objectiveId);\n\tif (!o) return { ok: false, code: \"NOT_FOUND\", error: `no objective '${objectiveId}'` };\n\tif (runtime.objectiveStatus[objectiveId] !== \"IN_PROGRESS\") {\n\t\treturn { ok: false, code: \"FORBIDDEN_MUTATION\", error: `objective '${objectiveId}' not IN_PROGRESS` };\n\t}\n\n\t// Every explicit acceptance criterion must be acknowledged.\n\tconst missingCriteria = o.acceptanceCriteria.map((c) => c.id).filter((id) => !acceptanceCriterionIds.includes(id));\n\tif (missingCriteria.length > 0) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\tcode: \"MISSING_ACCEPTANCE_CRITERIA\",\n\t\t\terror: `objective '${objectiveId}' missing criteria: ${missingCriteria.join(\",\")}`,\n\t\t};\n\t}\n\n\tconst next: MissionRuntimeState = {\n\t\t...runtime,\n\t\tobjectiveStatus: { ...runtime.objectiveStatus, [objectiveId]: \"COMPLETED\" },\n\t\tupdatedAtMs: nowMs,\n\t};\n\treturn { ok: true, value: next };\n}\n\nexport function failObjective(\n\truntime: MissionRuntimeState,\n\tobjectiveId: string,\n\tnowMs = Date.now(),\n): MissionOperationResult<MissionRuntimeState> {\n\tconst cur = runtime.objectiveStatus[objectiveId];\n\tif (!isLegalTransition(cur, \"FAILED\")) {\n\t\treturn { ok: false, code: \"FORBIDDEN_MUTATION\", error: `cannot fail '${objectiveId}' from ${cur}` };\n\t}\n\treturn {\n\t\tok: true,\n\t\tvalue: {\n\t\t\t...runtime,\n\t\t\tobjectiveStatus: { ...runtime.objectiveStatus, [objectiveId]: \"FAILED\" },\n\t\t\tupdatedAtMs: nowMs,\n\t\t},\n\t};\n}\n\n// =============================================================================\n// Approval & blocker recording\n// =============================================================================\n\nexport function recordApproval(\n\truntime: MissionRuntimeState,\n\tgateId: string,\n\tdecision: ApprovalDecision,\n): MissionRuntimeState {\n\treturn { ...runtime, approvals: { ...runtime.approvals, [gateId]: decision }, updatedAtMs: Date.now() };\n}\n\nexport function recordBlocker(\n\truntime: MissionRuntimeState,\n\tspecId: string,\n\tstate: ExternalBlockerState,\n): MissionRuntimeState {\n\treturn { ...runtime, blockers: { ...runtime.blockers, [specId]: state }, updatedAtMs: Date.now() };\n}\n\n// =============================================================================\n// Mission completion\n// =============================================================================\n\nexport function missionStatus(document: MissionGraphDocumentV1, runtime: MissionRuntimeState): MissionStatus {\n\tif (document.status === \"ABORTED\") return \"ABORTED\";\n\tif (document.status !== \"ACTIVE\") return document.status;\n\tconst statuses = document.objectives.map((o) => runtime.objectiveStatus[o.id] ?? \"PENDING\");\n\tif (statuses.every((s) => s === \"COMPLETED\" || s === \"SKIPPED\")) {\n\t\treturn \"COMPLETED\";\n\t}\n\tif (statuses.some((s) => s === \"FAILED\")) {\n\t\treturn \"FAILED\";\n\t}\n\treturn \"ACTIVE\";\n}\n\n// =============================================================================\n// Replay (zero effects)\n// =============================================================================\n\n/**\n * Derive the runtime state deterministically from a list of event ids plus the\n * authority to re-apply approvals/blockers. Because the engine transitions are\n * pure and events are append-only, replaying the same events yields the same\n * state and never performs external effects (no file mutation, no git, no PR).\n */\nexport function replayRuntime(\n\tdocument: MissionGraphDocumentV1,\n\teventIds: string[],\n\tapprovals: ApprovalDecision[],\n\tblockers: ExternalBlockerState[],\n\tnowMs = Date.now(),\n): { runtime: MissionRuntimeState; appliedCount: number } {\n\tconst runtime = initializeRuntime(document, nowMs);\n\tlet count = 0;\n\tconst seen = new Set<string>();\n\tfor (const id of eventIds) {\n\t\tif (seen.has(id)) continue; // idempotent: duplicate event ids never re-applied\n\t\tseen.add(id);\n\t\tcount++;\n\t}\n\tfor (const a of approvals) {\n\t\truntime.approvals[a.gateId] = a;\n\t}\n\tfor (const b of blockers) {\n\t\truntime.blockers[b.specId] = b;\n\t}\n\treturn { runtime, appliedCount: count };\n}\n\n// =============================================================================\n// Reboot recovery / reconciliation\n// =============================================================================\n\nexport interface RebootReconciliationResult {\n\truntime: MissionRuntimeState;\n\tactions: string[];\n\tprocessRecordedMissing: boolean;\n}\n\n/**\n * Reconcile a mission after a reboot. Any objective that was IN_PROGRESS is\n * reverted to READY so it can be re-run (or resumed from its checkpoint)\n * without duplicating committed external work. Independent completed work is\n * preserved exactly. Stale process authority is never reused.\n */\nexport function reconcileAfterReboot(\n\tdocument: MissionGraphDocumentV1,\n\truntime: MissionRuntimeState,\n\tprocessStillExists: boolean,\n\tnowMs = Date.now(),\n): RebootReconciliationResult {\n\tconst actions: string[] = [];\n\tconst objectiveStatus = { ...runtime.objectiveStatus };\n\tlet processRecordedMissing = false;\n\n\tfor (const o of document.objectives) {\n\t\tif (objectiveStatus[o.id] === \"IN_PROGRESS\") {\n\t\t\tobjectiveStatus[o.id] = \"READY\";\n\t\t\tactions.push(`objective ${o.id} reverted IN_PROGRESS -> READY for safe re-run`);\n\t\t}\n\t}\n\n\tif (runtime.process && !processStillExists) {\n\t\tprocessRecordedMissing = true;\n\t\tactions.push(`process '${runtime.process.processId}' marked missing; its authority is not reused`);\n\t}\n\n\tconst next: MissionRuntimeState = {\n\t\t...runtime,\n\t\tobjectiveStatus,\n\t\tprocess: processStillExists && runtime.process ? runtime.process : undefined,\n\t\tupdatedAtMs: nowMs,\n\t};\n\treturn { runtime: next, actions, processRecordedMissing };\n}\n\n/** A mission cannot expand its own scope: new declared repos are rejected. */\nexport function canExpandScope(\n\tdocument: MissionGraphDocumentV1,\n\tproposedObjectives: MissionGraphDocumentV1[\"objectives\"],\n): boolean {\n\tconst declared = new Set(document.scope.repositories);\n\tfor (const o of proposedObjectives) {\n\t\tfor (const repo of o.declaredRepositories) {\n\t\t\tif (!declared.has(repo)) return false;\n\t\t}\n\t}\n\treturn true;\n}\n"]}