{"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../../../src/core/mission/scheduler.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,sBAAsB,EAAmB,eAAe,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAExH,MAAM,WAAW,aAAa;IAC7B,wCAAwC;IACxC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IACrC,mDAAmD;IACnD,gBAAgB,EAAE,MAAM,CAAC;IACzB,kCAAkC;IAClC,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,iDAAiD;IACjD,aAAa,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,eAAO,MAAM,iBAAiB,EAAE,WAAW,CAAC,eAAe,CAA+C,CAAC;AAE3G,eAAO,MAAM,sBAAsB,EAAE,WAAW,CAAC,eAAe,CAM9D,CAAC;AAMH;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAChC,OAAO,EAAE,IAAI,CAAC,sBAAsB,EAAE,YAAY,GAAG,WAAW,GAAG,UAAU,CAAC,EAC9E,KAAK,EAAE,aAAa,GAClB,YAAY,CA6Gd;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC/B,OAAO,EAAE,IAAI,CAAC,sBAAsB,EAAE,YAAY,CAAC,EACnD,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,GAClC,OAAO,CAMT;AAED,wBAAgB,gBAAgB,CAC/B,UAAU,EAAE,IAAI,CAAC,sBAAsB,EAAE,YAAY,CAAC,CAAC,YAAY,CAAC,EACpE,aAAa,CAAC,EAAE,aAAa,EAC7B,cAAc,UAAO,GACnB,MAAM,EAAE,CAYV","sourcesContent":["/**\n * Durable Mission Graph — dependency-aware scheduler (2.0.0).\n *\n * Computes deterministic parallel scheduling waves from the dependency graph,\n * enforces a parallelism bound, serializes repository write conflicts, and\n * respects mission/objective budgets. Pure and deterministic.\n */\n\nimport { buildDependencyIndex, computeCriticalPath, objectiveIds } from \"./graph.js\";\nimport type { MissionBudget, MissionGraphDocumentV1, ObjectiveBudget, ObjectiveStatus, SchedulePlan } from \"./types.js\";\n\nexport interface ScheduleInput {\n\t/** Current objective statuses by id. */\n\tstatus: Map<string, ObjectiveStatus>;\n\t/** Parallelism bound (max objectives per wave). */\n\tparallelismBound: number;\n\t/** Mission-wide budget if any. */\n\tbudget?: MissionBudget;\n\t/** Enforce budget as hard gate on scheduling. */\n\tenforceBudget?: boolean;\n}\n\nexport const TERMINAL_STATUSES: ReadonlySet<ObjectiveStatus> = new Set([\"COMPLETED\", \"FAILED\", \"SKIPPED\"]);\n\nexport const UNSCHEDULABLE_STATUSES: ReadonlySet<ObjectiveStatus> = new Set([\n\t\"BLOCKED\",\n\t\"FAILED\",\n\t\"SKIPPED\",\n\t\"WAITING_APPROVAL\",\n\t\"WAITING_EXTERNAL\",\n]);\n\nfunction isSatisfied(status: ObjectiveStatus | undefined): boolean {\n\treturn status === \"COMPLETED\";\n}\n\n/**\n * Build a deterministic schedule plan.\n *\n * Returns waves of objectives that are dependency-ready and parallel-safe.\n * Write-conflict groups (objectives mutating the same repository) are\n * serialized so they never run in the same wave.\n */\nexport function buildSchedulePlan(\n\tmission: Pick<MissionGraphDocumentV1, \"objectives\" | \"missionId\" | \"revision\">,\n\tinput: ScheduleInput,\n): SchedulePlan {\n\tconst ids = objectiveIds(mission);\n\tconst byId = new Map(mission.objectives.map((o) => [o.id, o]));\n\tconst index = buildDependencyIndex(mission.objectives);\n\n\tconst waves: string[][] = [];\n\tconst serializedGroups: string[][] = [];\n\tconst unready: string[] = [];\n\tconst planned = new Set<string>();\n\n\tconst objectiveStatus = (id: string): ObjectiveStatus | undefined => input.status.get(id);\n\n\tconst repoSet = (id: string): Set<string> => {\n\t\tconst o = byId.get(id);\n\t\treturn new Set(o?.declaredRepositories ?? []);\n\t};\n\n\t// Remaining objectives = not terminal and not unschedulable permanently.\n\tconst remainingIds = [...ids]\n\t\t.filter((id) => {\n\t\t\tconst s = objectiveStatus(id);\n\t\t\tif (s === undefined) return true; // unassigned → treat as pending/ready\n\t\t\treturn !TERMINAL_STATUSES.has(s) && !UNSCHEDULABLE_STATUSES.has(s);\n\t\t})\n\t\t.sort();\n\n\tconst budgetExceededRef = { value: false };\n\n\twhile (remainingIds.length > planned.size && remainingIds.length > 0) {\n\t\t// Candidate ready: all dependencies satisfied, not yet planned.\n\t\tconst candidateIds = remainingIds.filter((id) => {\n\t\t\tif (planned.has(id)) return false;\n\t\t\tconst deps = (index.dependenciesBy.get(id) ?? []).filter((d) => ids.has(d));\n\t\t\treturn deps.every((d) => isSatisfied(objectiveStatus(d)));\n\t\t});\n\n\t\tif (candidateIds.length === 0) {\n\t\t\t// No progress possible; mark the rest unready.\n\t\t\tfor (const id of remainingIds) {\n\t\t\t\tif (!planned.has(id)) unready.push(id);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\n\t\t// Group candidates by repository write-conflict; within a wave, only one\n\t\t// objective per repository may run (write-conflict serialization).\n\t\tconst repoHolders = new Map<string, string>();\n\t\tconst wave: string[] = [];\n\n\t\tfor (const id of candidateIds) {\n\t\t\tif (wave.length >= input.parallelismBound) break;\n\t\t\tconst repos = repoSet(id);\n\t\t\tlet conflict = false;\n\t\t\tfor (const r of repos) {\n\t\t\t\tif (repoHolders.has(r)) {\n\t\t\t\t\tconflict = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (conflict) continue;\n\t\t\tfor (const r of repos) repoHolders.set(r, id);\n\t\t\twave.push(id);\n\t\t\tplanned.add(id);\n\t\t}\n\n\t\tif (wave.length > 0) {\n\t\t\twaves.push(wave);\n\t\t\t// Record write-conflict serialization evidence (group when wave had conflicts deferred).\n\t\t} else {\n\t\t\t// Parallelism/conflict prevented any progress this pass → serialized groups\n\t\t\tfor (const id of candidateIds) {\n\t\t\t\tif (!planned.has(id)) unready.push(id);\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t}\n\n\t// Serialized groups: objectives sharing a repository across waves.\n\t{\n\t\tconst repoWave = new Map<string, number>();\n\t\twaves.forEach((wave, wi) => {\n\t\t\tfor (const id of wave) {\n\t\t\t\tfor (const r of repoSet(id)) {\n\t\t\t\t\tconst prev = repoWave.get(r);\n\t\t\t\t\tif (prev !== undefined && prev !== wi) {\n\t\t\t\t\t\tserializedGroups.push([r, `${prev}->${wi}`]);\n\t\t\t\t\t}\n\t\t\t\t\trepoWave.set(r, wi);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t}\n\n\t// Budget accounting\n\tlet budgetExceeded = false;\n\tif (input.budget && input.enforceBudget && input.budget.spentCost > input.budget.maxCost) {\n\t\tbudgetExceeded = true;\n\t}\n\tbudgetExceededRef.value = budgetExceeded;\n\n\treturn {\n\t\tmissionId: mission.missionId,\n\t\trevision: mission.revision,\n\t\twaves,\n\t\tunready: [...new Set(unready)].sort(),\n\t\tserializedGroups,\n\t\tcriticalPath: computeCriticalPath(mission).path,\n\t\tbudgetExceeded,\n\t};\n}\n\n/**\n * Determine whether an objective is ready to start given current statuses.\n * A dependency counts as satisfied only when its objective is COMPLETED.\n */\nexport function isObjectiveReady(\n\tmission: Pick<MissionGraphDocumentV1, \"objectives\">,\n\tobjectiveId: string,\n\tstatus: Map<string, ObjectiveStatus>,\n): boolean {\n\tconst o = mission.objectives.find((x) => x.id === objectiveId);\n\tif (!o) return false;\n\tconst index = buildDependencyIndex(mission.objectives);\n\tconst deps = (index.dependenciesBy.get(objectiveId) ?? []).filter((d) => objectiveIds(mission).has(d));\n\treturn deps.every((d) => status.get(d) === \"COMPLETED\");\n}\n\nexport function budgetViolations(\n\tobjectives: Pick<MissionGraphDocumentV1, \"objectives\">[\"objectives\"],\n\tmissionBudget?: MissionBudget,\n\tenforceMission = true,\n): string[] {\n\tconst violations: string[] = [];\n\tfor (const o of objectives) {\n\t\tconst b: ObjectiveBudget | undefined = o.budget;\n\t\tif (b && b.spentCost > b.maxCost) {\n\t\t\tviolations.push(`objective ${o.id} spent ${b.spentCost} > budget ${b.maxCost}`);\n\t\t}\n\t}\n\tif (missionBudget && enforceMission && missionBudget.spentCost > missionBudget.maxCost) {\n\t\tviolations.push(`mission spent ${missionBudget.spentCost} > budget ${missionBudget.maxCost}`);\n\t}\n\treturn violations;\n}\n"]}