397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
149 lines
10 KiB
JavaScript
149 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* judge-gate-floor (§3.1/§3.2/§3.5, K2/K5, F7) — механический пол гейтов ($0, не ИИ).
|
|
* Бесплатные проверки «было/не было», на которых стоит думающая часть судьи:
|
|
* - existenceCheck (Гейт-1 шаг 1): все вопросы отвечены + обещанные навыки реально
|
|
* вызваны по журналу (неподделываемый канал), не по тексту.
|
|
* - specToPlanCoverage (Гейт-2): каждый § запечатанной спеки покрыт ≥1 шагом плана.
|
|
* - k5CriterionCheck (Гейт-2, жемчужина K5): значимый шаг объявляет КОНКРЕТНЫЙ критерий;
|
|
* голое «проверено/готово» за факт НЕ берём; значимость осторожна (сомнение → значим).
|
|
* - criteriaGreenMatched (Гейт-3): каждый критерий сопоставлен с настоящим ЗЕЛЁНЫМ прогоном.
|
|
* - skillTakenByJournal (A2, K2): навык взят по журналу вызовов, не по тексту TodoWrite.
|
|
* Тупо-механика: уболтать нельзя. Качество — думающая часть 4-D + владелец.
|
|
*/
|
|
import { classifyDestructive } from './classify-destructive.mjs';
|
|
import { verifyGreen } from './floor-signer.mjs';
|
|
|
|
/** Обещанные навыки реально вызваны (по журналу)? Общий помощник для Гейта-1 и A2. */
|
|
function skillsCoveredByJournal(promised = [], invoked = []) {
|
|
const seen = new Set(invoked);
|
|
return (promised || []).filter((s) => !seen.has(s));
|
|
}
|
|
|
|
/** Гейт-1 шаг 1: вопросы отвечены + обещанные навыки в журнале. */
|
|
export function existenceCheck({ questions = [], skillsPromised = [], skillsInvoked = [] }) {
|
|
const unanswered = (questions || []).filter((q) => !q.answered).map((q) => q.id);
|
|
const missingSkills = skillsCoveredByJournal(skillsPromised, skillsInvoked);
|
|
return { ok: unanswered.length === 0 && missingSkills.length === 0, unanswered, missingSkills };
|
|
}
|
|
|
|
/** Гейт-2 пол: каждый § спеки покрыт хотя бы одним шагом плана (по step.ref). */
|
|
export function specToPlanCoverage({ specSections = [], planSteps = [] }) {
|
|
const covered = new Set((planSteps || []).map((s) => s && s.ref).filter(Boolean));
|
|
const uncovered = (specSections || []).filter((sec) => !covered.has(sec));
|
|
return { ok: uncovered.length === 0, uncovered };
|
|
}
|
|
|
|
// Голое done-слово (не критерий). Кириллические варианты — подстрокой/якорем строки.
|
|
const BARE_DONE_RE = /^(проверено|проверен[аоы]|сделано|готово|ок|done|passed|pass|ready)\.?$/i;
|
|
const MIN_CRITERION_LEN = 6;
|
|
|
|
// Чисто-читающие инструменты: не мутируют состояние → шаг не значим, критерий не нужен.
|
|
const READONLY_OPS = new Set(['Read', 'Grep', 'Glob', 'LS', 'NotebookRead']);
|
|
|
|
/**
|
|
* 5.2 (C3/F18): МЕХАНИЧЕСКАЯ значимость шага — НЕ из контроллер-флагов. Значим, ЕСЛИ:
|
|
* - object разрушителен (classifyDestructive.suspicious) — перевешивает любой op (анти-обход
|
|
* «op:Read, object: rm -rf»); ИЛИ
|
|
* - op НЕ из чисто-читающего набора (мутирующий / неизвестный / пустой op → сомнение → значим).
|
|
* Контроллер-флаги trivial/significant ИГНОРИРУЮТСЯ: их подделка больше не разжалует значимый шаг.
|
|
*/
|
|
export function isSignificantStep(step) {
|
|
if (!step || typeof step !== 'object') return true; // сомнение → значим (fail-CLOSE)
|
|
const object = String(step.object ?? step.command ?? '');
|
|
if (classifyDestructive(object).suspicious) return true; // разрушительное → всегда значимо
|
|
const op = String(step.op || '');
|
|
if (READONLY_OPS.has(op)) return false; // чистое чтение — критерий не нужен
|
|
return true; // мутирующий / неизвестный op → значим
|
|
}
|
|
|
|
/** Гейт-2 K5: значимый шаг → конкретный проверяемый критерий; «проверено» не факт.
|
|
* Значимость — механическая (isSignificantStep), флаги контроллера не учитываются (C3/F18). */
|
|
export function k5CriterionCheck({ planSteps = [] }) {
|
|
const missingCriterion = [];
|
|
for (const s of planSteps || []) {
|
|
if (!isSignificantStep(s)) continue; // механически не значим (чистое чтение)
|
|
const c = s && typeof s.criterion === 'string' ? s.criterion.trim() : '';
|
|
const concrete = c.length >= MIN_CRITERION_LEN && !BARE_DONE_RE.test(c);
|
|
if (!concrete) missingCriterion.push(s.n);
|
|
}
|
|
return { ok: missingCriterion.length === 0, missingCriterion };
|
|
}
|
|
|
|
/** Гейт-3 пол: каждый объявленный критерий сопоставлен с настоящим ЗЕЛЁНЫМ прогоном.
|
|
* Это «green-присутствие» (Δ6 шаг 2): свежесть отпечатка и подпись — отдельные шаги. */
|
|
export function criteriaGreenMatched({ criteria = [], greenRuns = [] }) {
|
|
const greenIds = new Set((greenRuns || []).filter((r) => r && r.green === true).map((r) => r.criterion_id));
|
|
const unproven = (criteria || []).filter((c) => !greenIds.has(c.id)).map((c) => c.id);
|
|
return { ok: unproven.length === 0, unproven };
|
|
}
|
|
|
|
/**
|
|
* 5.3 (Δ2, Δ6 шаг 3): свежесть отпечатка. Зелёный прогон засчитывается только если его
|
|
* code_fingerprint совпадает с ТЕКУЩИМ (изменённые файлы шага + тест-файлы; считается живым
|
|
* гейтом, инъектируется как currentFingerprints[criterion_id]). Правка файла после прогона
|
|
* меняет текущий отпечаток → расхождение → green аннулирован (stale). Чистая функция.
|
|
* Конструктивно fail-CLOSE: нет текущего отпечатка (undefined) ≠ записанному → stale.
|
|
* Красные прогоны не проверяются (их «не-зелёность» ловит criteriaGreenMatched).
|
|
*/
|
|
export function fingerprintFresh({ greenRuns = [], currentFingerprints = {} }) {
|
|
const stale = (greenRuns || [])
|
|
.filter((r) => r && r.green === true)
|
|
.filter((r) => r.code_fingerprint !== currentFingerprints[r.criterion_id])
|
|
.map((r) => r.criterion_id);
|
|
return { ok: stale.length === 0, stale };
|
|
}
|
|
|
|
/**
|
|
* 5.4 (Δ5, Δ6 шаг 4): подлинность green — подпись ПОДПИСАНТА, не совпадение id. Подпись
|
|
* подписанта (floor-signer) покрывает аутентифицирующую тройку {criterion_id, code_fingerprint,
|
|
* occurrence}; реконструируем её из green-run и проверяем verifyGreen. id = целостность (Δ5),
|
|
* подлинность = подпись. Синергия с 5.3: подмена отпечатка для прохода свежести ломает подпись
|
|
* здесь (отпечаток в подписанной тройке). Чистая, fail-CLOSE (нет ключа/sig → unsigned).
|
|
* Красные прогоны не требуют подписи (их «не-зелёность» ловит criteriaGreenMatched).
|
|
*/
|
|
export function greenSignaturesValid({ greenRuns = [], key }) {
|
|
const unsigned = (greenRuns || [])
|
|
.filter((r) => r && r.green === true)
|
|
.filter((r) => !verifyGreen(
|
|
{ criterion_id: r.criterion_id, code_fingerprint: r.code_fingerprint, occurrence: r.occurrence, sig: r.sig },
|
|
key,
|
|
))
|
|
.map((r) => r.criterion_id);
|
|
return { ok: unsigned.length === 0, unsigned };
|
|
}
|
|
|
|
/** A2 K2: требуемые навыки взяты по фактическому журналу вызовов (не по тексту). */
|
|
export function skillTakenByJournal({ requiredSkills = [], journalSkillCalls = [] }) {
|
|
const missing = skillsCoveredByJournal(requiredSkills, journalSkillCalls);
|
|
return { ok: missing.length === 0, missing };
|
|
}
|
|
|
|
/**
|
|
* #3 (Гейт-1, §3.1 шаг 3): механический пол полноты — потребляет результат машины
|
|
* охвата Машины 3 (`readinessChecklist` → {ready, items}) и гейтит на дыры/циклы/
|
|
* сироты/непокрытые просьбы. Результат инъектируется (без жёсткой связки с файлом
|
|
* coverage-machine — оркестрация/живой режим вызывает readinessChecklist и подаёт сюда).
|
|
* Нет результата → fail-closed (сужу вслепую по полноте).
|
|
*/
|
|
export function gate1CoverageGate({ readiness }) {
|
|
if (!readiness || typeof readiness !== 'object') {
|
|
return { ok: false, failures: [{ label: 'машина охвата не отработала — нет результата (fail-closed)', pointer: null }] };
|
|
}
|
|
const failures = (readiness.items || [])
|
|
.filter((i) => i && i.ok !== true)
|
|
.map((i) => ({ label: i.label, pointer: i.pointer, detail: i.detail }));
|
|
return { ok: readiness.ready === true && failures.length === 0, failures };
|
|
}
|
|
|
|
/**
|
|
* #5 (F3/F9): объявленные критерии приёмки обязаны происходить из ЗАПЕЧАТАННОГО набора
|
|
* (Гейт-1/Гейт-2), а не дозаполняться контроллером на демо-времени. Критерий, чьего id
|
|
* нет в sealedCriterionIds, → unsealed (дозаполнен мимо печати) → флаг.
|
|
*/
|
|
export function criteriaFromSealedPlan({ criteria = [], sealedCriterionIds = [] }) {
|
|
const sealed = new Set(sealedCriterionIds);
|
|
const unsealed = (criteria || []).filter((c) => !sealed.has(c.id)).map((c) => c.id);
|
|
return { ok: unsealed.length === 0, unsealed };
|
|
}
|