#!/usr/bin/env node
/**
 * enforce-criterion-gate (Level B, М5-family) — PreToolUse Bash. На git commit/push требует, чтобы
 * КАЖДЫЙ критерий запечатанного плана, затронутый staged-кодом, имел настоящий по-критерийный GREEN
 * (test-passed И mutation-killed), свежий по отпечатку и подписанный (runCriterionGate). Строго сильнее
 * G1 (whole-suite): доказывает не «сюита зелёная», а «тесты изменённого кода реально что-то проверяют».
 * fail-CLOSE; escapable M6; docs-only short-circuit; рубильник общий с G1 (флаг+ключ; inert $0). Нет
 * плана на кодовое изменение → block (SE-LB-5). Регистрация в settings.json — шаг ВЛАДЕЛЬЦА.
 */
import { readStdin, parseEventJson, exitDecision, detectGitCommandKind, isDocsOnlyChange, listChangedFiles } from './enforce-hook-helpers.mjs';
import { runCriterionGate } from './judge-orchestrator.mjs';
import { verifyGateActive } from './verify-gate-config.mjs';
import { canonicalAction, escapeGrantOpen, loadFloorEscapes, loadConsumed } from './escape-grant.mjs';
import { resolveReceiptKey } from './receipt-key-config.mjs';
import { logGuardBlock } from './guard-block-log.mjs';

/** Чистое решение. */
export function decide({ toolName, command, gate, key, codeChanged = false, frozenPlanValid = false,
  criteria = [], sealedCriterionIds = [], greenRuns = [], currentFingerprints = {}, escapeOpen = false, changedPaths = [] }) {
  if (toolName !== 'Bash' || typeof command !== 'string') return { block: false };
  const kind = detectGitCommandKind(command);
  if (kind !== 'commit' && kind !== 'push') return { block: false };
  if (isDocsOnlyChange(changedPaths)) return { block: false };
  if (!gate || !gate.active) {
    if (gate && gate.keyMissing) return { block: true, message: '[criterion-gate] флаг ON, но ключ подписанта недоступен — fail-CLOSE' };
    return { block: false }; // inert ($0)
  }
  if (escapeOpen) return { block: false };
  if (codeChanged && !frozenPlanValid) {
    return { block: true, message: '[criterion-gate] кодовое изменение без валидного запечатанного плана — заморозьте план (нет по-критерийного доказательства)' };
  }
  const r = runCriterionGate({ criteria, greenRuns, sealedCriterionIds, currentFingerprints, signerKey: key });
  if (!r.passed) {
    return { block: true, message: `[criterion-gate] по-критерийный GREEN не доказан (${r.stoppedAt}${r.detail ? ': ' + JSON.stringify(r.detail) : ''}) — прогоните node tools/produce-criterion-greens.mjs` };
  }
  return { block: false };
}

// ── I/O-main (под активацией владельцем) ──
async function main() {
  let event, gate;
  try { event = parseEventJson(await readStdin()); gate = verifyGateActive(); }
  catch { exitDecision({ block: false }); return; } // pre-gate ошибка → inert-safe ($0)
  if (!gate.active && !gate.keyMissing) { exitDecision({ block: false }); return; }
  try {
    const { loadFrozenPlan, verifyFrozenPlan, treeLeaves, sealedCriterionIds: sealedIds } = await import('./plan-lock.mjs');
    const { codeFingerprint } = await import('./criterion-green.mjs');
    const { mapStepToFiles } = await import('./produce-criterion-greens.mjs');
    const { readFileSync, existsSync } = await import('node:fs');
    const { join } = await import('node:path');
    const { homedir } = await import('node:os');

    const command = (event.tool_input && event.tool_input.command) || '';
    const kind = detectGitCommandKind(command);
    const changedPaths = (kind === 'commit' || kind === 'push') ? listChangedFiles(kind) : [];
    const codeChanged = changedPaths.some((p) => !isDocsOnlyChange([p]));
    const sess = event.session_id || 'unknown';
    const key = resolveReceiptKey();
    const runtimeDir = join(homedir(), '.claude', 'runtime');
    const gitCwd = process.cwd();
    const plan = loadFrozenPlan({ sessionId: sess, runtimeDir });
    const frozenPlanValid = !!(plan && verifyFrozenPlan(plan, key));

    const changedSet = new Set(changedPaths);
    const criteria = [];
    const currentFingerprints = {};
    if (frozenPlanValid) {
      for (const leaf of treeLeaves(plan.steps || [])) {
        const files = mapStepToFiles(leaf);
        if (!files || !changedSet.has(files.sourceFile)) continue;
        criteria.push({ id: leaf.criterion_id });
        const read = (f) => { try { return readFileSync(join(gitCwd, f), 'utf-8'); } catch { return ''; } };
        currentFingerprints[leaf.criterion_id] = codeFingerprint({ [files.sourceFile]: read(files.sourceFile), [files.testFile]: read(files.testFile) });
      }
    }
    let greenRuns = [];
    const gp = join(runtimeDir, `criterion-greens-${sess}.json`);
    if (existsSync(gp)) { try { greenRuns = JSON.parse(readFileSync(gp, 'utf-8')); } catch { greenRuns = []; } }

    const action = canonicalAction('Bash', { command });
    const escapeOpen = escapeGrantOpen(action, loadFloorEscapes(sess), loadConsumed(sess));
    const r = decide({ toolName: event.tool_name, command, gate, key, codeChanged, frozenPlanValid,
      criteria, sealedCriterionIds: frozenPlanValid ? sealedIds(plan) : [], greenRuns, currentFingerprints, escapeOpen, changedPaths });
    if (r.block) logGuardBlock(event, 'Level B Criterion', r.message);
    exitDecision({ block: r.block, message: r.block ? r.message : undefined });
  } catch {
    exitDecision({ block: true, message: '[criterion-gate] внутренняя ошибка — fail-CLOSED' });
  }
}

import { fileURLToPath } from 'node:url';
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
if (isCli) main();
