Files
brain/tools/verdict-surface-store.mjs
T

85 lines
3.6 KiB
JavaScript

#!/usr/bin/env node
/** verdict-surface-store — пер-сессионный транзиентный стор вердиктов + pending-ack маркер (SP1).
* Fail-quiet: любая ошибка → безопасный no-op/пусто. Дефолт baseDir = ~/.claude/runtime. */
import { homedir } from 'node:os';
import { join } from 'node:path';
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
function baseOf(baseDir) { return baseDir || join(homedir(), '.claude', 'runtime'); }
function queuePath(sid, baseDir) { return join(baseOf(baseDir), `verdict-surface-${sid || 'unknown'}.json`); }
function ackPath(sid, baseDir) { return join(baseOf(baseDir), `verdict-pending-ack-${sid || 'unknown'}.json`); }
function readJson(p) { try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return null; } }
function ensureDir(p) { try { mkdirSync(baseOf(p), { recursive: true }); } catch {} }
export function pushVerdict(sid, obj, baseDir) {
try {
ensureDir(baseDir);
const p = queuePath(sid, baseDir);
let arr = existsSync(p) ? readJson(p) : [];
if (!Array.isArray(arr)) arr = [];
arr.push(obj);
writeFileSync(p, JSON.stringify(arr));
return true;
} catch { return false; }
}
export function drainVerdicts(sid, baseDir) {
try {
const p = queuePath(sid, baseDir);
if (!existsSync(p)) return [];
const arr = readJson(p);
try { writeFileSync(p, '[]'); } catch {}
return Array.isArray(arr) ? arr : [];
} catch { return []; }
}
export function markSurfaced(sid, outcomes, baseDir) {
try { ensureDir(baseDir); writeFileSync(ackPath(sid, baseDir), JSON.stringify(outcomes || [])); return true; }
catch { return false; }
}
export function readPendingAck(sid, baseDir) {
try {
const p = ackPath(sid, baseDir);
if (!existsSync(p)) return null;
const v = readJson(p);
return Array.isArray(v) ? v : null;
} catch { return null; }
}
export function clearPendingAck(sid, baseDir) {
try { const p = ackPath(sid, baseDir); if (existsSync(p)) writeFileSync(p, 'null'); return true; }
catch { return false; }
}
// Снимок стадий (видимость «всё в лоб»): персистентная карта решений по артефакту, читаемая в любой
// момент (в отличие от очереди, дренящейся один раз). Файл = { [hash]: { [stage]: {status,reason,ts} } }.
function snapshotPath(sid, baseDir) { return join(baseOf(baseDir), `verdict-snapshot-${sid || 'unknown'}.json`); }
/** Upsert стадии решения по hash артефакта. status: pending → GO|NO-GO|degraded|skip|recommend|sealed. */
export function writeStage(sid, { stage, hash, status, reason = '', ts = 0 } = {}, baseDir) {
try {
if (!stage || !hash) return false;
ensureDir(baseDir);
const p = snapshotPath(sid, baseDir);
let obj = existsSync(p) ? readJson(p) : {};
if (!obj || typeof obj !== 'object') obj = {};
if (!obj[hash] || typeof obj[hash] !== 'object') obj[hash] = {};
obj[hash][stage] = { status, reason, ts };
writeFileSync(p, JSON.stringify(obj));
return true;
} catch { return false; }
}
/** Карта стадий по hash: { [stage]: {status,reason,ts} } или {} (неизвестный hash / сбой). */
export function readSnapshot(sid, hash, baseDir) {
try {
const p = snapshotPath(sid, baseDir);
if (!existsSync(p)) return {};
const obj = readJson(p);
if (!obj || typeof obj !== 'object') return {};
const v = obj[hash];
return v && typeof v === 'object' ? v : {};
} catch { return {}; }
}