#!/usr/bin/env node /** * judge-subrun-journal (F1, §9.1) — НЕподделываемый контроллером канал прилежности * судьи. Под-прогон (реальный запуск думающего навыка судьёй) пишется в ОТДЕЛЬНЫЙ * judge-only файл judge-subrun-.jsonl, цепочка+голова подписаны КЛЮЧОМ СУДЬИ * (router-mentor-judge). Контроллер пишет только в общий action-journal — туда * запись «судья прогнал X» доверять НЕЛЬЗЯ; доверяем лишь этому каналу под judge-ключом. * Переиспользует хеш-цепочку Машины 1 (appendEntry/verifyChain), отдельный путь+ключ. */ import fsDefault from 'node:fs'; import { appendEntry, verifyChain, assertSafeSessionId } from './action-journal.mjs'; function paths(runtimeDir, sessionId) { assertSafeSessionId(sessionId); // N3-shared: тот же guard формы, что в action-journal const sep = runtimeDir.endsWith('/') ? '' : '/'; return { jsonl: `${runtimeDir}${sep}judge-subrun-${sessionId}.jsonl`, head: `${runtimeDir}${sep}judge-subrun-${sessionId}.head`, }; } /** Прочитать judge-only журнал под-прогонов (+ подпись головы). Нет файла → пусто. */ export function loadJudgeSubRuns({ sessionId, runtimeDir, fsImpl = fsDefault }) { const { jsonl, head } = paths(runtimeDir, sessionId); let entries = []; try { const raw = fsImpl.readFileSync(jsonl, 'utf8'); entries = String(raw).split('\n').filter(Boolean).map((l) => JSON.parse(l)); } catch (e) { if (e && e.code === 'ENOENT') return { entries: [], headSig: null }; throw e; } let headSig = null; try { headSig = String(fsImpl.readFileSync(head, 'utf8')).trim() || null; } catch { headSig = null; } return { entries, headSig }; } /** Добавить под-прогон судьи (персист через Node fs, подпись головы judge-ключом). */ export function appendJudgeSubRun({ payload, key, sessionId, runtimeDir, nowMs, fsImpl = fsDefault }) { const { entries } = loadJudgeSubRuns({ sessionId, runtimeDir, fsImpl }); const { entries: next, headSig } = appendEntry(entries, payload, { key, nowMs }); const entry = next[next.length - 1]; const { jsonl, head } = paths(runtimeDir, sessionId); fsImpl.appendFileSync(jsonl, JSON.stringify(entry) + '\n'); fsImpl.writeFileSync(head, String(headSig ?? '')); return { entry, headSig }; } /** Цепочка цела И голова подписана judge-ключом? (контроллер без ключа → false). */ export function verifyJudgeJournal(entries, headSig, judgeKey) { return verifyChain(entries, headSig, { key: judgeKey }); } /** * Дисциплина #1: для каждой требующей реального запуска линзы вердикта в журнале * должен быть совпавший под-прогон. required: [{lens, skill?}]; subRuns: payload'ы. * Совпадение по lens (+ skill, если задан в required). Нет → missing (вердикт фейк). */ export function requiredSubRunsPresent(required = [], subRuns = []) { const missing = (required || []).filter((req) => !(subRuns || []).some((r) => r.lens === req.lens && (req.skill === undefined || r.skill === req.skill)) ); return { ok: missing.length === 0, missing }; }