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