#!/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 };
}
