import type { GateEntry } from '@mosaicstack/macp'; import type { AuthorityGate, ForgeGate, ForgeGateResult, ForgeOutcome, ForgeTaskResult, ProviderGate, } from './types.js'; /** * Gate and dependency satisfaction predicate (SDLC-D-035). * * ONLY a verified `passed` outcome satisfies. Every other member of the closed * outcome set — including `simulated` — is non-satisfying, so a simulated or * authority-blocked result can never be read as success-by-verification. */ export function isSatisfyingOutcome(outcome: ForgeOutcome): boolean { return outcome === 'passed'; } /** Whether a gate is an authority or provider gate (capability-based, command-less). */ export function isCapabilityGate(gate: ForgeGate): gate is AuthorityGate | ProviderGate { if (typeof gate !== 'object' || gate === null) return false; const kind = (gate as Record)['kind']; return kind === 'authority' || kind === 'provider'; } /** Whether a gate definition carries a real command a mechanical runner can execute. */ export function isCommandGate(gate: ForgeGate): gate is string | GateEntry { if (typeof gate === 'string') { return gate.trim().length > 0; } if (isCapabilityGate(gate)) { // Authority and provider gates are satisfied by a capability, not a command. return false; } return typeof gate.command === 'string' && gate.command.trim().length > 0; } /** Typed label identifying a gate in results and logs. */ export function gateLabel(gate: ForgeGate): string { if (typeof gate === 'string') return gate; if (isCapabilityGate(gate)) return `${gate.kind}:${gate.capability}`; return gate.command || gate.type || 'unnamed-gate'; } /** Reason string stamped on every simulated gate result. */ export const SIMULATED_GATE_REASON = 'simulated execution (--simulate): gate was not evaluated by a real implementation'; /** Build typed gate results with a uniform outcome for a stage's declared gates. */ export function uniformGateResults( gates: ForgeGate[], outcome: ForgeOutcome, reason: string, ): ForgeGateResult[] { return gates.map((gate) => ({ gate: gateLabel(gate), outcome, reason })); } /** Typed simulated gate results — used exclusively in `--simulate` runs. */ export function simulatedGateResults(gates: ForgeGate[]): ForgeGateResult[] { return uniformGateResults(gates, 'simulated', SIMULATED_GATE_REASON); } /** Typed waiting-for-authority gate results for approval-based stages. */ export function waitingGateResults(gates: ForgeGate[], reason: string): ForgeGateResult[] { return uniformGateResults(gates, 'waiting-for-authority', reason); } /** Typed blocked gate results for stages whose provider capability is not wired. */ export function blockedGateResults(gates: ForgeGate[], reason: string): ForgeGateResult[] { return uniformGateResults(gates, 'blocked', reason); } /** Outcome of evaluating a completed stage in normal mode. */ export interface StageEvaluation { outcome: ForgeOutcome; reason: string; gateResults: ForgeGateResult[]; } /** * Evaluate a stage's declared gates against the executor's typed result. * * Fail-closed mapping: * - a `simulated` task or gate outcome in normal mode maps to `error` * - a missing gate result for a required command gate maps to `blocked` * - a non-passing task outcome propagates as the stage outcome * - only verified `passed` task and gate outcomes yield a `passed` stage */ export function evaluateStageGates( stageName: string, gates: ForgeGate[], result: ForgeTaskResult, ): StageEvaluation { const gateResults = result.gate_results ?? []; if (result.outcome === 'simulated') { return { outcome: 'error', reason: `executor reported a simulated outcome for stage '${stageName}' in normal mode — refusing to treat simulated results as verified`, gateResults, }; } if (!isSatisfyingOutcome(result.outcome)) { return { outcome: result.outcome, reason: `task outcome is '${result.outcome}': ${result.reason}`, gateResults, }; } for (const gate of gates) { // Authority and provider gates are pre-flighted before execution; they have // no mechanical result to verify here. if (!isCommandGate(gate)) continue; const label = gateLabel(gate); const gateResult = gateResults.find((r) => r.gate === label); if (!gateResult) { return { outcome: 'blocked', reason: `no gate result was reported for required gate '${label}' (stage '${stageName}')`, gateResults, }; } if (!isSatisfyingOutcome(gateResult.outcome)) { return { outcome: gateResult.outcome === 'simulated' ? 'error' : gateResult.outcome, reason: `gate '${label}' outcome is '${gateResult.outcome}': ${gateResult.reason}`, gateResults, }; } } return { outcome: 'passed', reason: gates.length === 0 ? "stage declares no gates; task outcome 'passed' accepted" : 'all declared gates verified passed', gateResults, }; }