# `plugin-ledger.ts` _(no submodules)_ ## Purpose `PluginLedger` is the host-owned metadata store backing every plugin record the API host orchestrates. It persists the §4 plugin row (state, version, manifest hash, isolation tier, crash + install-failure counters, capability set) and the §8.1a rollback-journal subset (ring buffer of `(specifier, manifestHash, migrationCursor, capturedAt, version)` tuples). The store is the source of truth that survives host restart — the in-memory `WeakRef` from `@suaveplan/api-loader` stays in memory, but the persisted subset round-trips through this ledger so a post-restart rollback only costs one extra `import()`. The ledger is intentionally driver-agnostic: it consumes a structural `LedgerDb` (a SQLite-shaped `prepare(sql).run(...) / .all(...) / .get(...)`) seam so `bun:sqlite`, `better-sqlite3`, or any thin wrapper can wire in without recompilation. Forward-only migrations install the host-owned tables idempotently — re-instantiating a `PluginLedger` against an already-migrated DB is a no-op. ## Features - Forward-only idempotent migration runner (`HostMigrationRunner`). - Schema covers the five §4 host-owned tables — the active plugin ledger + rollback journal, plus reserved schemas for Stage-6 saga + 2PC. - Ring-buffer trim with `(plugin_name, position)` composite PK so the oldest entry is overwritten when depth (default 3) is exceeded. - Adapter views (`asLifecycleLedger`, `asRollbackPersistence`) hand the lifecycle + reload drivers their narrow ports. - Zod-validated journal rows — corrupt entries raise `LEDGER.INVALID_ROW`. ## Usage: Basic ```ts import { Database } from "bun:sqlite"; import { PluginLedger } from "@suaveplan/api-host/plugin-ledger"; const db = new Database(":memory:"); const ledger = new PluginLedger({ db }); ledger.upsertPlugin({ name: "@acme/billing", version: "1.4.2", manifestHash: "sha256:abc", isolation: "trusted", state: "started", lastTransitionAt: null, installFailures: 0, crashCount: 0, crashBudgetResetsAt: null, capabilities: [{ id: "audit-logger", version: "1.0.0" }], }); const all = ledger.listPlugins(); ``` ## Usage: Advanced ```ts // Hand the rollback driver its persisted journal seam. import { ReloadDriver } from "@suaveplan/api-host/hot-reload"; const driver = new ReloadDriver({ // ..., adapters: { // ..., persistence: ledger.asRollbackPersistence(), }, }); // Push a journal entry directly (e.g. on initial activation). ledger.pushJournalEntry("@acme/billing", { specifier: "file:///opt/plugins/billing/dist/index.js?v=1", manifestHash: "sha256:abc", migrationCursor: 7, capturedAt: new Date().toISOString(), version: "1.4.2", }); const latest = ledger.latestJournal("@acme/billing"); ``` ## API Reference - `new PluginLedger({ db, journalDepth?, now?, autoMigrate? })` — opens the ledger and runs migrations unless `autoMigrate === false`. - `upsertPlugin(record): PluginLedgerRow` — insert-or-replace. - `getPlugin(name): PluginLedgerRow | null` - `listPlugins(): readonly PluginLedgerRow[]` - `removePlugin(name): boolean` - `pushJournalEntry(plugin, entry)` — appends with ring-buffer trim. - `peekJournal(plugin, n): readonly PersistedRollbackEntry[]` - `latestJournal(plugin): PersistedRollbackEntry | null` - `journalLength(plugin): number` - `asLifecycleLedger(): LedgerAdapter` - `asRollbackPersistence(): RollbackPersistence` ## Implementation Notes The ring-buffer trim chooses the oldest entry by `(captured_at, position)` so ties (same timestamp on two pushes) deterministically pick the lower position. The composite primary key `(plugin_name, position)` lets the upsert `ON CONFLICT` clause overwrite the chosen slot atomically. `asLifecycleLedger().write` preserves the existing isolation tier and counter values when the lifecycle driver writes a partial row. The caller is expected to call `upsertPlugin` with the full record once the manifest is parsed. `materialiseRow` re-parses the JSON-serialised capability set on every read so the in-memory shape is always validated. A malformed `capabilities_json` raises `LEDGER.INVALID_ROW` rather than passing through a half-typed array — operators see the corrupt row early.