#!/usr/bin/env node
/**
 * round-control — покруговой контроль разговорной фазы (spec 2026-06-05 §3А реш.3):
 * новый план не рождается, пока текущий круг не сошёлся. Здесь — ЧИСТЫЕ ($0)
 * части: код-проверка круга (скил реально вызван по журналу + ни один вопрос не
 * повис) + решение круга (3 прохода диалога → эскалация владельцу). LLM-проверка
 * смысла (роутер-верность) и оркестратор — под-план 2b.
 */
import { hangingQuestions } from './question-slots.mjs';

/**
 * Код-проверка круга ($0, НЕ ИИ, §3А): (1) ни один заданный вопрос не повис
 * (question-slots); (2) каждый требуемый скил реально вызван (по журналу М1 —
 * invokedSkills; принцип K2: факт вызова, не текст/слово). requiredSkills/
 * invokedSkills — инъекция (журнал читается при live-wiring).
 */
export function codeRoundCheck({ slots = [], invokedSkills = [], requiredSkills = [] } = {}) {
  const reasons = [];
  const hanging = hangingQuestions(slots);
  if (hanging.length > 0) reasons.push(`повисшие вопросы: [${hanging.map((s) => s.id).join(', ')}]`);
  const invoked = new Set(invokedSkills);
  const missing = requiredSkills.filter((s) => !invoked.has(s));
  if (missing.length > 0) reasons.push(`скил не вызван (журнал): [${missing.join(', ')}]`);
  return { ok: reasons.length === 0, reasons };
}

// Исходы круга (закрытый список).
export const ROUND_OUTCOMES = Object.freeze(['proceed', 'soft-return', 'escalate-owner']);

/**
 * Решение круга (§3А реш.2/3): сошёлся (код+смысл) → proceed; пробел и проходов
 * < maxRounds → soft-return (дозадай/перезадай нужным скилом); пробел и проходы
 * исчерпаны → escalate-owner (та же тройка, что апелляция C-12). Двойной
 * терминатор: раунды→владелец (тут) + Гейт-1 (в конце).
 *
 * roundCount — счётчик ЗАВЕРШЁННЫХ проходов (0-based; escalate при >= maxRounds).
 * Битый счётчик (не целое ≥0) → консервативно эскалируем (аудит F4).
 *
 * ⚠️ ДВА РЕЖИМА (аудит F2):
 *  • managed (рекомендуемый, live-wiring): runRoundControl сам ведёт счётчик по
 *    roundKey через инъектируемый стор — инкремент = САМ факт вызова, забыть
 *    невозможно → вечный soft-return исключён по конструкции. См. runRoundControl.
 *  • legacy (pure decideRoundOutcome с явным roundCount): инкремент на
 *    ответственности caller'а; без него — повторный soft-return. Оставлен для
 *    обратной совместимости/юнит-тестов; в проде использовать managed-режим.
 */
export function decideRoundOutcome({ codeOk, routerFaithful, roundCount = 0, maxRounds = 3 } = {}) {
  const gap = !codeOk || !routerFaithful;
  if (!gap) return { outcome: 'proceed', reason: 'круг сошёлся: код + смысл' };
  const rc = (Number.isInteger(roundCount) && roundCount >= 0) ? roundCount : maxRounds;
  if (rc < maxRounds) return { outcome: 'soft-return', reason: `пробел (код:${codeOk} смысл:${routerFaithful}); проход ${rc}/${maxRounds} — мягкий возврат` };
  return { outcome: 'escalate-owner', reason: `пробел не закрыт за ${maxRounds} прохода — эскалация владельцу (C-12)` };
}

/**
 * Оркестратор круга (§3А реш.3): связывает код-проверку ($0) + роутер-верность
 * смысла (verityCall — предсвязанный runVerityCheck: ответы легли в план?) +
 * решение (3 прохода → владелец). verityCall инъецируется (round-control не знает
 * про LLM). Сбой верности → routerFaithful=false (не подтвердили → пробел, безопасно).
 *
 * F2 — managed-режим терминатора: если переданы roundKey + readRounds/writeRounds
 * (инъектируемый стор, как в parallel-session-lock/learning-queue), счётчик ведёт
 * САМ оркестратор: soft-return ⇒ +1 (проход потрачен), proceed/escalate ⇒ сброс в 0.
 * Инкремент = факт вызова → «забыть подвинуть счётчик» невозможно, вечного
 * soft-return нет по конструкции. Без roundKey — legacy: используется явный roundCount.
 */
export async function runRoundControl({ slots = [], invokedSkills = [], requiredSkills = [], roundCount = 0, maxRounds = 3, verityCall, roundKey = null, readRounds, writeRounds } = {}) {
  const codeCheck = codeRoundCheck({ slots, invokedSkills, requiredSkills });
  let verity;
  try { verity = await verityCall(); } catch { verity = { ok: false, faithful: false, reason: 'сбой верности' }; }
  const routerFaithful = !!(verity && verity.ok && verity.faithful);

  // C (аудит M1-M4): пустой/нестроковый roundKey НЕ активирует managed — иначе все
  // сессии с roundKey='' делят один счётчик-бакет (преждевременная эскалация/сброс чужого круга).
  const managed = typeof roundKey === 'string' && roundKey.length > 0 && typeof readRounds === 'function' && typeof writeRounds === 'function';
  const effectiveCount = managed ? (Number(readRounds(roundKey)) || 0) : roundCount;

  const decision = decideRoundOutcome({ codeOk: codeCheck.ok, routerFaithful, roundCount: effectiveCount, maxRounds });

  if (managed) {
    if (decision.outcome === 'soft-return') writeRounds(roundKey, effectiveCount + 1); // проход потрачен → следующий вызов видит +1
    else writeRounds(roundKey, 0); // proceed/escalate → круг разрешён, сброс
  }

  return { outcome: decision.outcome, reason: decision.reason, codeCheck, verity, routerFaithful, roundCount: effectiveCount };
}
