const MODES = ['initial', 'correction1', 'review1', 'correction2', 'review2']; const EVENT_TYPES = new Set(['start_reserved', 'spawn_entered', 'spawn_accepted', 'completion_prepared', 'spawn_returned', 'review_outcome']); const IDENTIFIER = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,79}$/; const PLAN = /^plan-\d{1,40}$/; const GATES = new Set(['critic', 'code-review', 'security', 'qa']); const VERDICTS = { critic: { APPROVED: 'APPROVED', APPROVED_WITH_COMMENTS: 'APPROVED', REJECTED: 'CHANGES_REQUESTED' }, 'code-review': { APPROVED: 'APPROVED', APPROVED_WITH_COMMENTS: 'APPROVED', REJECTED: 'CHANGES_REQUESTED' }, security: { APPROVED: 'APPROVED', APPROVED_WITH_CONTROLS: 'CHANGES_REQUESTED', REJECTED: 'CHANGES_REQUESTED' }, qa: { COMPLETE: 'APPROVED', FAILED: 'CHANGES_REQUESTED' }, }; const CANONICAL_OUTCOMES = new Set(['APPROVED', 'accepted', 'CHANGES_REQUESTED', 'READY_FOR_REVIEW', 'SUBMITTED', 'closed', 'USER_DECISION_REQUIRED']); export function normalizeReviewPlan(value) { const raw = String(value ?? '').trim(); const digits = raw.match(/^(?:plan-)?(\d{1,40})(?:[-_].*)?$/i)?.[1]; return digits ? `plan-${digits.length <= 3 ? digits.padStart(3, '0') : digits}` : null; } function identityFrom(value) { if (!value || typeof value !== 'object' || Array.isArray(value)) return null; const { runFamilyId, planId, reviewGate, reviewMode, attemptId } = value; if (!IDENTIFIER.test(String(runFamilyId || '')) || !PLAN.test(String(planId || '')) || !GATES.has(reviewGate) || !MODES.includes(reviewMode) || !IDENTIFIER.test(String(attemptId || ''))) return null; return { runFamilyId, planId, reviewGate, reviewMode, attemptId }; } export function validateReviewIdentity(value) { const identity = identityFrom(value); return identity ? { ok: true, value } : { ok: false, code: 'REVIEW_IDENTITY_INVALID' }; } export function reviewAgentMatches(agent, identity) { if (!validateReviewIdentity(identity).ok) return false; if (identity.reviewMode.startsWith('correction')) return agent === (identity.reviewGate === 'critic' ? 'pidex-planner' : 'pidex-implementer'); return agent === ({ critic: 'pidex-critic', 'code-review': 'pidex-code-reviewer', security: 'pidex-security', qa: 'pidex-qa' })[identity.reviewGate]; } function sameIdentity(left, right) { return left.runFamilyId === right.runFamilyId && left.planId === right.planId && left.reviewGate === right.reviewGate && left.reviewMode === right.reviewMode && left.attemptId === right.attemptId; } function sameFamily(left, right) { return left.runFamilyId === right.runFamilyId && left.planId === right.planId && left.reviewGate === right.reviewGate; } export function normalizeReviewVerdict(gate, verdict) { if (!GATES.has(gate) || typeof verdict !== 'string') return null; return VERDICTS[gate]?.[verdict] || null; } function outcomeOf(metadata, gate) { if (!metadata || typeof metadata !== 'object') return null; if (typeof metadata.outcome === 'string' && !('verdict' in metadata)) return CANONICAL_OUTCOMES.has(metadata.outcome) ? metadata.outcome : null; if (typeof metadata.verdict === 'string' && !('outcome' in metadata)) return normalizeReviewVerdict(gate, metadata.verdict); return null; } function nextAfter(mode, outcome) { if (['APPROVED', 'accepted'].includes(outcome) && ['initial', 'review1', 'review2'].includes(mode)) return { terminal: 'accepted' }; if (outcome === 'closed' && mode === 'review2') return { terminal: 'closed' }; if (outcome === 'CHANGES_REQUESTED' && mode === 'initial') return { nextMode: 'correction1' }; if (outcome === 'CHANGES_REQUESTED' && mode === 'review1') return { nextMode: 'correction2' }; if (['READY_FOR_REVIEW', 'SUBMITTED'].includes(outcome) && mode === 'correction1') return { nextMode: 'review1' }; if (['READY_FOR_REVIEW', 'SUBMITTED'].includes(outcome) && mode === 'correction2') return { nextMode: 'review2' }; return null; } function denied() { return { status: 'denied', code: 'REVIEW_HISTORY_INVALID' }; } export function allowedCompletionOutcome(identity, outcome) { if (!validateReviewIdentity(identity).ok || typeof outcome !== 'string') return false; const normalized = CANONICAL_OUTCOMES.has(outcome) ? outcome : normalizeReviewVerdict(identity.reviewGate, outcome); return normalized === 'CHANGES_REQUESTED' && identity.reviewMode === 'review2' || normalized === 'USER_DECISION_REQUIRED' && !identity.reviewMode.startsWith('correction') || Boolean(nextAfter(identity.reviewMode, normalized)); } export function foldReviewHistory(rows, requested) { if (!validateReviewIdentity(requested).ok || !Array.isArray(rows)) return denied(); const reviewRows = []; for (const row of rows) { if (!row || typeof row !== 'object' || Array.isArray(row) || !row.metadata || row.metadata.planId !== requested.planId || row.metadata.reviewGate !== requested.reviewGate) continue; if (!EVENT_TYPES.has(row.event_type)) return denied(); const identity = identityFrom(row.metadata); if (!identity) return denied(); reviewRows.push({ event_type: row.event_type, metadata: row.metadata, identity }); } if (!reviewRows.length) return requested.reviewMode === 'initial' ? { status: 'allowed', nextMode: 'initial' } : denied(); if (reviewRows.length === 1 && reviewRows[0].event_type === 'review_outcome' && sameIdentity(reviewRows[0].identity, requested)) { const terminal = nextAfter(requested.reviewMode, outcomeOf(reviewRows[0].metadata, requested.reviewGate)); if (terminal?.terminal) return { status: 'terminal', terminal: terminal.terminal }; } for (let left = 0; left < reviewRows.length; left++) { for (let right = left + 1; right < reviewRows.length; right++) { if (reviewRows[left].identity.reviewMode === reviewRows[right].identity.reviewMode && reviewRows[left].identity.attemptId !== reviewRows[right].identity.attemptId) return denied(); } } let expectedMode = 'initial'; let index = 0; while (index < reviewRows.length) { const first = reviewRows[index]; if (first.identity.reviewMode !== expectedMode) return denied(); const active = first.identity; const events = []; while (index < reviewRows.length && sameIdentity(reviewRows[index].identity, active)) { events.push(reviewRows[index++]); } const canonical = []; for (const event of events) { const prior = canonical.find((item) => item.event_type === event.event_type); if (prior) { if (JSON.stringify(prior.metadata) !== JSON.stringify(event.metadata)) return denied(); } else canonical.push(event); } const types = canonical.map((event) => event.event_type); // Plan 059 Slice 2 (AD-1): dual sequence selected by the discriminator at index 3. // types[3] === 'completion_prepared' is the new uniform receipt sequence // (receipt after spawn_accepted, before spawn_returned). Legacy histories keep // types[3] === 'spawn_returned' with unchanged five-event behavior and four-event // returned uncertainty. No sidecar receipt store; the receipt is an event row. const newSequence = types[3] === 'completion_prepared'; const expectedPrefix = newSequence ? ['start_reserved', 'spawn_entered', 'spawn_accepted', 'completion_prepared', 'spawn_returned', 'review_outcome'] : ['start_reserved', 'spawn_entered', 'spawn_accepted', 'spawn_returned', 'review_outcome']; if (types.length > expectedPrefix.length || types.some((type, position) => type !== expectedPrefix[position])) return denied(); if (types.length === 1) return sameIdentity(active, requested) ? { status: 'resume_reserved', nextMode: expectedMode } : denied(); if (types.length === 2) return { status: 'uncertain', code: 'SPAWN_ENTERED_UNCERTAIN' }; if (types.length === 3) return sameIdentity(active, requested) ? { status: 'spawn_accepted', nextMode: expectedMode } : denied(); if (types.length === 4) { // Prepared-only: receipt present, spawn_returned missing. Resumable under the // exact same receipt (identity + digests + intended outcome + TBR IDs). if (newSequence) return sameIdentity(active, requested) ? { status: 'prepared', nextMode: expectedMode } : denied(); return { status: 'uncertain', code: 'SPAWN_RETURNED_UNCERTAIN' }; } if (types.length === 5) { // Prepared + returned, outcome missing: still resumable under the same receipt. if (newSequence) return sameIdentity(active, requested) ? { status: 'prepared', nextMode: expectedMode } : denied(); const next = nextAfter(active.reviewMode, outcomeOf(canonical[4].metadata, active.reviewGate)); if (!next) return denied(); if (next.terminal) { if (index !== reviewRows.length) return denied(); return { status: 'terminal', terminal: next.terminal }; } expectedMode = next.nextMode; continue; } // New six-event sequence: terminal receipt/outcome agreement is required. const outcome = outcomeOf(canonical[5].metadata, active.reviewGate); if (canonical[3].metadata.intendedOutcome !== outcome) return denied(); if (outcome === 'USER_DECISION_REQUIRED') return { status: 'expansion_pending' }; const next = nextAfter(active.reviewMode, outcome); if (!next) return denied(); if (next.terminal) { if (index !== reviewRows.length) return denied(); return { status: 'terminal', terminal: next.terminal }; } expectedMode = next.nextMode; } return requested.reviewMode === expectedMode ? { status: 'allowed', nextMode: expectedMode } : denied(); }