a0a73d8d3a
Δ3: убрано обещание «атомарно на исполнении» (PreToolUse не видит факт). Достижимый максимум — два такта: - 8.1 (runGate): пред-запись НАМЕРЕНИЯ в журнал ДО allow. Журнал вернул false ИЛИ бросил → стена НЕ разрешает (block), указатель не двигается («нет записи → нет действия», явно). Backward-compat: push → length (truthy) = успех; только явный false/throw → block. - 8.2 (enforce-reconcile.mjs, новый): PostToolUse-сверка. reconcileAction — исполненное действие без пред-записи → action-without-record (возможен обход). findOrphanIntents — пред-записи без исполнения → record-without-action. WARN-уровень (не блок: PreToolUse-пол уже отработал, PostToolUse не отменяет исполненное). Чистые функции + fail-quiet I/O main. +2 (supreme-gate runGate) +5 (reconcile) тестов. Полная tools-only регрессия 2788 + 2 skip (0 регрессий). Машина 5 (Пакеты 5-8) собрана полностью. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
59 lines
3.7 KiB
JavaScript
59 lines
3.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* enforce-reconcile (Машина 5 Пакет 8, Δ3) — PostToolUse-реконсилер двухтактного reconcile.
|
|
*
|
|
* Δ3 честно: PreToolUse-стена (supreme-gate) не видит факт исполнения — она лишь ПРЕД-записывает
|
|
* намерение в журнал ДО allow (8.1). Достижимый второй такт — PostToolUse-сверка «произошло ровно
|
|
* записанное»:
|
|
* reconcileAction — исполненное действие имеет совпадающую пред-запись? нет → action-without-record
|
|
* (действие проскользнуло мимо пред-записи стены — возможен обход).
|
|
* findOrphanIntents — пред-записи без соответствующего исполнения → record-without-action.
|
|
*
|
|
* WARN-уровень (НЕ блок): PreToolUse-пол уже отработал; расхождение — сигнал владельцу, а не
|
|
* остановка (PostToolUse не может отменить уже исполненное). Чистые функции + fail-quiet I/O main.
|
|
*/
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
function sameAction(entry, action) {
|
|
return !!entry && !!action
|
|
&& String(entry.op) === String(action.op)
|
|
&& String(entry.object) === String(action.object);
|
|
}
|
|
|
|
/** Исполненное действие имеет совпадающую пред-запись в журнале намерений? */
|
|
export function reconcileAction({ action, journalEntries = [] }) {
|
|
const matched = (journalEntries || []).some((e) => sameAction(e, action));
|
|
if (matched) return { matched: true };
|
|
return {
|
|
matched: false,
|
|
flag: 'action-without-record',
|
|
reason: `действие ${action && action.op} «${action && action.object}» исполнено без журнальной пред-записи (возможен обход стены)`,
|
|
};
|
|
}
|
|
|
|
/** Пред-записи (намерения), под которые НЕ было исполнения → сироты (record-without-action). */
|
|
export function findOrphanIntents({ journalEntries = [], executedActions = [] }) {
|
|
const orphans = (journalEntries || []).filter(
|
|
(e) => !(executedActions || []).some((a) => sameAction(e, a)),
|
|
);
|
|
return { ok: orphans.length === 0, orphans, flag: orphans.length ? 'record-without-action' : null };
|
|
}
|
|
|
|
// I/O-обёртка PostToolUse: сверяет факт-действие с журналом намерений; расхождение → WARN в stderr,
|
|
// НИКОГДА не блокирует (exit 0). Fail-quiet. Загрузка журнала/событий — owner-активируемая живая часть;
|
|
// здесь — минимальный каркас (реальный парс журнала добавляется при регистрации хука владельцем).
|
|
async function main() {
|
|
try {
|
|
let input = '';
|
|
for await (const chunk of process.stdin) input += chunk;
|
|
let event; try { event = JSON.parse(input); } catch { process.exit(0); }
|
|
// Реконсиляция требует загруженного журнала намерений сессии; при регистрации владелец
|
|
// прокидывает реальный reader. Без него — каркас не кричит (нет данных = нет сигнала).
|
|
void event; void reconcileAction; void findOrphanIntents;
|
|
} catch { /* fail-quiet: реконсилер — сигнал, не блок */ }
|
|
process.exit(0);
|
|
}
|
|
|
|
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1];
|
|
if (isCli) main();
|