Files
portal/tools/guard-block-log.mjs
T
Дмитрий 84231a1470 feat(board): live source for guard board escapes/blocks (D-3)
Доска «кто на посту» (STATUS.md §7) теперь показывает реальные недавние
escape владельца и блоки машин М1–М6 вместо хардкода []/[].

- new tools/guard-block-log.mjs: logGuardBlock (best-effort, fail-quiet,
  Node fs append в guard-blocks-<sess>.jsonl) + loadRecentBlocks/
  loadRecentEscapes (скан session-файлов runtime, окно 24ч + cap 10, ts→ISO).
- проводка logGuardBlock в block-ветку main() 9 машинных хуков (floor /
  supreme-gate / judge-gate / snapshot / read-path-deny / mcp-classification /
  normative-content-rules / verify-gate / criterion-gate). Логгер вызывается
  ПОСЛЕ решения, не влияет на block; decide() pure не тронут.
- status-md-generator CLI: recentEscapes/recentBlocks из читателей вместо []/[].

До флипа Фазы 8 доска показывает 0/0 (хуки не зарегистрированы — данных нет);
реальная польза — пост-флип наблюдаемость.

TDD: guard-block-log.test (6) + 9 структурных wiring-тестов + 1 board-тест.
Гейт закрытия: sharp-edges (промежуточный по 9 хукам + читатели) +
variant-analysis (все block-ветки покрыты, иных источников нет). Регрессия
tools-only 3465 passed / 2 skipped / 0 failed (было 3449+2skip). 0 регрессий.

Plan: docs/superpowers/plans/2026-06-10-guard-board-live-source.md
2026-06-10 04:28:53 +03:00

71 lines
3.8 KiB
JavaScript

#!/usr/bin/env node
/**
* guard-block-log — журнал блоков обороны М1–М6 для доски «кто на посту» (D-3).
* Логгер пишут машинные хуки при РЕШЁННОМ блоке (best-effort, fail-quiet, Node fs —
* как logVerdictLine/logViolation). Читатели сканируют все session-файлы runtime для
* глобальной доски (board-генератор не имеет одного session_id). Достоверность журнала —
* при зарегистрированном поле-страже runtime (Фаза 8); до флипа данных нет (0/0).
*/
import fsDefault from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
import { canonicalAction } from './escape-grant.mjs';
function defaultRuntimeDir() { return join(homedir(), '.claude', 'runtime'); }
/** Чистая запись блока. */
export function buildGuardBlockEntry({ machine, action, reason, now }) {
return { ts: now, machine: String(machine ?? ''), action: String(action ?? ''), reason: String(reason ?? '') };
}
/** Best-effort: записать блок машины. action — из canonicalAction(event); sess — из event.session_id.
* НИКОГДА не бросает (вызывается в block-ветке хука; сбой логгирования не влияет на блок). */
export function logGuardBlock(event, machine, reason, { fsImpl = fsDefault, runtimeDir = defaultRuntimeDir(), now = Date.now() } = {}) {
try {
const action = canonicalAction(event && event.tool_name, (event && event.tool_input) || {});
const sess = (event && event.session_id) || 'unknown';
const entry = buildGuardBlockEntry({ machine, action, reason, now });
fsImpl.mkdirSync(runtimeDir, { recursive: true });
fsImpl.appendFileSync(join(runtimeDir, `guard-blocks-${sess}.jsonl`), JSON.stringify(entry) + '\n');
} catch { /* fail-quiet */ }
}
function scanSessionFiles(fsImpl, runtimeDir, prefix) {
let names = [];
try { names = fsImpl.readdirSync(runtimeDir).filter((f) => f.startsWith(prefix) && f.endsWith('.jsonl')); }
catch { return []; }
const out = [];
for (const name of names) {
let raw; try { raw = fsImpl.readFileSync(join(runtimeDir, name), 'utf8'); } catch { continue; }
for (const line of String(raw).split(/\r?\n/)) {
const t = line.trim(); if (!t) continue;
let r; try { r = JSON.parse(t); } catch { continue; }
out.push(r);
}
}
return out;
}
function windowSortCap(recs, { now, windowMs, limit }) {
return recs
.filter((r) => r && typeof r.ts === 'number' && now - r.ts >= 0 && now - r.ts <= windowMs)
.sort((a, b) => b.ts - a.ts)
.slice(0, limit)
.map((r) => ({ ...r, ts: new Date(r.ts).toISOString() }));
}
/** Недавние блоки машин для доски. */
export function loadRecentBlocks({ fsImpl = fsDefault, runtimeDir = defaultRuntimeDir(), now = Date.now(), windowMs = 86400000, limit = 10 } = {}) {
const recs = scanSessionFiles(fsImpl, runtimeDir, 'guard-blocks-')
.map((r) => ({ ts: r.ts, machine: r.machine, action: r.action, reason: r.reason }));
return windowSortCap(recs, { now, windowMs, limit });
}
/** Недавние escape владельца (floor_escape) для доски. */
export function loadRecentEscapes({ fsImpl = fsDefault, runtimeDir = defaultRuntimeDir(), now = Date.now(), windowMs = 86400000, limit = 10 } = {}) {
const recs = scanSessionFiles(fsImpl, runtimeDir, 'askuser-decisions-')
.filter((r) => r && r.type === 'floor_escape' && typeof r.action === 'string')
.map((r) => ({ ts: typeof r.ts === 'number' ? r.ts : 0, machine: 'escape', action: r.action, reason: 'escape владельца' }));
return windowSortCap(recs, { now, windowMs, limit });
}