Files
brain/tools/spec-verity.mjs
T

97 lines
6.9 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
/**
* spec-verity — роутер-шаг ВЫХОДНОЙ лесенки (spec 2026-06-05 §3А «контроль на
* выходе», шаг 2): когда контроллер объявил «готово», роутер сверяет ГОТОВУЮ
* спеку против СЫРЫХ ответов владельца + ОДОБРЕННОГО плана. Ловит «прошёл
* движения, а написал своё» (kind fabricated — в спеке есть, в ответах/плане нет).
* Существование ($0) и судья (полнота+пре-мортем) — Машина 4, НЕ здесь.
* РАССУЖДЕНИЕ — у модели (llmCall мокается); проверки вокруг — механические.
* R8: это ИИ — грубое «написал не то» ловит, тонкую подмену смыслом может
* пропустить → последний рубеж владелец.
*/
import { parseRouterResponse } from './router-engine.mjs';
export const SPEC_VERITY_SLOTS = Object.freeze(['promised', 'discrepancies']);
export const SPEC_DISCREPANCY_KINDS = Object.freeze(['missing', 'distorted', 'fabricated']);
function isNonEmptyString(v) { return typeof v === 'string' && v.trim().length > 0; }
function isSpecSourceRef(v) { return v === 'prompt' || v === 'plan' || (typeof v === 'number' && Number.isInteger(v)); }
/**
* Валидатор формы (отдельный код): promised — НЕПУСТОЙ массив {item, source};
* discrepancies — массив (пуст = спека верна) {kind∈SPEC_DISCREPANCY_KINDS,
* detail, source}. source: 'prompt' | 'plan' | номер ответа.
*/
export function validateSpecVerityTrace(trace) {
const missingSlots = [];
const badItems = [];
if (!trace || typeof trace !== 'object') return { ok: false, missingSlots: [...SPEC_VERITY_SLOTS], badItems };
if (!Array.isArray(trace.promised) || trace.promised.length === 0) {
missingSlots.push('promised');
} else {
trace.promised.forEach((it, i) => {
if (!it || typeof it !== 'object' || !isNonEmptyString(it.item) || !isSpecSourceRef(it.source)) badItems.push(`promised[${i}]`);
});
}
if (!Array.isArray(trace.discrepancies)) {
missingSlots.push('discrepancies');
} else {
trace.discrepancies.forEach((it, i) => {
const kindOk = it && typeof it === 'object' && SPEC_DISCREPANCY_KINDS.includes(it.kind);
if (!kindOk || !isNonEmptyString(it.detail) || !isSpecSourceRef(it.source)) badItems.push(`discrepancies[${i}]`);
});
}
return { ok: missingSlots.length === 0 && badItems.length === 0, missingSlots, badItems };
}
/** Заземление: source резолвится 'prompt' | 'plan' | индекс ответа в [0, answersCount). */
export function groundSpecVerityTrace(trace, { answersCount = 0 } = {}) {
const dangling = [];
const check = (src, where) => {
if (src === 'prompt' || src === 'plan') return;
if (typeof src === 'number' && Number.isInteger(src) && src >= 0 && src < answersCount) return;
dangling.push(`${where}:${JSON.stringify(src)}`);
};
(trace?.promised || []).forEach((it, i) => check(it?.source, `promised[${i}]`));
(trace?.discrepancies || []).forEach((it, i) => check(it?.source, `discrepancies[${i}]`));
return { grounded: dangling.length === 0, dangling };
}
/** Построитель промпта (чистый, {system,user}). Анти-внушение: promised из якорей ДО чтения спеки. */
export function buildSpecVerityPrompt({ rawPrompt = '', answers = [], approvedPlan = null, spec = '' } = {}) {
const ans = Array.isArray(answers) ? answers : [];
const answersText = ans.length ? ans.map((a, i) => `Ответ ${i}: ${typeof a === 'string' ? a : JSON.stringify(a)}`).join('\n') : '(ответов нет)';
const planText = approvedPlan == null ? '(одобренного плана нет)' : (typeof approvedPlan === 'string' ? approvedPlan : JSON.stringify(approvedPlan));
const specText = typeof spec === 'string' ? spec : JSON.stringify(spec);
const system = [
'Ты — РОУТЕР-НАСТАВНИК, ВЫХОДНАЯ сверка верности спеки. Контроллер объявил «готово».',
'ПОРЯДОК (анти-внушение): СНАЧАЛА, читая ТОЛЬКО якоря (сырые ответы владельца + одобренный план), независимо выпиши, что было ОБЕЩАНО → promised (source: "prompt"|"plan"|номер ответа). ДО чтения спеки.',
'ПОТОМ сверь готовую спеку с promised и выпиши discrepancies: missing (обещано, в спеке нет), distorted (в спеке искажено), fabricated (в спеке есть, но НИ в ответах НИ в плане не обещано — «написал своё»).',
'Спека верна (расхождений нет) → discrepancies пустой — допустимо.',
'ВЫВОД — строго JSON: {"promised":[{"item":"…","source":"prompt"|"plan"|N}], "discrepancies":[{"kind":"missing"|"distorted"|"fabricated","detail":"…","source":"prompt"|"plan"|N}]}. Пустой promised недопустим.',
].join('\n');
const user = [
'--- СЫРОЙ ПРОМПТ ВЛАДЕЛЬЦА ---', rawPrompt,
'--- СЫРЫЕ ОТВЕТЫ ВЛАДЕЛЬЦА ПО ПОРЯДКУ ---', answersText,
'--- ОДОБРЕННЫЙ ПЛАН (якорь) ---', planText,
'--- ГОТОВАЯ СПЕКА (читать ПОСЛЕ promised) ---', specText,
].join('\n');
return { system, user };
}
/** Оркестратор: построить → llmCall → распарсить → валидировать → заземлить → faithful=код. */
export async function runSpecVerityCheck({ rawPrompt = '', answers = [], approvedPlan = null, spec = '', llmCall }) {
const ans = Array.isArray(answers) ? answers : [];
let trace;
try { trace = await llmCall({ buildPrompt: () => buildSpecVerityPrompt({ rawPrompt, answers: ans, approvedPlan, spec }) }); }
catch { return { ok: false, reason: 'сбой вызова модели', trace: null }; }
if (typeof trace === 'string') trace = parseRouterResponse(trace);
if (!trace) return { ok: false, reason: 'пустой/неразборный ответ роутера', trace: null };
const v = validateSpecVerityTrace(trace);
if (!v.ok) return { ok: false, reason: `невалидная трасса спеки: слоты [${v.missingSlots.join(', ')}] пункты [${v.badItems.join(', ')}]`, trace };
const g = groundSpecVerityTrace(trace, { answersCount: ans.length });
if (!g.grounded) return { ok: false, reason: `висящие ссылки: [${g.dangling.join(', ')}]`, trace };
const faithful = trace.discrepancies.length === 0;
return { ok: true, faithful, discrepancies: trace.discrepancies, trace };
}