#!/usr/bin/env node
/**
 * enforce-reconcile (Машина 5 Пакет 8, Δ3) — PostToolUse-реконсилер двухтактного reconcile.
 *
 * Δ3 честно: PreToolUse-стена (supreme-gate) не видит факт исполнения — она лишь ПРЕД-записывает
 * намерение в журнал ДО allow (8.1). Достижимый второй такт — PostToolUse-сверка «произошло ровно
 * записанное»:
 *   reconcileAction   — исполненное действие имеет совпадающую пред-запись? нет → action-without-record
 *                       (действие проскользнуло мимо пред-записи стены — возможен обход).
 *   findOrphanIntents — пред-записи без соответствующего исполнения → record-without-action.
 *
 * WARN-уровень (НЕ блок): PreToolUse-пол уже отработал; расхождение — сигнал владельцу, а не
 * остановка (PostToolUse не может отменить уже исполненное). Чистые функции + fail-quiet I/O main.
 */
import { fileURLToPath } from 'node:url';
import { homedir } from 'node:os';
import { actionOf } from './enforce-supreme-gate.mjs';
import { loadJournal } from './action-journal.mjs';
import { resolveReceiptKey } from './receipt-key-config.mjs';

function sameAction(entry, action) {
  return !!entry && !!action
    && String(entry.op) === String(action.op)
    && String(entry.object) === String(action.object);
}

/** Исполненное действие имеет совпадающую пред-запись в журнале намерений? */
export function reconcileAction({ action, journalEntries = [] }) {
  const matched = (journalEntries || []).some((e) => sameAction(e, action));
  if (matched) return { matched: true };
  return {
    matched: false,
    flag: 'action-without-record',
    reason: `действие ${action && action.op} «${action && action.object}» исполнено без журнальной пред-записи (возможен обход стены)`,
  };
}

/** Пред-записи (намерения), под которые НЕ было исполнения → сироты (record-without-action). */
export function findOrphanIntents({ journalEntries = [], executedActions = [] }) {
  const orphans = (journalEntries || []).filter(
    (e) => !(executedActions || []).some((a) => sameAction(e, a)),
  );
  return { ok: orphans.length === 0, orphans, flag: orphans.length ? 'record-without-action' : null };
}

/**
 * Pure: harness-событие PostToolUse + загруженные журнальные пред-записи (payload'ы намерений)
 * → WARN-строка (если действие исполнено без пред-записи) или null. Никогда не бросает, не блок.
 * actionOf реюзится из supreme-gate — единый критерий {op, object}.
 */
export function reconcileEvent({ event, journalEntries = [] }) {
  if (!event) return null;
  const name = event.tool_name || (event.tool_use && event.tool_use.name);
  if (!name) return null;
  const action = actionOf({ name, input: event.tool_input || (event.tool_use && event.tool_use.input) || {} });
  const r = reconcileAction({ action, journalEntries });
  return r.matched ? null : `[reconcile] ⚠️ ${r.reason}`;
}

// I/O-обёртка PostToolUse: сверяет факт-действие с журналом намерений; расхождение → WARN в stderr,
// НИКОГДА не блокирует (exit 0). Fail-quiet. Загрузка журнала — реальная, но реконсилер ИНЕРТЕН до
// регистрации хука владельцем (owner-шаг A3); без ключа/файла нет данных = нет сигнала.
async function main() {
  try {
    let input = '';
    for await (const chunk of process.stdin) input += chunk;
    let event; try { event = JSON.parse(input); } catch { process.exit(0); }
    const key = resolveReceiptKey();
    const sessionId = event.session_id || event.sessionId;
    let journalEntries = [];
    if (key && sessionId) {
      try {
        const runtimeDir = `${homedir()}/.claude/runtime`;
        const { entries } = loadJournal({ sessionId, runtimeDir });
        journalEntries = entries.map((e) => e && e.payload).filter(Boolean);
      } catch { journalEntries = []; }
    }
    const warn = reconcileEvent({ event, journalEntries });
    if (warn) process.stderr.write(warn + '\n');
    void findOrphanIntents;
  } catch { /* fail-quiet: реконсилер — сигнал, не блок */ }
  process.exit(0);
}

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