#!/usr/bin/env node /** * mentor-journal-store — персист журнала переговоров наставника (F-C2-4) + вердикта. * Формат журнала = action-journal (jsonl-цепь + head-подпись [action-journal.mjs:83-119]); * файлы mentor-journal-.jsonl/.head + mentor-verdict-.json. Atomic temp→rename * (зеркало saveTaskId F-C1 / writeAtomic plan-lock). Node fs — НЕ Write-tool. */ import fsDefault from 'node:fs'; import { assertSafeSessionId } from './action-journal.mjs'; function paths(runtimeDir, sessionId) { assertSafeSessionId(sessionId); const sep = runtimeDir.endsWith('/') ? '' : '/'; return { jsonl: `${runtimeDir}${sep}mentor-journal-${sessionId}.jsonl`, head: `${runtimeDir}${sep}mentor-journal-${sessionId}.head`, verdict: `${runtimeDir}${sep}mentor-verdict-${sessionId}.json`, }; } function writeAtomic(path, data, fsImpl) { const tmp = `${path}.tmp`; fsImpl.writeFileSync(tmp, data); fsImpl.renameSync(tmp, path); } /** Загрузить цепь переговоров (F-C2-4: ИМЕННО это идёт в onPlanWrite.journalEntries). */ export function loadMentorJournal({ sessionId, runtimeDir, fsImpl = fsDefault }) { const { jsonl, head } = paths(runtimeDir, sessionId); let entries = []; try { entries = String(fsImpl.readFileSync(jsonl, 'utf8')).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 }; } /** Персист полной цепи (выход onPlanWrite.journal = {entries, headSig}) — atomic. */ export function persistMentorJournal({ journal, sessionId, runtimeDir, fsImpl = fsDefault }) { const { jsonl, head } = paths(runtimeDir, sessionId); const lines = (journal && Array.isArray(journal.entries) ? journal.entries : []).map((e) => JSON.stringify(e)).join('\n'); writeAtomic(jsonl, lines ? lines + '\n' : '', fsImpl); writeAtomic(head, String((journal && journal.headSig) ?? ''), fsImpl); } /** Персист/чтение последнего вердикта наставника (потребитель — freeze-gate в пути печати). */ export function persistMentorVerdict({ record, sessionId, runtimeDir, fsImpl = fsDefault }) { writeAtomic(paths(runtimeDir, sessionId).verdict, JSON.stringify(record ?? null), fsImpl); } export function loadMentorVerdict({ sessionId, runtimeDir, fsImpl = fsDefault }) { try { return JSON.parse(fsImpl.readFileSync(paths(runtimeDir, sessionId).verdict, 'utf8')); } catch (e) { if (e && e.code === 'ENOENT') return null; throw e; } }