#!/usr/bin/env node
/**
 * floor-manifest-check (Машина 5, Блок 5, Δ8) — манифест-самопроверка регистрации (SessionStart).
 *
 * Проблема черепах: хук НЕ может гарантировать, что ДРУГИЕ хуки (пол + защитные) реально
 * зарегистрированы в settings.json. Достижимый максимум — на старте сессии прочитать
 * settings.json и, если пол-хук (enforce-floor) или защитные хуки не прописаны, ГРОМКО
 * предупредить владельца. Это **сигнал, не гарантия** и **НЕ блок** (созвучно judgeHealth →
 * cry:true): нельзя объявить «пол стоит» без подтверждения регистрации, но и блокировать
 * сессию из-за нерегистрации нельзя (сам этот хук тоже мог быть не зарегистрирован).
 *
 * Чистое ядро (settings инъектируется) + тонкая I/O-обёртка main() (читает файл, fail-quiet, exit 0).
 */
import fsDefault from 'node:fs';
import { fileURLToPath } from 'node:url';

// Полный список событий-хуков Claude Code — обходим каждое (структура одинакова:
// settings.hooks[event] = [{ matcher?, hooks: [{ type:'command', command }] }]).
const HOOK_EVENTS = [
  'PreToolUse', 'PostToolUse', 'Stop', 'SubagentStop', 'SessionStart',
  'SessionEnd', 'UserPromptSubmit', 'PreCompact', 'Notification',
];

// Security-load-bearing набор для «пол подтверждён» (F-3, аудит 2026-06-07): не один пол, а
// весь защитный контур — несущий пол (вето-до-плана) + верховная стена (вне-плана) + стражи
// нормативки/read-exfil/egress-exfil. Раньше проверялся только enforce-floor → owner мог
// забыть стену/стражей и получить зелёный «protected». Живая обёртка/владелец может передать
// иной набор; контракт checkManifest — по requiredHooks (DEFAULT — фолбэк main()). WARN-only.
// SE-B (М7 Фаза 6): расширено до ПОЛНОГО пост-переездного набора М1–М6 + М1-журналер — раньше
// проверялись только пол+стена+3 exfil-стража, без М4 (judge-gate) и М6 (snapshot/escape), и доска
// рапортовала «protected» при незарегистрированных М4/М6. Теперь «ПОСТ ПОЛНЫЙ» = весь контур.
const DEFAULT_REQUIRED_HOOKS = [
  'enforce-floor.mjs',                   // М5 несущий пол: вето-до-плана на необратимое
  'enforce-supreme-gate.mjs',            // М2 верховная стена: default-deny вне плана (нужны ОБА)
  'enforce-normative-content-rules.mjs', // М1/М5 защита нормативки/памяти от подмены
  'enforce-read-path-deny.mjs',          // М5 read-exfil (transcript/runtime/secrets)
  'enforce-mcp-classification.mjs',      // М5 egress-exfil (исходящий MCP-payload)
  'enforce-judge-gate.mjs',              // М4 судья: приёмка ТЗ/плана/результата + надзор
  'enforce-snapshot.mjs',                // М6 снимок: точка отката перед разрушительным
  'enforce-floor-escape-consume.mjs',    // М6 escape владельца: единственная законная дверь
  'enforce-skill-journaler.mjs',         // М1 журналер навыков: неподделываемый канал K2
  'enforce-verify-gate.mjs',             // G1 М5-family: подписанный verify-receipt перед commit/push
  'enforce-criterion-gate.mjs',          // Level B М5-family: по-критерийный mutation-proven GREEN
  'enforce-coverage-verify.mjs',         // G2 М7 Фаза 8: журнал-K2 coverage (Фаза 4a) — поглощённая дисциплина
  'enforce-todowrite-skill-verifier.mjs',// G2 М7 Фаза 8: журнал-K2 todowrite (Фаза 4a) — поглощённая дисциплина
];

/** Собрать все command-строки хуков из settings (битые/пустые секции игнорируются, не бросает). */
export function collectHookCommands(settings) {
  const out = [];
  const hooks = settings && settings.hooks;
  if (!hooks || typeof hooks !== 'object') return out;
  for (const ev of HOOK_EVENTS) {
    const arr = hooks[ev];
    if (!Array.isArray(arr)) continue;
    for (const entry of arr) {
      const inner = entry && entry.hooks;
      if (!Array.isArray(inner)) continue;
      for (const h of inner) {
        if (h && typeof h.command === 'string') out.push(h.command);
      }
    }
  }
  return out;
}

/**
 * Проверить регистрацию требуемых хуков. requiredHooks — basenames (подстрока в command).
 * Возвращает СИГНАЛ (без поля block): {ok, registered, missing, cry, mode}. cry = !ok (громкий
 * WARN), mode = ok ? 'protected' : 'floor-unverified'. Никогда не блокирует (Δ8).
 */
export function checkManifest({ settings = {}, requiredHooks = DEFAULT_REQUIRED_HOOKS } = {}) {
  const commands = collectHookCommands(settings);
  const req = requiredHooks || [];
  const registered = req.filter((h) => commands.some((c) => c.includes(h)));
  const missing = req.filter((h) => !commands.some((c) => c.includes(h)));
  const ok = missing.length === 0;
  return { ok, registered, missing, cry: !ok, mode: ok ? 'protected' : 'floor-unverified' };
}

function readSettings(path, fsImpl) {
  try { return JSON.parse(fsImpl.readFileSync(path, 'utf8')); }
  catch { return {}; }
}

// I/O-обёртка SessionStart: читает .claude/settings.json относительно cwd; cry → WARN в stderr;
// НИКОГДА не блокирует (exit 0). Fail-quiet: ошибка чтения — тоже WARN-уровень, не падение.
async function main() {
  try {
    const settings = readSettings('.claude/settings.json', fsDefault);
    const r = checkManifest({ settings });
    if (r.cry) {
      process.stderr.write(
        `[floor-manifest] ⚠️ пол НЕ подтверждён — не зарегистрированы хуки: ${r.missing.join(', ')}. ` +
        '«Пол стоит» — сигнал, не гарантия (Δ8); зарегистрируйте их в .claude/settings.json.\n',
      );
    }
  } catch { /* fail-quiet: манифест — сигнал, не блок */ }
  process.exit(0);
}

const isCli = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
if (isCli) main();

export const _internals = { readSettings, DEFAULT_REQUIRED_HOOKS, HOOK_EVENTS };
