// SPEC: PJ2026-01060703 CI/CD branch follower controller scheduling. // Responsibility: keep source discovery and active closeout ahead of unrelated followers. import type { FollowerSpec, FollowerState, ParsedOptions } from "./cicd-types"; const ACTIVE_PHASES = new Set(["PendingTrigger", "Triggering", "Superseded"]); const CLOSEOUT_PHASES = new Set(["ClosingOut"]); const SOURCE_DISCOVERY_PHASES = new Set(["Observed", "Noop", "Succeeded", "Failed", "Blocked", "Skipped"]); export function orderFollowersForControllerCloseout( followers: FollowerSpec[], stateByFollower: Record>, ): FollowerSpec[] { return followers .map((follower, index) => ({ follower, index, priority: followerPriority(stateByFollower[follower.id]) })) .sort((left, right) => left.priority - right.priority || left.index - right.index) .map((item) => item.follower); } export function shouldYieldAfterAutomaticTrigger(options: ParsedOptions, state: FollowerState): boolean { if (!options.inCluster || !options.confirm || options.wait || options.dryRun) return false; return state.phase === "Triggering" && state.inFlightJob !== null; } function followerPriority(state: Record | undefined): number { if (state === undefined) return 1; const phase = typeof state.phase === "string" ? state.phase : null; if (typeof state.inFlightJob === "string" && state.inFlightJob.trim() !== "") return 0; if (phase !== null && ACTIVE_PHASES.has(phase)) return 0; if (isSourceDiscoveryCandidate(phase)) return 1; if (hasStoredSourceTargetMismatch(state)) return 2; if (hasUnfinishedObservedSource(state)) return 2; if (phase !== null && CLOSEOUT_PHASES.has(phase)) return 2; return 3; } function isSourceDiscoveryCandidate(phase: string | null): boolean { return phase === null || SOURCE_DISCOVERY_PHASES.has(phase); } function hasStoredSourceTargetMismatch(state: Record): boolean { const observedSha = nestedString(state, "source", "observedSha"); const targetSha = nestedString(state, "target", "targetSha"); return observedSha !== null && targetSha !== null && observedSha !== targetSha; } function hasUnfinishedObservedSource(state: Record): boolean { const observedSha = nestedString(state, "source", "observedSha"); const lastSucceededSha = typeof state.lastSucceededSha === "string" ? state.lastSucceededSha : null; if (observedSha === null) return false; return lastSucceededSha === null || lastSucceededSha !== observedSha; } function nestedString(source: Record, parentKey: string, childKey: string): string | null { const parent = source[parentKey]; if (parent === null || typeof parent !== "object" || Array.isArray(parent)) return null; const value = (parent as Record)[childKey]; return typeof value === "string" && value.trim() !== "" ? value : null; }