Files
brain/tools/judge-watermark.mjs
T

39 lines
2.1 KiB
JavaScript

#!/usr/bin/env node
/**
* judge-watermark (K6, §11/§2; F2 per-session) — анти-откатная монотонная зарубка.
* Журнал/план/указатель шага лежат в runtime и ПОДПИСАНЫ, но СТАРАЯ подпись валидна
* → подмена на раннюю валидную пару файлов = откат (спрятать прошлое / отмотать шаг).
* verifyChain это НЕ ловит (печать цела, просто старая). Лечение: max(seq/шаг),
* виденный судьёй, хранится в OS-keychain рядом с ключом судьи — ВНЕ откатываемого
* runtime. current < виденного → откат → блок. Per-session: свежая сессия = генезис 0
* (это не обход — там отката нет). keychainGet/Set инъектируются (тесты).
*/
const SERVICE = 'router-mentor-judge-watermark';
function account(sessionId) { return `wm:${sessionId}`; }
export function readWatermark({ sessionId, keychainGet }) {
let raw = null;
try { raw = keychainGet({ service: SERVICE, account: account(sessionId) }); } catch { raw = null; }
const n = Number.parseInt(String(raw ?? ''), 10);
return Number.isFinite(n) && n >= 0 ? n : 0; // genesis 0 при отсутствии/мусоре
}
/** Поднять зарубку ТОЛЬКО вверх (монотонно). */
export function bumpWatermark({ sessionId, value, keychainGet, keychainSet }) {
const cur = readWatermark({ sessionId, keychainGet });
const next = Math.max(cur, Number.isFinite(value) ? value : cur);
if (next !== cur) keychainSet({ service: SERVICE, account: account(sessionId), value: next });
return next;
}
/** current ниже виденного → откат → блок. */
export function checkNoRollback({ sessionId, current, keychainGet }) {
const seen = readWatermark({ sessionId, keychainGet });
const cur = Number.isFinite(current) ? current : 0;
const rolledBack = cur < seen;
return { ok: !rolledBack, rolledBack, seen };
}
export const _internals = { SERVICE };