#!/usr/bin/env node
/**
 * router-verity — роутер в роли КРИТИКА ВЕРНОСТИ для разговорной фазы (1а).
 * Поправка P16-e (spec 2026-06-05): роли НЕ меняются — контроллер ПРОИЗВОДИТ
 * разговорный план, роутер КРИТИКУЕТ его против СЫРЫХ слов владельца (промпт +
 * вся история ответов по порядку), НЕ против пересказа Claude. Анти-внушение:
 * роутер сперва САМ выводит «что просил владелец» (owner_asks), ПОТОМ сверяет с
 * планом (discrepancies) — оба слота обязательны по форме (пустой слот = красный
 * флаг, как validateTrace). РАССУЖДЕНИЕ — у модели (llmCall мокается); все
 * ПРОВЕРКИ вокруг — тупо-механические («умный не баран»).
 */
import { parseRouterResponse } from './router-engine.mjs';

// Обязательные структурные слоты трассы верности (анти-внушение §2.4).
export const VERITY_SLOTS = Object.freeze(['owner_asks', 'discrepancies']);

// Закрытый список видов расхождения (свободный текст в kind отклоняется).
export const DISCREPANCY_KINDS = Object.freeze(['missing', 'distorted']);

function isNonEmptyString(v) { return typeof v === 'string' && v.trim().length > 0; }
function isSourceRef(v) { return v === 'prompt' || (typeof v === 'number' && Number.isInteger(v)); }

/**
 * Механический валидатор трассы верности (ОТДЕЛЬНЫЙ код, не сам роутер — §6.2 B,
 * аналог A0). ФОРМА: owner_asks — НЕПУСТОЙ массив пунктов {ask, source};
 * discrepancies — массив (может быть пуст = план верен) пунктов
 * {kind∈DISCREPANCY_KINDS, detail, source}. Пустой/неверный слот = красный флаг.
 * НЕ судит «правильность мысли» и НЕ резолвит номер источника (groundVerityTrace).
 */
export function validateVerityTrace(trace) {
  const missingSlots = [];
  const badItems = [];
  if (!trace || typeof trace !== 'object') return { ok: false, missingSlots: [...VERITY_SLOTS], badItems };

  if (!Array.isArray(trace.owner_asks) || trace.owner_asks.length === 0) {
    missingSlots.push('owner_asks');
  } else {
    trace.owner_asks.forEach((it, i) => {
      if (!it || typeof it !== 'object' || !isNonEmptyString(it.ask) || !isSourceRef(it.source))
        badItems.push(`owner_asks[${i}]`);
    });
  }

  if (!Array.isArray(trace.discrepancies)) {
    missingSlots.push('discrepancies');
  } else {
    trace.discrepancies.forEach((it, i) => {
      const kindOk = it && typeof it === 'object' && DISCREPANCY_KINDS.includes(it.kind);
      if (!kindOk || !isNonEmptyString(it.detail) || !isSourceRef(it.source))
        badItems.push(`discrepancies[${i}]`);
    });
  }

  return { ok: missingSlots.length === 0 && badItems.length === 0, missingSlots, badItems };
}

/**
 * Заземление источников (§2.2 — «вся история по порядку, не потерять ранний
 * ответ»): каждый source в owner_asks/discrepancies обязан резолвиться в реальный
 * источник — 'prompt' ИЛИ целочисленный индекс ответа в [0, answersCount).
 * Ссылка вне диапазона → dangling (роутер сослался на несуществующий ответ).
 */
export function groundVerityTrace(trace, { answersCount = 0 } = {}) {
  const dangling = [];
  const check = (src, where) => {
    if (src === 'prompt') return;
    if (typeof src === 'number' && Number.isInteger(src) && src >= 0 && src < answersCount) return;
    dangling.push(`${where}:${JSON.stringify(src)}`);
  };
  (trace?.owner_asks || []).forEach((it, i) => check(it?.source, `owner_asks[${i}]`));
  (trace?.discrepancies || []).forEach((it, i) => check(it?.source, `discrepancies[${i}]`));
  return { grounded: dangling.length === 0, dangling };
}

/**
 * Построитель промпта верности (чистый, {system,user}). Анти-внушение §2.4 зашито
 * ДВАЖДЫ: (1) инструкция порядка в system (сперва owner_asks из сырых слов, потом
 * discrepancies против плана); (2) физически — сырые слова в user ПЕРЕД планом.
 * Вся история ответов нумеруется (0-based, под groundVerityTrace).
 */
export function buildVerityPrompt({ rawPrompt = '', answers = [], plan = null } = {}) {
  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 = plan == null ? '(плана пока нет)' : (typeof plan === 'string' ? plan : JSON.stringify(plan));
  const system = [
    'Ты — РОУТЕР-НАСТАВНИК в роли КРИТИКА ВЕРНОСТИ (разговорная фаза). Роли НЕ меняются: план производит контроллер, ты его КРИТИКУЕШЬ против СЫРЫХ слов владельца, а не против пересказа.',
    'ПОРЯДОК (обязателен, анти-внушение): СНАЧАЛА, читая ТОЛЬКО сырые слова владельца (промпт + ответы), независимо выпиши, что владелец РЕАЛЬНО просил → заполни owner_asks. Сделай это ДО того, как смотреть план.',
    'ПОТОМ сверь план с owner_asks и выпиши discrepancies. Не поддакивай плану. Если ранний ответ владельца в плане НЕ отражён или искажён — это расхождение, не пропусти.',
    'Каждый пункт owner_asks ОБЯЗАН ссылаться на источник: source = "prompt" ИЛИ номер ответа (целое, как в нумерации ниже).',
    'discrepancies: каждый пункт {kind: "missing"|"distorted", detail, source}. Пустой массив discrepancies = план ВЕРЕН (расхождений нет) — это допустимо.',
    'ВЫВОД — строго JSON: {"owner_asks":[{"ask":"…","source":"prompt"|N}], "discrepancies":[{"kind":"missing"|"distorted","detail":"…","source":"prompt"|N}]}. Пустой owner_asks недопустим.',
  ].join('\n');
  const user = [
    '--- СЫРОЙ ПРОМПТ ВЛАДЕЛЬЦА (дословно) ---',
    rawPrompt,
    '--- СЫРЫЕ ОТВЕТЫ ВЛАДЕЛЬЦА ПО ПОРЯДКУ (полная история) ---',
    answersText,
    '--- ПЛАН КОНТРОЛЛЕРА (разговорный, читать ПОСЛЕ owner_asks) ---',
    planText,
  ].join('\n');
  return { system, user };
}

/**
 * Оркестратор верности: построить промпт → llmCall (инъектируется; в проде
 * callAnthropicAPI) → распарсить → МЕХАНИЧЕСКИ провалидировать (слоты+форма) →
 * заземлить источники → вывести faithful = (нет расхождений). faithful считает
 * КОД из массива discrepancies, не со слов роутера (не доверяем самоотчёту).
 */
export async function runVerityCheck({ rawPrompt = '', answers = [], plan = null, llmCall }) {
  const ans = Array.isArray(answers) ? answers : [];
  let trace;
  try { trace = await llmCall({ buildPrompt: () => buildVerityPrompt({ rawPrompt, answers: ans, plan }) }); }
  catch { return { ok: false, reason: 'сбой вызова модели', trace: null }; }
  if (typeof trace === 'string') trace = parseRouterResponse(trace);
  if (!trace) return { ok: false, reason: 'пустой/неразборный ответ роутера', trace: null };
  const v = validateVerityTrace(trace);
  if (!v.ok) return { ok: false, reason: `невалидная трасса верности: слоты [${v.missingSlots.join(', ')}] пункты [${v.badItems.join(', ')}]`, trace };
  const g = groundVerityTrace(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 };
}
