622ac4df28
Третий построчный аудит машин 1-4 свежим объективом (корректность логики /
реальные баги — НЕ понимание, НЕ грабли; это были два прошлых прохода).
4 читающих под-агента code-analyzer. M1/M2/M3 — багов ядра нет (подтверждено).
M4 (судья, инертен; код должен быть верен и при включении): 3 реальные дыры по TDD.
M4:
- judge-engine.mjs runJudge: (raw.objections||[]).filter((o)=>o.verdict) падал на
objections=[null] (o.verdict на null) и на не-массиве (.filter is not a function).
|| гасит только falsy. Краш ломал вердикт; в инертной обёртке выброс уходил в
catch→block:false = fail-open. Fix: Array.isArray(...)?...:[] + (o && o.verdict).
- judge-verdict-slots.mjs: String(raw).trim().length скрывал не-строки — слот {}
давал '[object Object]' (длина 15) и проходил как содержательный (мусорный
объект/массив штамповал форму вердикта). Fix: слот обязан быть строкой
(typeof raw !== 'string' → trivial). Мягкий fail-open формы закрыт.
- judge-orchestrator.mjs runGateLadder: step.run() без try/catch пробрасывал
исключение упавшего шага пола вместо «пол не пройден» → решение неопределённо
(в обёртке catch→block:false = fail-open). Fix: бросок шага = passed:false
(fail-closed → блок), последующие не запускаются. Чистый модуль теперь сам
гарантирует безопасную сторону, не полагаясь на обёртку.
Регрессия tools-only 2560 passed + 2 skip (+5 TDD-тестов, 0 регрессий).
Осознанно НЕ менялось (без призраков):
- M1 verifyChain без 3-го арг = нарушение контракта вызова, не валидный вход.
- M2 node-в-цепочке = то же разрешение, что одиночный node (контракт, тест L53);
readonly-git-в-цепочке блок = осознанный default-deny (fail-safe).
- M3 defer уже защищён G-фиксом (if e.status!=='pending' return e — ДО defer);
N3 stale-комментарий (код строже докстринга).
- M4-C DESTRUCTIVE_RE иллюстративен (divergence всё равно судится; разрушительный
bash режется полом M2/M5 до судьи); M4-D slop-counter↔logVerdict — live-wiring.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
124 lines
7.8 KiB
JavaScript
124 lines
7.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* judge-engine (§7/§8/§10, K7, G1) — думающая часть судьи. Образец — router-classifier:
|
|
* построитель промпта ЧИСТЫЙ (детерминированно тестируется), сам вызов модели МОКАЕТСЯ,
|
|
* а ПРОВЕРКИ вокруг — тупо-механические (пол судьи 4-A). «Умный не баран».
|
|
* - VOTE_LAYOUTS: раскладка голосов по функциям (§7), голоса РАЗДЕЛЬНО.
|
|
* - PREMORTEM_CLASSES: фиксированные классы провала (§10) — общая карта, не растущий список.
|
|
* - K7: премортем-слот на Гейте-1 обязателен (входит в layout gate1).
|
|
* - G1 classifyByReversibility: фатальное = тяжёлое И необратимое → блок; иначе совет.
|
|
* - runJudge: один вердикт + механический пост-чек (слоты/под-прогоны/якорь/обратимость).
|
|
* - consensusDecision (J3): на высоком риске несколько судей, любой НЕТ → блок.
|
|
* Судья СЛЕП к переписке: на вход подаётся только продукт+цель+карточки (по конструкции).
|
|
*/
|
|
import { canonicalJson } from './receipt-sign.mjs';
|
|
import { validateVerdictSlots } from './judge-verdict-slots.mjs';
|
|
import { partitionObjections } from './judge-anchor.mjs';
|
|
import { requiredSubRunsPresent } from './judge-subrun-journal.mjs';
|
|
|
|
export const VOTE_LAYOUTS = Object.freeze({
|
|
gate1: ['completeness', 'premortem', 'goal_advocate', 'correctness'], // премортем = K7
|
|
gate2: ['spec_fidelity', 'verifiability', 'plan_soundness', 'execution_risk', 'step_clarity'],
|
|
a2_divergence: ['intent_fidelity'],
|
|
a2_destructive: ['radius_reversibility', 'attacker'],
|
|
part_light: ['correctness', 'simplicity', 'footgun'],
|
|
part_risky: ['radius_tests', 'twins', 'attacker'],
|
|
gate3: ['goal_achieved', 'premortem_whole', 'behavior_vs_goal'],
|
|
});
|
|
|
|
export const PREMORTEM_CLASSES = Object.freeze([
|
|
'technique', 'seam', 'user', 'future_change', 'abuse', 'money_irreversible',
|
|
]);
|
|
|
|
/** Линзы функции (+ по риску добавляются attacker и money). */
|
|
export function requiredLensesFor(functionName, { risk = false } = {}) {
|
|
const base = VOTE_LAYOUTS[functionName] ? [...VOTE_LAYOUTS[functionName]] : [];
|
|
if (risk) {
|
|
if (!base.includes('attacker')) base.push('attacker');
|
|
if (!base.includes('money')) base.push('money');
|
|
}
|
|
return base;
|
|
}
|
|
|
|
/** Чистый построитель промпта судьи: {system, user}. Детерминирован. */
|
|
export function buildJudgePrompt({ functionName, requiredLenses = [], product = {}, goal = '', cards = [] }) {
|
|
const system = [
|
|
'Ты — судья (критик, не исполнитель). Выноси МОТИВИРОВАННЫЕ возражения с якорем.',
|
|
'Ты СЛЕП к переписке и рассуждениям сторон — судишь только поданный продукт против цели.',
|
|
'План — это БУДУЩЕЕ: слово «проверено»/«сделано» за ФАКТ не принимай (K5).',
|
|
`Функция: ${functionName}.`,
|
|
`Заполни слот на КАЖДУЮ линзу (пустой слот → вердикт невалиден): ${requiredLenses.join(', ')}.`,
|
|
`Премортем по классам провала (по каждому: что от решений НЕ закрыто): ${PREMORTEM_CLASSES.join(', ')}.`,
|
|
'Каждое возражение укажи с якорем {kind, ref}; без якоря оно станет советом, не блоком.',
|
|
].join('\n');
|
|
const user = [
|
|
`ЦЕЛЬ (неизменный якорь): ${goal}`,
|
|
`КАРТОЧКИ НАВЫКОВ: ${canonicalJson(cards)}`,
|
|
`ПРОДУКТ НА СУД: ${canonicalJson(product)}`,
|
|
].join('\n');
|
|
return { system, user };
|
|
}
|
|
|
|
/**
|
|
* G1: что блокирует (фатальное=тяжёлое И необратимое), что идёт советом.
|
|
* #8 (сомнение→блок): тяжёлое блокирует, ПОКА не доказано обратимое (reversible===true).
|
|
* Отсутствующая/неизвестная обратимость у тяжёлого возражения → блок (fail-closed).
|
|
*/
|
|
export function classifyByReversibility({ severity, reversible } = {}) {
|
|
const heavy = severity === 'fatal' || severity === 'heavy';
|
|
return (heavy && reversible !== true) ? 'block' : 'advice';
|
|
}
|
|
|
|
function isBlockingObjection(o) {
|
|
return classifyByReversibility(o) === 'block';
|
|
}
|
|
|
|
/**
|
|
* Один заход судьи + механический пост-чек. llmCall(prompt)→{decision, slots, objections}.
|
|
* Механика поверх модели: слоты валидны? под-прогоны на месте? какие возражения реально
|
|
* блокируют (якорь + необратимость)? Модель НЕ может проштамповать GO мимо механики.
|
|
*/
|
|
export function runJudge({ functionName, requiredLenses, subRunsRequired = [], subRuns = [], llmCall, promptArgs = {} }) {
|
|
const prompt = buildJudgePrompt({ functionName, requiredLenses, ...promptArgs });
|
|
const raw = llmCall(prompt) || {};
|
|
|
|
const slotCheck = validateVerdictSlots(raw, requiredLenses);
|
|
if (!slotCheck.valid) {
|
|
return { decision: 'NO-GO', accepted: false, reason: 'слоты вердикта невалидны (§9.2)', slotCheck };
|
|
}
|
|
const subRunCheck = requiredSubRunsPresent(subRunsRequired, subRuns);
|
|
if (!subRunCheck.ok) {
|
|
return { decision: 'NO-GO', accepted: false, reason: 'нет требуемого под-прогона (фейк прилежности, дисциплина #1)', subRunCheck };
|
|
}
|
|
|
|
// Аудит M1-M4 (свежий объектив): нормализуем выход модели — objections может прийти
|
|
// не массивом (строка/объект) или с null-элементом. Раньше `(raw.objections||[])` гасил
|
|
// только falsy → .filter падал на не-массиве, а `o.verdict` — на null-элементе (краш ломал
|
|
// вердикт; в инертной обёртке выброс уходил в catch→block:false = fail-open). Теперь не падает.
|
|
const rawObjs = Array.isArray(raw.objections) ? raw.objections : [];
|
|
const noObjs = rawObjs.filter((o) => o && o.verdict === 'NO');
|
|
const { blocking: anchored, advisory: anchorless } = partitionObjections(noObjs);
|
|
const trulyBlocking = anchored.filter(isBlockingObjection); // якорь + необратимость
|
|
const demotedReversible = anchored.filter((o) => !isBlockingObjection(o)); // якорь, но обратимо → совет
|
|
const advice = [...anchorless, ...demotedReversible];
|
|
|
|
return {
|
|
decision: trulyBlocking.length ? 'NO-GO' : 'GO',
|
|
accepted: true,
|
|
blocking: trulyBlocking,
|
|
advice,
|
|
slots: raw.slots,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* J3-консенсус (высокий риск, несколько судей): GO только если ЕСТЬ голоса И КАЖДЫЙ
|
|
* из них чистый GO. Иначе NO-GO. (Аудит M1-M4: пустой список и битый/неполный голос
|
|
* раньше дрейфовали к GO — «нет согласия = не блок» неверно для hard-risk; теперь fail-closed.)
|
|
*/
|
|
export function consensusDecision(verdicts = []) {
|
|
const list = verdicts || [];
|
|
if (list.length === 0) return 'NO-GO';
|
|
return list.every((v) => v && v.decision === 'GO') ? 'GO' : 'NO-GO';
|
|
}
|