Phase 1 of #139.
Goal
Introduce a StorageAdapter interface inside @relayburn/ledger and refactor the existing filesystem code to live behind a FileAdapter implementation. Zero behavior change — same JSONL output, same sidecars, same locks, byte-identical artifacts. This phase is purely the seam; no new adapter ships.
This is the foundation every other phase builds on, and the phase with the largest regression surface (it touches writer.ts, reader.ts, content.ts, lock.ts, archive.ts).
Interface
packages/ledger/src/adapters/adapter.ts:
export interface StorageAdapter {
readonly kind: 'file' | 'sqlite' | 'postgres' | 'http';
appendTurns(turns: TurnRecord[]): Promise<void>;
appendCompactions(events: CompactionEvent[]): Promise<void>;
appendRelationships(records: SessionRelationshipRecord[]): Promise<void>;
appendToolResultEvents(events: ToolResultEventRecord[]): Promise<void>;
appendUserTurns(records: UserTurnRecord[]): Promise<void>;
appendStamp(stamp: StampLine): Promise<void>;
appendContent(records: ContentRecord[]): Promise<void>;
queryTurns(q: Query): AsyncIterable<EnrichedTurn>;
queryCompactions(q: Query): AsyncIterable<CompactionEvent>;
queryRelationships(q: Query): AsyncIterable<SessionRelationshipRecord>;
queryToolResultEvents(q: Query): AsyncIterable<ToolResultEventRecord>;
queryUserTurns(q: Query): AsyncIterable<UserTurnRecord>;
readContent(selector: ReadContentSelector): AsyncIterable<ContentLine>;
listContentSessionIds(): Promise<string[]>;
pruneContent(opts: PruneOptions): Promise<PruneResult>;
withLock<T>(name: string, fn: () => Promise<T>): Promise<T>;
init(): Promise<void>;
close(): Promise<void>;
}
packages/ledger/src/adapters/factory.ts exposes a process-singleton getAdapter() that resolves from RELAYBURN_STORAGE (defaults to file).
Refactor
- New:
packages/ledger/src/adapters/{adapter,factory,file-adapter}.ts.
- Modified: the public functions in
writer.ts, reader.ts, content.ts, lock.ts become 1-line wrappers delegating to getAdapter(). Their internal helpers (appendLines, streamLines, loadIndex, FS lock impl) move into FileAdapter.
- Modified:
archive.ts becomes FileAdapter-internal — it's a JSONL-specific read cache and isn't part of the cross-adapter contract. Its public functions (buildArchive, openArchive, getArchiveStatus, rebuildArchive) stay re-exported for backwards compat but become no-ops on non-file adapters (relevant only in later phases).
- Reused unchanged:
index-sidecar.ts (pure dedup-hash functions used by every adapter), schema.ts (record types), cursors.ts/hwm.ts/plans.ts/config.ts (host-local state, stays on the filesystem).
- Modified:
index.ts re-exports StorageAdapter, getAdapter, the adapter-kind type.
Verification
- All existing
pnpm test suites (ledger.test.ts, content.test.ts, lock.test.ts, archive.test.ts, cursors.test.ts, plans.test.ts) pass unchanged — they exercise the FileAdapter through the unchanged public API.
relayburn ingest against a real ~/.claude directory produces a byte-identical ledger.jsonl and content/ tree before vs. after the refactor.
relayburn analyze --json returns identical output.
Phase 1 of #139.
Goal
Introduce a
StorageAdapterinterface inside@relayburn/ledgerand refactor the existing filesystem code to live behind aFileAdapterimplementation. Zero behavior change — same JSONL output, same sidecars, same locks, byte-identical artifacts. This phase is purely the seam; no new adapter ships.This is the foundation every other phase builds on, and the phase with the largest regression surface (it touches
writer.ts,reader.ts,content.ts,lock.ts,archive.ts).Interface
packages/ledger/src/adapters/adapter.ts:packages/ledger/src/adapters/factory.tsexposes a process-singletongetAdapter()that resolves fromRELAYBURN_STORAGE(defaults tofile).Refactor
packages/ledger/src/adapters/{adapter,factory,file-adapter}.ts.writer.ts,reader.ts,content.ts,lock.tsbecome 1-line wrappers delegating togetAdapter(). Their internal helpers (appendLines,streamLines,loadIndex, FS lock impl) move intoFileAdapter.archive.tsbecomes FileAdapter-internal — it's a JSONL-specific read cache and isn't part of the cross-adapter contract. Its public functions (buildArchive,openArchive,getArchiveStatus,rebuildArchive) stay re-exported for backwards compat but become no-ops on non-fileadapters (relevant only in later phases).index-sidecar.ts(pure dedup-hash functions used by every adapter),schema.ts(record types),cursors.ts/hwm.ts/plans.ts/config.ts(host-local state, stays on the filesystem).index.tsre-exportsStorageAdapter,getAdapter, the adapter-kind type.Verification
pnpm testsuites (ledger.test.ts,content.test.ts,lock.test.ts,archive.test.ts,cursors.test.ts,plans.test.ts) pass unchanged — they exercise the FileAdapter through the unchanged public API.relayburn ingestagainst a real~/.claudedirectory produces a byte-identicalledger.jsonlandcontent/tree before vs. after the refactor.relayburn analyze --jsonreturns identical output.