397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
146 lines
9.2 KiB
JavaScript
146 lines
9.2 KiB
JavaScript
#!/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));
|
||
}
|