397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
74 lines
5.8 KiB
JavaScript
74 lines
5.8 KiB
JavaScript
#!/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 чист' };
|
||
}
|