#!/usr/bin/env node /** * freeze-gate (§5.6/6.1 CD-3) — ОТДЕЛЬНОЕ предусловие ПЕРЕД freezePlan/freezeArtifact. * plan-lock/seal/М6 НЕ трогаются (CD-3): caller сначала зовёт freezeGate, и ТОЛЬКО при * pass:true вызывает freezePlan/freezeArtifact [plan-lock.mjs:62/:194]. * Проверки (порядок A1): (0) binding — verdict.plan_hash === planHash (нах.F4: stale/чужой * содержательный вердикт НЕ пропускает печать нового плана); (а) содержательный * mentor-вердикт (VA-8/SE-R6-6); (б) VA-9 (Д-2а, SE-A3-обязательство из context-verity.mjs): * пустой артефакт / 0 EXTRACTED — НЕ «проверен» → блок; (в) verity — нет неразрешённой * EXTRACTED (✅O2). hasUnresolvedExtractedImpl инъектируется (в проде * artifactHasUnresolvedExtracted из A; полярность: true = ЕСТЬ неразрешённая EXTRACTED = * ГРЯЗНО = блок) → C автономна. fail-CLOSED (SE-3): провал/сбой → блок → owner-escape; * «наставник ОШИБСЯ» (таймаут/краш → RETRY) vs «ЗАБЛОКИРОВАЛ» (legit → escape) — R2-SE-h, * различает caller по reason. */ import { validateMentorVerdict, isMentorVerdictSubstantive } from './mentor-verdict.mjs'; /** * @param {object} args * @param {object} args.mentorVerdict — вердикт наставника (runMentorVerdict) * @param {boolean} args.mentorWired — реальный заход (SE-R6-6) * @param {string|null} args.planHash — planId(замораживаемых steps) [plan-lock.mjs:15-17] * @param {Array} args.verifiedContextArtifact — артефакт проверенного контекста (§5.5) * @param {Function} args.hasUnresolvedExtractedImpl — (artifact)=>boolean; true = ЕСТЬ * неразрешённая EXTRACTED = ГРЯЗНО = блок (зеркало artifactHasUnresolvedExtracted из A) * @returns {{pass: boolean, reason: string}} pass:true только когда ВСЕ условия выполнены. */ export function freezeGate({ mentorVerdict, mentorWired = false, planHash = null, verifiedContextArtifact = [], hasUnresolvedExtractedImpl } = {}) { // (0) binding нах.F4 — ДО substance: «вердикт есть» ≠ «вердикт ДЛЯ ЭТОГО плана». // F-C4 (sharp-edges, ужесточение A1): planHash ОБЯЗАТЕЛЕН — омиссия отключала бы // binding-проверку (configuration cliff); печать без binding запрещена (fail-CLOSED). if (planHash == null || !String(planHash).trim()) { return { pass: false, reason: 'planHash не передан — печать без binding вердикт↔план запрещена (F-C4, fail-CLOSED)' }; } if (!mentorVerdict || mentorVerdict.plan_hash !== planHash) { return { pass: false, reason: 'вердикт не привязан к замораживаемому плану (stale/чужой) — нах.F4' }; } // (а) substance VA-8/SE-R6-6. if (!isMentorVerdictSubstantive(mentorVerdict, { wired: mentorWired })) { const v = validateMentorVerdict(mentorVerdict); return { pass: false, reason: !mentorWired ? 'mentor-вердикт не из реального захода (wired:false) — не суд (SE-R6-6)' : `mentor-вердикт несодержателен: пустые слоты [${v.missingSlots.join(', ')}] (VA-8)` }; } // (б) VA-9 (Д-2а/SE-A3): 0 EXTRACTED = «ничего не проверено» — не пускать. const list = Array.isArray(verifiedContextArtifact) ? verifiedContextArtifact : []; const extractedCount = list.filter((e) => e && e.kind === 'EXTRACTED').length; if (extractedCount === 0) { return { pass: false, reason: 'пустой артефакт контекста (0 EXTRACTED) — НЕ «проверен» (VA-9/SE-A3)' }; } // FR-1 (финревью 2026-06-11, SE-A3-обязательство context-verity:93-94 — В ГЕЙТЕ): // (б2) VF-1 — INFERRED без derivation_ref не «проверен», печать с ним запрещена; // (б3) SE-A1 — неизвестный kind (третья полоса обхода: запись ни резолвится, ни // флагуется) печать не проходит. Чистые data-чеки — автономность C сохранена // (kinds — спека §5.5, зеркало CITATION_KINDS без импорта A). for (const e of list) { if (!e || typeof e !== 'object') { return { pass: false, reason: 'битая запись артефакта контекста — печать отказана (FR-1, fail-CLOSED)' }; } if (e.kind === 'INFERRED' && !String(e.derivation_ref || '').trim()) { return { pass: false, reason: 'INFERRED без derivation_ref в артефакте — печать отказана (VF-1/FR-1)' }; } if (e.kind !== 'EXTRACTED' && e.kind !== 'INFERRED') { return { pass: false, reason: `неизвестный kind ${JSON.stringify(e.kind)} в артефакте — печать отказана (SE-A1/FR-1)` }; } } // (в) verity ✅O2: fail-CLOSED — сбой/отсутствие impl = считаем грязным. let dirty; try { dirty = typeof hasUnresolvedExtractedImpl === 'function' ? !!hasUnresolvedExtractedImpl(list) : true; } catch { dirty = true; } if (dirty) return { pass: false, reason: 'неразрешённая EXTRACTED-цитата в контексте — печать отказана (✅O2)' }; return { pass: true, reason: 'binding ok + mentor-вердикт содержателен + VA-9 ok + verity чист' }; }