397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
39 lines
2.1 KiB
JavaScript
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 };
|