148 lines
4.9 KiB
TypeScript
148 lines
4.9 KiB
TypeScript
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<string, unknown>)['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,
|
|
};
|
|
}
|