#!/usr/bin/env node /** * observer-verdicts — извлечение четырёх вердиктов (роутер / наставник / судья / gate3) для * эпизода наблюдателя из персистентного снимка решений verdict-snapshot-.json (его пишет * writeStage из verdict-surface-store). Чистые функции; любой сбой I/O — мягкий ({}/null). * Спека роутер-реестр §7 (логирование решающих — восстановимо, на каком звене план отскочил). */ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { homedir } from 'node:os'; /** * Читает снимок решений сессии и агрегирует по каждой стадии последнюю запись (max ts). * @returns {{ [stage:string]: {status, reason, ts} }} либо {} (нет файла / сбой). */ export function readVerdictSnapshot(sessionId, baseDir) { try { const dir = baseDir || join(homedir(), '.claude', 'runtime'); const obj = JSON.parse(readFileSync(join(dir, `verdict-snapshot-${sessionId}.json`), 'utf8')); if (!obj || typeof obj !== 'object') return {}; const latest = {}; for (const hash of Object.keys(obj)) { const stages = obj[hash]; if (!stages || typeof stages !== 'object') continue; for (const stage of Object.keys(stages)) { const v = stages[stage]; if (!v || typeof v !== 'object') continue; const ts = Number(v.ts) || 0; if (!latest[stage] || ts >= (latest[stage].ts || 0)) { latest[stage] = { status: v.status ?? null, reason: v.reason ?? '', ts }; } } } return latest; } catch { return {}; } } /** * Сводит агрегированный снимок к четырём звеньям. Для звена с вариантами стадий берётся запись * с наибольшим ts. Поле = {status, reason} либо null (звено не выносило вердикта). */ export function extractFourVerdicts(snapshot) { const s = snapshot && typeof snapshot === 'object' ? snapshot : {}; const pick = (...stages) => { let best = null; for (const st of stages) { const v = s[st]; if (v && (!best || (Number(v.ts) || 0) >= (Number(best.ts) || 0))) best = v; } return best ? { status: best.status ?? null, reason: best.reason ?? '' } : null; }; return { router: pick('router'), mentor: pick('mentor:plan', 'mentor:spec'), judge: pick('judge:plan', 'judge:spec'), gate3: pick('judge:gate3', 'judge:gate3card'), }; }