diff --git a/apps/web/src/lib/__tests__/gate-transition.test.ts b/apps/web/src/lib/__tests__/gate-transition.test.ts index 40cb1d1c6..27ced91a9 100644 --- a/apps/web/src/lib/__tests__/gate-transition.test.ts +++ b/apps/web/src/lib/__tests__/gate-transition.test.ts @@ -112,6 +112,13 @@ describe('G.A.T.E. transition contract', () => { const assessed = assessZeroSim({ evidenceRefs: [] }); expect(assessed.verdict).toBe('unverified'); expect(assessed.reason_code).toBe('ZERO_SIM_MISSING_EVIDENCE'); + + const forcedReal = assessZeroSim({ + result: { verdict: 'real', reason_code: 'ZERO_SIM_REAL' }, + evidenceRefs: [], + }); + expect(forcedReal.verdict).toBe('unverified'); + expect(forcedReal.reason_code).toBe('ZERO_SIM_MISSING_EVIDENCE'); }); it('emits a versioned receipt whose hash matches canonical JSON without the hash field', () => { diff --git a/apps/web/src/lib/gate-transition.ts b/apps/web/src/lib/gate-transition.ts index 3b53d6237..dc6dd5117 100644 --- a/apps/web/src/lib/gate-transition.ts +++ b/apps/web/src/lib/gate-transition.ts @@ -217,10 +217,39 @@ export function assessZeroSim(input: { result?: ZeroSimResult; evidenceRefs?: GateEvidenceRef[]; }): ZeroSimResult { + const refs = input.evidenceRefs ?? []; if (input.result) { - return input.result; + const candidate = input.result; + if (candidate.verdict === 'unreal') { + return candidate; + } + if (candidate.verdict === 'real') { + if (refs.length === 0) { + return { verdict: 'unverified', reason_code: 'ZERO_SIM_MISSING_EVIDENCE' }; + } + for (const ref of refs) { + if (ref.hash && !SHA256_HEX.test(ref.hash)) { + return { verdict: 'unreal', reason_code: 'ZERO_SIM_UNREAL' }; + } + if (ref.kind === 'live_url' && presentedLiveValue(ref) && !liveRefValue(ref)) { + return { verdict: 'unreal', reason_code: 'ZERO_SIM_UNREAL' }; + } + } + return candidate; + } + if (refs.length === 0) { + return { verdict: 'unverified', reason_code: 'ZERO_SIM_MISSING_EVIDENCE' }; + } + for (const ref of refs) { + if (ref.hash && !SHA256_HEX.test(ref.hash)) { + return { verdict: 'unreal', reason_code: 'ZERO_SIM_UNREAL' }; + } + if (ref.kind === 'live_url' && presentedLiveValue(ref) && !liveRefValue(ref)) { + return { verdict: 'unreal', reason_code: 'ZERO_SIM_UNREAL' }; + } + } + return { verdict: 'unverified', reason_code: 'ZERO_SIM_UNVERIFIED' }; } - const refs = input.evidenceRefs ?? []; if (refs.length === 0) { return { verdict: 'unverified', reason_code: 'ZERO_SIM_MISSING_EVIDENCE' }; } @@ -268,10 +297,11 @@ function finish( } export function evaluateTransition(request: GateTransitionRequest): GateEvaluation { - const transitionId = request.transitionId.trim(); - const kind = request.kind.trim(); - const fromState = request.fromState.trim(); - const toState = request.toState.trim(); + const transitionId = (request.transitionId ?? '').trim(); + const kind = (request.kind ?? '').trim(); + const fromState = (request.fromState ?? '').trim(); + const toState = (request.toState ?? '').trim(); + const actor = request.authority?.actor ?? ''; if (!transitionId || !kind || !fromState || !toState) { return finish( request, @@ -282,12 +312,12 @@ export function evaluateTransition(request: GateTransitionRequest): GateEvaluati ); } - if (!KNOWN_AUTHORITY_ACTORS.has(request.authority.actor)) { + if (!actor || !KNOWN_AUTHORITY_ACTORS.has(actor)) { return finish( request, 'ESCALATE', 'GATE_ESCALATE_AUTHORITY_UNKNOWN', - `Authority actor "${request.authority.actor}" is not a known G.A.T.E. actor.`, + `Authority actor "${actor || 'unknown'}" is not a known G.A.T.E. actor.`, request.zeroSim ?? { verdict: 'unverified', reason_code: 'ZERO_SIM_UNVERIFIED' }, ); }