export type TransitionAction =
| "init"
| "start_stage"
| "complete_stage"
| "fail_stage"
| "submit_design"
| "start_phase"
| "submit_for_verify"
| "complete_phase"
| "fail_phase"
| "record_artifact_hash"
| "rollback_to_stage";
export type StageStatus = "pending" | "in_progress" | "completed" | "failed";
export type PhaseStatus =
| "pending"
| "in_progress"
| "awaiting_verify"
| "in_debug"
| "verified"
| "blocked";
export type PhaseEntry = {
status: PhaseStatus;
cycles: number;
failure_category?: string;
failing_module_boundary?: string | null;
last_error?: string;
verified_at?: string;
};
export type Stage5Phases = {
active_phase: string | null;
phase_status: Record<string, PhaseEntry>;
max_cycles_per_phase: number;
};
export type RollbackEntry = {
from_stage: number;
from_phase?: string;
to_stage: number;
reason: string;
failure_category?: string;
timestamp: string;
};
export type ArtifactHashes = Record<string, string>;
export type OrchestratorState = {
operator_name?: string;
schema_version?: string;
max_stage: number;
current_stage: number;
stage_status: Record<string, StageStatus | string>;
stage_retry_count: Record<string, number>;
stage5_phases?: Stage5Phases;
artifact_hashes?: ArtifactHashes;
rollback_history?: RollbackEntry[];
stage8_iteration?: {
count: number;
last_improvement: number;
consecutive_no_improvement: number;
};
last_updated?: string;
[key: string]: unknown;
};
export type TransitionInput =
| { action: "init"; stage?: number; max_stage?: number }
| { action: "start_stage"; stage: number; reason?: string }
| { action: "complete_stage"; stage: number }
| { action: "fail_stage"; stage: number; reason?: string }
| { action: "submit_design"; stage: 4 }
| { action: "start_phase"; stage: 5; phase: string }
| { action: "submit_for_verify"; stage: 5; phase: string }
| { action: "complete_phase"; stage: 5; phase: string }
| {
action: "fail_phase";
stage: 5;
phase: string;
failure_category: string;
failing_module_boundary?: string | null;
last_error?: string;
}
| { action: "record_artifact_hash"; name: string; hash: string }
| {
action: "rollback_to_stage";
target_stage: number;
reason: string;
failure_category?: string;
failed_phase?: string;
};
const DEFAULT_MAX_STAGE = 7;
function cloneState(prev: OrchestratorState): OrchestratorState {
return JSON.parse(JSON.stringify(prev));
}
function ensureStageNumber(val: unknown, max: number): number {
const n = Number(val);
if (!Number.isFinite(n) || n < 1 || n > max) {
throw new Error(`invalid stage: ${val} (must be 1..${max})`);
}
return Math.floor(n);
}
function emptyStageStatus(maxStage: number): Record<string, StageStatus> {
const out: Record<string, StageStatus> = {};
for (let i = 1; i <= maxStage; i++) out[String(i)] = "pending";
return out;
}
function emptyRetryCount(maxStage: number): Record<string, number> {
const out: Record<string, number> = {};
for (let i = 1; i <= maxStage; i++) out[String(i)] = 0;
return out;
}
function emptyStage5Phases(): Stage5Phases {
return {
active_phase: null,
phase_status: {},
max_cycles_per_phase: 10,
};
}
export function applyTransition(
prev: OrchestratorState,
input: TransitionInput,
): OrchestratorState {
const next = cloneState(prev);
const maxStage = next.max_stage ?? DEFAULT_MAX_STAGE;
next.max_stage = maxStage;
if (!next.stage_status) next.stage_status = emptyStageStatus(maxStage);
if (!next.stage_retry_count) next.stage_retry_count = emptyRetryCount(maxStage);
const statusMap = next.stage_status;
const retryMap = next.stage_retry_count;
switch (input.action) {
case "init": {
const stage = ensureStageNumber(input.stage ?? 1, maxStage);
if (stage !== 1) {
throw new Error(`init action must target stage 1, got stage ${stage}`);
}
const hasInProgress = Object.values(statusMap).some((s) => s === "in_progress");
if (hasInProgress) {
throw new Error(`cannot init: a stage is already in_progress`);
}
next.current_stage = stage;
statusMap[String(stage)] = "in_progress";
next.schema_version = next.schema_version ?? "2.0";
break;
}
case "start_stage": {
const stage = ensureStageNumber(input.stage, maxStage);
const otherInProgress = Object.entries(statusMap).some(
([k, s]) => s === "in_progress" && k !== String(stage),
);
if (otherInProgress) {
throw new Error(`cannot start stage ${stage}: another stage is already in_progress`);
}
const key = String(stage);
if (statusMap[key] === "completed") {
throw new Error(`cannot start stage ${stage}: already completed`);
}
if (stage > 1) {
const prevKey = String(stage - 1);
const prevStatus = statusMap[prevKey];
if (prevStatus !== "completed") {
throw new Error(
`cannot start stage ${stage}: previous stage ${stage - 1} is "${prevStatus ?? "unknown"}", not "completed"`,
);
}
}
next.current_stage = stage;
statusMap[key] = "in_progress";
break;
}
case "complete_stage": {
const stage = ensureStageNumber(input.stage, maxStage);
if (stage !== prev.current_stage) {
throw new Error(
`cannot complete stage ${stage}: current_stage is ${prev.current_stage}`,
);
}
const compKey = String(stage);
if (statusMap[compKey] !== "in_progress") {
throw new Error(
`cannot complete stage ${stage}: status is "${statusMap[compKey]}", not "in_progress"`,
);
}
if (stage === 5 && next.stage5_phases) {
const phases = next.stage5_phases.phase_status;
for (const [pname, entry] of Object.entries(phases)) {
if (entry.status !== "verified") {
throw new Error(
`cannot complete stage 5: phase ${pname} status is "${entry.status}", expected "verified"`,
);
}
}
}
statusMap[compKey] = "completed";
const nextStage = stage + 1;
const nextKey = String(nextStage);
if (nextKey in statusMap) {
next.current_stage = nextStage;
statusMap[nextKey] = "in_progress";
}
break;
}
case "fail_stage": {
const stage = ensureStageNumber(input.stage, maxStage);
const failKey = String(stage);
retryMap[failKey] = (Number(retryMap[failKey]) || 0) + 1;
statusMap[failKey] = "failed";
break;
}
case "start_phase": {
if (input.stage !== 5) {
throw new Error(`start_phase only valid for stage 5, got stage ${input.stage}`);
}
if (statusMap["5"] !== "in_progress") {
throw new Error(`cannot start a phase: stage 5 is not in_progress`);
}
if (!next.stage5_phases) next.stage5_phases = emptyStage5Phases();
const phases = next.stage5_phases.phase_status;
const otherActive = Object.entries(phases).find(
([k, e]) => e.status === "in_progress" && k !== input.phase,
);
if (otherActive) {
throw new Error(
`cannot start phase ${input.phase}: phase ${otherActive[0]} is already in_progress`,
);
}
const existing = phases[input.phase];
if (existing && existing.status === "verified") {
throw new Error(`cannot start phase ${input.phase}: already verified`);
}
phases[input.phase] = existing
? { ...existing, status: "in_progress" }
: { status: "in_progress", cycles: 0 };
next.stage5_phases.active_phase = input.phase;
break;
}
case "submit_design": {
if (input.stage !== 4) {
throw new Error(`submit_design only valid for stage 4, got stage ${input.stage}`);
}
const stageKey = String(input.stage);
const currentStatus = next.stage_status[stageKey];
if (currentStatus !== "in_progress") {
throw new Error(
`cannot submit_design at stage 4: stage_status is "${currentStatus}" ` +
`(expected "in_progress"; call start_stage(stage=4) first)`,
);
}
break;
}
case "submit_for_verify": {
if (input.stage !== 5) {
throw new Error(`submit_for_verify only valid for stage 5, got stage ${input.stage}`);
}
if (!next.stage5_phases) {
throw new Error(`cannot submit phase for verify: stage5_phases not initialised`);
}
const phases = next.stage5_phases.phase_status;
const entry = phases[input.phase];
if (!entry) {
throw new Error(`cannot submit phase ${input.phase} for verify: not started`);
}
if (
entry.status !== "in_progress" &&
entry.status !== "in_debug"
) {
throw new Error(
`cannot submit phase ${input.phase} for verify: status is "${entry.status}" ` +
`(expected "in_progress" or "in_debug")`,
);
}
phases[input.phase] = { ...entry, status: "awaiting_verify" };
next.stage5_phases.active_phase = input.phase;
break;
}
case "complete_phase": {
if (input.stage !== 5) {
throw new Error(`complete_phase only valid for stage 5, got stage ${input.stage}`);
}
if (!next.stage5_phases) {
throw new Error(`cannot complete phase: stage5_phases not initialised`);
}
const phases = next.stage5_phases.phase_status;
const entry = phases[input.phase];
if (!entry) {
throw new Error(`cannot complete phase ${input.phase}: not started`);
}
if (
entry.status !== "in_progress" &&
entry.status !== "in_debug" &&
entry.status !== "awaiting_verify"
) {
throw new Error(
`cannot complete phase ${input.phase}: status is "${entry.status}"`,
);
}
phases[input.phase] = {
...entry,
status: "verified",
verified_at: new Date().toISOString(),
};
if (next.stage5_phases.active_phase === input.phase) {
next.stage5_phases.active_phase = null;
}
break;
}
case "fail_phase": {
if (input.stage !== 5) {
throw new Error(`fail_phase only valid for stage 5, got stage ${input.stage}`);
}
if (!next.stage5_phases) next.stage5_phases = emptyStage5Phases();
const phases = next.stage5_phases.phase_status;
const entry = phases[input.phase] ?? { status: "in_progress" as PhaseStatus, cycles: 0 };
const cycles = entry.cycles + 1;
const blocked = cycles >= next.stage5_phases.max_cycles_per_phase;
phases[input.phase] = {
...entry,
status: blocked ? "blocked" : "in_debug",
cycles,
failure_category: input.failure_category,
failing_module_boundary: input.failing_module_boundary ?? null,
last_error: input.last_error,
};
next.stage5_phases.active_phase = input.phase;
break;
}
case "record_artifact_hash": {
if (!input.name) throw new Error(`record_artifact_hash requires name`);
if (!input.hash) throw new Error(`record_artifact_hash requires hash`);
if (!next.artifact_hashes) next.artifact_hashes = {};
next.artifact_hashes[input.name] = input.hash;
break;
}
case "rollback_to_stage": {
const target = ensureStageNumber(input.target_stage, maxStage);
if (target >= prev.current_stage) {
throw new Error(
`rollback target stage ${target} must be strictly less than current_stage ${prev.current_stage}`,
);
}
if (!input.reason || !input.reason.trim()) {
throw new Error(`rollback_to_stage requires a non-empty reason`);
}
for (const key of Object.keys(statusMap)) {
const k = Number(key);
if (k > target) statusMap[key] = "pending";
}
retryMap[String(target)] = (Number(retryMap[String(target)]) || 0) + 1;
statusMap[String(target)] = "in_progress";
next.current_stage = target;
if (target < 5) {
next.stage5_phases = emptyStage5Phases();
}
const HASH_STAGE: Record<string, number> = {
spec_md: 1,
api_report_md: 1,
golden_py: 2,
design_md: 3,
module_interfaces_yaml: 4,
};
if (next.artifact_hashes) {
for (const [name, owningStage] of Object.entries(HASH_STAGE)) {
if (owningStage > target && name in next.artifact_hashes) {
delete next.artifact_hashes[name];
}
}
}
if (!next.rollback_history) next.rollback_history = [];
next.rollback_history.push({
from_stage: prev.current_stage,
from_phase: input.failed_phase,
to_stage: target,
reason: input.reason,
failure_category: input.failure_category,
timestamp: new Date().toISOString(),
});
break;
}
default: {
const a = (input as { action?: string }).action ?? "unknown";
throw new Error(`unsupported action: ${a}`);
}
}
next.last_updated = new Date().toISOString();
return next;
}