#!/usr/bin/env node
/**
 * Rule #2 — Coverage = реальный навык, по ЖУРНАЛУ K2 (М7 Фаза 4a, §4.2).
 *
 * Stop-хук. Парсит `coverage: <channel>:<id>` из текста ассистента. Для skill:-канала
 * требует навык X в ПЕРЕСЕЧЕНИИ «Skill-tool_use ЭТОГО хода ∩ журнал вызовов» — turn-scope
 * даёт граница хода transcript (turnToolUses), факт «реально отработал» — журнал (канал М1,
 * skillTakenByJournal K2). Не по строке coverage: (Класс 1 закрыт). direct/node/chain/hook/agent
 * журналом не верифицируемы (§4.2) — принимаются на этом слое (их судит думающий М4, §1 П3).
 *
 * fail-CLOSE (М7 Фаза 0 правило 1): любая внутренняя ошибка → блок. Override-вокабуляр снят
 * (§12 escape≠override — единственная дверь escape М6).
 *
 * Spec: docs/superpowers/specs/2026-06-08-router-mentor-machine-7-design.md §4.2
 */

import {
  readStdin,
  parseEventJson,
  readTranscript,
  lastAssistantText,
  parseCoverageLine,
  turnToolUses,
  runtimeDir,
  exitDisciplineDecision,
} from './enforce-hook-helpers.mjs';
import { skillTakenByJournal } from './judge-gate-floor.mjs';
import { loadJournal } from './action-journal.mjs';
import { extractSkillCalls } from './enforce-skill-journaler.mjs';

const MUTATING_TOOLS = new Set(['Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'Bash']);

/** Нормализация имени навыка: trim+lower, снять префикс superpowers:. */
function normName(x) {
  return String(x || '').trim().toLowerCase().replace(/^superpowers:/, '');
}

export function decide({ toolUses = [], assistantText = '', journalSkillCalls = [] }) {
  // Чисто-разговорный ход (нет мутирующих) — пропуск.
  const hasMutating = toolUses.some((u) => MUTATING_TOOLS.has(u.name));
  if (!hasMutating) return { block: false };

  const cov = parseCoverageLine(assistantText);
  if (!cov) {
    return {
      block: true,
      message: [
        `[enforce-coverage-verify] ход выполнил мутирующие вызовы, но в ответе нет строки \`coverage:\`.`,
        `Первой строкой следующего ответа:`,
        `  coverage: skill:<name>   (навык должен быть РЕАЛЬНО вызван в этом ходе — проверяется по журналу)`,
        `  coverage: direct:<role>  (например direct:memory-sync)`,
      ].join('\n'),
    };
  }

  if (cov.channel === 'skill') {
    const want = normName(cov.id);
    const turnSkillNames = toolUses
      .filter((u) => u.name === 'Skill' && u.input && u.input.skill)
      .map((u) => normName(u.input.skill));
    const inTurn = turnSkillNames.includes(want);
    const inJournal = skillTakenByJournal({
      requiredSkills: [want],
      journalSkillCalls: (journalSkillCalls || []).map(normName),
    }).ok === true;
    if (!(inTurn && inJournal)) {
      return {
        block: true,
        message: [
          `[enforce-coverage-verify] coverage skill:${cov.id} не подтверждён фактом.`,
          inTurn
            ? `Навык НЕ найден в журнале вызовов (skill-журналер М1) — не доказано, что он реально отработал.`
            : `Навык НЕ вызван Skill-инструментом в ЭТОМ ходе (журнал кумулятивен — нужен вызов в текущем ходе).`,
          `Вызови навык через Skill, либо переключи coverage на direct:<role> с обоснованием.`,
        ].join('\n'),
      };
    }
    return { block: false };
  }

  // direct / node / chain / hook / agent — журналом не верифицируемы (§4.2), приняты здесь.
  return { block: false };
}

async function main() {
  const raw = await readStdin();
  const event = parseEventJson(raw);
  await exitDisciplineDecision(
    () => {
      const transcript = readTranscript(event.transcript_path);
      const toolUses = turnToolUses(transcript);
      const assistantText = lastAssistantText(transcript);
      const { entries } = loadJournal({
        sessionId: event.session_id || 'unknown',
        runtimeDir: runtimeDir(),
      });
      const journalSkillCalls = extractSkillCalls(entries);
      return decide({ toolUses, assistantText, journalSkillCalls });
    },
    { label: 'enforce-coverage-verify' },
  );
}

const isCli = process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/enforce-coverage-verify.mjs');
if (isCli) main();
