Files
brain/tools/judge-orchestrator.mjs
T

146 lines
9.2 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
/**
* judge-orchestrator (§3, §6, §12, J1/J2/J5/J8/J9, F8/F10, A0) — машинерия судьи:
* тупо-механическая сборка из пола (4-A/4-C), думающей части (4-D) и печати (4-B).
* - gateFires (A0): ворота срабатывают СТРУКТУРНО от состояния, судью не зовут по желанию.
* - runGateLadder: 3-шаговая лесенка (существование $0 → верность → судья); провал → стоп раньше.
* - finalGate (J9): «да» судьи НЕ отпирает детерминированный пол; пол перевешивает.
* - appealStep (F8): 3 круга на ОДНО несогласие → эскалация владельцу.
* - judgeHealth (J5): упал/деградировал → громкий крик + деградация до пола (молча не пропускает).
* - gate3Terminal (F10): Гейт-3 ничего не печатает вниз — только принято/не принято.
* - logVerdict (J8): каждый вердикт append-only в журнал (инъекция).
* Сам вызов модели и persist — в 4-D (мок) и хук-обёртках 4-G; здесь чистая логика.
*/
import { classifyDestructive } from './classify-destructive.mjs';
import { criteriaFromSealedPlan, criteriaGreenMatched, fingerprintFresh, greenSignaturesValid, specToPlanCoverage, k5CriterionCheck } from './judge-gate-floor.mjs';
/** A0: ворота срабатывают от состояния (готово И не запечатано), не «по желанию». */
export function gateFires({ ready, alreadySealed }) {
return !!ready && !alreadySealed;
}
/** Лесенка: шаги по порядку; первый ok===false → стоп раньше (остальные не запускаются). */
export function runGateLadder(steps = []) {
for (const step of steps) {
// Аудит M1-M4 (свежий объектив): шаг пола может УПАСТЬ (I/O, битый ввод). Раньше step.run()
// без try/catch пробрасывал исключение наверх → решение становилось неопределённым (в инертной
// обёртке catch→block:false = fail-open). Теперь бросок = «пол не пройден» (fail-closed → блок),
// последующие шаги не запускаются. Сам чистый модуль гарантирует безопасную сторону.
let result;
try {
result = step.run();
} catch {
return { passed: false, stoppedAt: step.name, detail: 'шаг пола упал (fail-closed → блок)' };
}
if (!result || result.ok !== true) {
return { passed: false, stoppedAt: step.name, detail: result && result.detail };
}
}
return { passed: true };
}
/**
* J9: пол перевешивает «да» судьи. Снять вето пола может ТОЛЬКО явный floorBlocked===false.
* (Аудит M1-M4: раньше любой falsy — undefined/null от упавшей проверки пола — трактовался
* как «пол чист» → fail-open. Теперь недоказанный пол → block.)
*/
export function finalGate({ judgeDecision, floorBlocked }) {
if (floorBlocked !== false) return 'block';
return judgeDecision === 'GO' ? 'allow' : 'block';
}
/** F8: следующий круг апелляции; >3 кругов на одно несогласие → к владельцу. */
export function appealStep({ round, maxRounds = 3 }) {
const next = round + 1;
return { round: next, toOwner: next > maxRounds };
}
/** J5: здоровье судьи. Недоступен/деградировал → пост пустой + крик + режим пола. */
export function judgeHealth({ available, degraded }) {
const healthy = !!available && !degraded;
return healthy
? { onPost: true, cry: false, mode: 'judge' }
: { onPost: false, cry: true, mode: 'floor-only' };
}
/** F10: Гейт-3 терминален — печати-вниз нет, только принято/не принято. */
export function gate3Terminal(verdict) {
return { accepted: !!verdict && verdict.decision === 'GO', sealDown: null };
}
/** J8: записать вердикт append-only (журнал инъектируется). */
export function logVerdict({ verdict, nowMs, journal }) {
const entry = {
kind: 'verdict',
functionName: verdict && verdict.functionName,
decision: verdict && verdict.decision,
at: typeof nowMs === 'number' ? nowMs : null,
};
if (typeof journal === 'function') journal(entry);
return entry;
}
// #4 (§3.3): A2 просыпается на двух поводах — детерминированный разбор по СУТИ команды.
// Разрушительное → destructive (несколько голосов: радиус/обратимость + атакующий);
// иначе → divergence (1 голос). Единый источник — classifyDestructive (§4, Δ9-б):
// локальный DESTRUCTIVE_RE удалён, suspicious-набор берётся из классификатора.
export function a2CaseSelect(action) {
const cmd = String((action && (action.object || action.command)) || '');
return classifyDestructive(cmd).suspicious ? 'destructive' : 'divergence';
}
/**
* #4 — упаковка функции-гейта: сперва механический пол ($0, лесенка floorSteps);
* провал любого шага → NO-GO на стадии 'floor' (судья не зовётся, экономия + лесенка).
* Весь пол ок → runEngine() (думающая часть, инъекция) → решение на стадии 'judge'.
* Один runner для всех пяти функций; конкретные floorSteps/runEngine задаёт вызывающий.
*/
export function runGateFunction({ floorSteps = [], runEngine }) {
const ladder = runGateLadder(floorSteps);
if (!ladder.passed) {
return { decision: 'NO-GO', stage: 'floor', stoppedAt: ladder.stoppedAt, detail: ladder.detail };
}
const verdict = typeof runEngine === 'function' ? runEngine() : { decision: 'NO-GO' };
return { decision: verdict.decision, stage: 'judge', verdict };
}
/**
* 5.5 (Δ6): шаги критерий-гейта — РОВНО 4 проверки поверх существующей runGateLadder
* (НЕ плодим criterionFullyProven; лекарство «забыл пол» уже встроено в лесенку). Порядок
* И-семантики: id ∈ печать (целостность) → green-присутствие → свежесть отпечатка (Δ2) →
* подпись подписанта (Δ5, подлинность). Структурный тест «ровно эти 4 шага» ловит пропуск.
* Каждый шаг — {name, run:()=>{ok,...}}; runGateLadder короткозамыкается на первом провале.
*/
export function criterionGateSteps({ criteria = [], greenRuns = [], sealedCriterionIds = [], currentFingerprints = {}, signerKey } = {}) {
return [
{ name: 'criteria-from-sealed-plan', run: () => criteriaFromSealedPlan({ criteria, sealedCriterionIds }) },
{ name: 'criteria-green-matched', run: () => criteriaGreenMatched({ criteria, greenRuns }) },
{ name: 'fingerprint-fresh', run: () => fingerprintFresh({ greenRuns, currentFingerprints }) },
{ name: 'green-signatures-valid', run: () => greenSignaturesValid({ greenRuns, key: signerKey }) },
];
}
/** 5.5 (Δ6): прогнать критерий-гейт через И-лесенку (короткое замыкание на первом провале). */
export function runCriterionGate(input) {
return runGateLadder(criterionGateSteps(input));
}
/**
* Гейт-2 (§3.2, М7 Фаза 4c): шаги план-гейта — РОВНО 2 проверки через runGateLadder (зеркало
* criterionGateSteps). Поглощает tdd-gate Rule #6 «план перед prod-кодом» в ЗАПЕЧАТАННОЙ форме:
* спека покрыта шагом плана (specToPlanCoverage) + значимый шаг несёт конкретный критерий
* (k5CriterionCheck). НЕ text-mention пути плана (Класс 1 hasPlanIndicator закрыт). Синергия с
* М2-стеной: стена = членство действия в плане, Гейт-2 = полнота покрытия спеки + критерий.
*/
export function planGateSteps({ specSections = [], planSteps = [] } = {}) {
return [
{ name: 'spec-to-plan-coverage', run: () => specToPlanCoverage({ specSections, planSteps }) },
{ name: 'k5-criterion-check', run: () => k5CriterionCheck({ planSteps }) },
];
}
/** Гейт-2: прогнать план-гейт через И-лесенку (короткое замыкание на первом провале). */
export function runPlanGate(input) {
return runGateLadder(planGateSteps(input));
}