/*
ADR-0092: passport staleness helpers — classify bordbuch drift as
bookkeeping-only (propagation records such as mirror-sync) versus
state-changing ledger activity. Used by sternsystem.passport.verify and
sternsystem.validate (PASSPORT-03) so that a sync run no longer marks a fresh
passport stale.
- Does not read the bordbuch — callers pass entries from readBordbuch.
- Does not define a bordbuch entry-kind taxonomy — the bookkeeping set is a local constant until a schema-driven classification supersedes it.
- ADR-0092: initial staleness helpers — BOOKKEEPING_BORDBUCH_KINDS, isBordbuchDriftBookkeepingOnly, dropTrailingBookkeepingEntries.
- RFC-1210: closed-loop signal ingest to bounded missions (RFC-1210)
*/
import type { BordbuchEntry, BordbuchEntryKind } from "@warpgogol/werkstatt-engine/schemas";
/**
* ADR-0092: bordbuch kinds that record propagation of already-committed state
* rather than a change to it. Deliberately fail-safe: kinds not listed here are
* treated as state-changing, so a new bookkeeping kind must be added
* explicitly or it will (correctly) stale passports.
*/
export const BOOKKEEPING_BORDBUCH_KINDS: ReadonlySet = new Set([
"mirror-sync",
// RFC-1210: remediation-mission records signal-triggered mission opens —
// propagation/bookkeeping of an autonomy decision, not authored state change.
"remediation-mission",
]);
/**
* ADR-0092: true when every entry appended after `recordedHead` is a
* bookkeeping kind — i.e. the ledger recorded propagation only, no state
* change. A `null` recorded head means the passport predates the ledger, so
* every entry counts as drift; the drift is bookkeeping-only iff all entries
* are bookkeeping kinds. A recorded head that no longer resolves in the ledger
* (rewritten history) fails safe: not bookkeeping-only.
*/
export function isBordbuchDriftBookkeepingOnly(
entries: readonly BordbuchEntry[],
recordedHead: string | null,
): boolean {
if (recordedHead === null) {
return entries.length > 0 && entries.every((e) => BOOKKEEPING_BORDBUCH_KINDS.has(e.kind));
}
const headIndex = entries.findIndex((e) => e.hash === recordedHead);
if (headIndex === -1) {
return false;
}
const tail = entries.slice(headIndex + 1);
return tail.length > 0 && tail.every((e) => BOOKKEEPING_BORDBUCH_KINDS.has(e.kind));
}
/**
* ADR-0092: return the ledger without its trailing run of bookkeeping entries —
* the state-significant suffix. Used to evaluate drift rules (e.g. the
* post-handover tolerance) on the entries that actually changed state.
*/
export function dropTrailingBookkeepingEntries(
entries: readonly BordbuchEntry[],
): readonly BordbuchEntry[] {
let end = entries.length;
while (end > 0 && BOOKKEEPING_BORDBUCH_KINDS.has(entries[end - 1].kind)) {
end -= 1;
}
return entries.slice(0, end);
}