Files
brain/tools/freeze-gate.mjs
T

74 lines
5.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 чист' };
}