feat(log): the guarded log-authority core — group-commit durable-at-ack, the per-brain switch, the verification oracle
All checks were successful
CI / Node 22 (push) Successful in 12m14s
CI / Node 24 (push) Successful in 12m7s
CI / Bun (latest) (push) Successful in 12m20s

The storage-authority adoption path, guarded shape: the canonical tree
stays authoritative by default ('tree'); a brain flips to 'log' only
through the verification oracle, and the flip is stored, per-brain,
checked at open only.

- FactLog.ensureSynced(): classic group commit — concurrent writers
  append, then join ONE covering fsync (running + queued slots give the
  covering guarantee: the sync a caller awaits always starts after its
  append landed). Solo writer = immediate sync.
- GenerationStore.logDurability 'deferred' (default, byte-identical to
  today: fact durability rides the group-commit flush, ack latency
  unchanged) | 'at-ack' (log-authority mode: every single-op ack awaits a
  covering log fsync — an acked write's fact survives power loss, by
  contract). transact() was already durable-at-return in both modes.
- src/db/logAuthority.ts: the stored switch artifact
  (_system/log-authority.json, absent = tree), readLogAuthority, and the
  VERIFICATION ORACLE — replay the fact log, fold latest state per id
  (digests, never bodies — memory-bounded), diff against the canonical
  tree paged; verdict green iff every canonical row is exactly reproduced
  AND the log claims nothing canonical denies. Divergences are NAMED by
  class (pre-log-record → needs baseline backfill; state-differs;
  log-live-canonical-absent; log-tombstone-canonical-present). The flip
  REFUSES on red with the first divergence and the cure in the message.
- Brainy: authority read at open (log → durable-at-ack enabled);
  logAuthority() / verifyLogAuthority() / adoptLogAuthority() public API.

Nothing flips by itself; nothing changes for existing brains.
This commit is contained in:
David Snelling 2026-08-06 10:08:18 -07:00
parent 9fda6d9566
commit 6595309765
4 changed files with 409 additions and 3 deletions

View file

@ -442,6 +442,50 @@ export class FactLog {
await this.storage.syncRawObjects(paths)
}
// --- GROUP COMMIT ON THE LOG (durable-at-ack mode) ------------------------
// Classic group commit: concurrent writers append, then join ONE fsync
// whose completion releases every covered ack. Two slots — the running
// sync and at most one queued behind it — give the covering guarantee:
// an append followed by ensureSynced() is always covered, because the
// sync it awaits STARTS after the append landed (a running sync that
// may have snapshotted earlier is never joined; the queued one is).
private syncRunning: Promise<void> | null = null
private syncQueued: Promise<void> | null = null
/**
* Await a sync that covers every byte appended before this call. Many
* concurrent callers share one fsync (solo caller = immediate sync). The
* durability contract of an acked write in log-durable mode: this promise
* resolving means the caller's frames survive power loss.
*/
async ensureSynced(): Promise<void> {
if (this.syncQueued) {
// A sync that has NOT started yet exists — it will snapshot after our
// append, so it covers us.
return this.syncQueued
}
if (this.syncRunning) {
// The running sync may have snapshotted before our append — queue the
// next one behind it and join that.
const queued = this.syncRunning
.catch(() => {})
.then(() => {
// Promote: the queued sync becomes the running one.
this.syncQueued = null
this.syncRunning = this.sync().finally(() => {
this.syncRunning = null
})
return this.syncRunning
})
this.syncQueued = queued
return queued
}
this.syncRunning = this.sync().finally(() => {
this.syncRunning = null
})
return this.syncRunning
}
/**
* Open a scan over committed facts. The scan runs against a MANIFEST
* SNAPSHOT (sealed segments + the tail's decoded facts at open) exactly-