@loomwork/plugin-content 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/README.md +92 -2
  4. package/dist/CachingContentStore.d.ts +54 -0
  5. package/dist/CachingContentStore.d.ts.map +1 -0
  6. package/dist/CachingContentStore.js +171 -0
  7. package/dist/CachingContentStore.js.map +1 -0
  8. package/dist/ContentResolvingEventStore.d.ts +70 -0
  9. package/dist/ContentResolvingEventStore.d.ts.map +1 -0
  10. package/dist/ContentResolvingEventStore.js +245 -0
  11. package/dist/ContentResolvingEventStore.js.map +1 -0
  12. package/dist/ContentStore.d.ts +186 -0
  13. package/dist/ContentStore.d.ts.map +1 -0
  14. package/dist/ContentStore.js +177 -0
  15. package/dist/ContentStore.js.map +1 -0
  16. package/dist/ContentTouchingSnapshotStore.d.ts +56 -0
  17. package/dist/ContentTouchingSnapshotStore.d.ts.map +1 -0
  18. package/dist/ContentTouchingSnapshotStore.js +85 -0
  19. package/dist/ContentTouchingSnapshotStore.js.map +1 -0
  20. package/dist/citations.d.ts +40 -0
  21. package/dist/citations.d.ts.map +1 -0
  22. package/dist/citations.js +118 -0
  23. package/dist/citations.js.map +1 -0
  24. package/dist/content.bench.d.ts +2 -0
  25. package/dist/content.bench.d.ts.map +1 -0
  26. package/dist/content.bench.js +75 -0
  27. package/dist/content.bench.js.map +1 -0
  28. package/dist/content.d.ts +40 -0
  29. package/dist/content.d.ts.map +1 -0
  30. package/dist/content.js +177 -0
  31. package/dist/content.js.map +1 -0
  32. package/dist/derive.d.ts +9 -0
  33. package/dist/derive.d.ts.map +1 -0
  34. package/dist/derive.js +40 -0
  35. package/dist/derive.js.map +1 -0
  36. package/dist/errors.d.ts +6 -0
  37. package/dist/errors.d.ts.map +1 -0
  38. package/dist/errors.js +20 -0
  39. package/dist/errors.js.map +1 -0
  40. package/dist/executionEnd.d.ts +22 -0
  41. package/dist/executionEnd.d.ts.map +1 -0
  42. package/dist/executionEnd.js +66 -0
  43. package/dist/executionEnd.js.map +1 -0
  44. package/dist/gc.d.ts +470 -0
  45. package/dist/gc.d.ts.map +1 -0
  46. package/dist/gc.js +819 -0
  47. package/dist/gc.js.map +1 -0
  48. package/dist/index.d.ts +10 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +42 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/touchAround.d.ts +46 -0
  53. package/dist/touchAround.d.ts.map +1 -0
  54. package/dist/touchAround.js +98 -0
  55. package/dist/touchAround.js.map +1 -0
  56. package/package.json +58 -3
  57. package/src/CachingContentStore.ts +184 -0
  58. package/src/ContentResolvingEventStore.ts +325 -0
  59. package/src/ContentStore.ts +317 -0
  60. package/src/ContentTouchingSnapshotStore.ts +109 -0
  61. package/src/citations.ts +117 -0
  62. package/src/content.bench.ts +84 -0
  63. package/src/content.ts +191 -0
  64. package/src/derive.ts +40 -0
  65. package/src/errors.ts +19 -0
  66. package/src/executionEnd.ts +64 -0
  67. package/src/gc.ts +1213 -0
  68. package/src/index.ts +54 -0
  69. package/src/touchAround.ts +131 -0
package/dist/gc.d.ts ADDED
@@ -0,0 +1,470 @@
1
+ import type { ArchiveStore, EventStore, ExecutionEnumerable } from "@loomwork/event-runtime";
2
+ import type { ContentStore } from "./ContentStore";
3
+ /**
4
+ * A {@link PinPredicate} for `LogLifecycle` (K10.6): pin any execution whose log
5
+ * carries at least one event of the designated kinds — e.g. the app's finding
6
+ * events. Use when no archive is configured and reclaiming the run would destroy
7
+ * the only copy of its durable conclusions.
8
+ */
9
+ export declare function pinByEventKinds(eventStore: Pick<EventStore, "read">, kinds: Iterable<string>): (executionId: string) => Promise<boolean>;
10
+ /** Everything but the `dryRun` / `reclaimOrphans` pair, which {@link ContentSweepOptions} ties together. */
11
+ export interface ContentSweepBase {
12
+ /**
13
+ * Every log store whose events may cite content — the live store AND any
14
+ * archive stores. All must be enumerable (`listExecutionsPage()` preferred —
15
+ * SP-2, bounded memory — or `listExecutions()`): a store that cannot be
16
+ * enumerated cannot be swept safely (its pins would be invisible), so the
17
+ * sweep refuses rather than silently under-pinning.
18
+ *
19
+ * List the LIVE store BEFORE its archive. Retention copies an execution to the archive and
20
+ * then deletes it from the live store. Scanned live-first, an execution that moves during
21
+ * the sweep is seen in one store or the other. Scanned archive-first, it can be missed by
22
+ * both, and its blobs then look like orphans.
23
+ */
24
+ eventStores: readonly (Pick<EventStore, "read"> & ExecutionEnumerable)[];
25
+ /**
26
+ * ADR 0055 D6 — where compaction checkpoints live: the live snapshot store AND EACH
27
+ * ARCHIVE'S snapshot store. `LogLifecycle.compact` checkpoints the fold and truncates the
28
+ * prefix, so a ref the prefix cited survives only in snapshot state.
29
+ *
30
+ * Which logs were compacted (ADR 0106, superseding "no sequence-1 event"): compaction
31
+ * removes its checkpoint's slot and, when it keeps pinned-kind events, never the tip. So a
32
+ * gap-free log with more than the bootstrap was never compacted (no snapshot read); a
33
+ * gapped log's checkpoint sits at its highest missing sequence `gap`; a bootstrap-only log
34
+ * was compacted through its tip. For each compacted execution the sweep pins every ref in:
35
+ * - the COMPACTION CHECKPOINT: the lowest snapshot in `[gap, tip]` — exactly `gap` for a
36
+ * compaction's own, the fork seed (`metadata.forkSequence`, one `loadAt`) for a child
37
+ * forked from a compacted parent — or, for a bootstrap-only log, its latest snapshot.
38
+ * ANY snapshot at or above `gap` covers a lost checkpoint, matching `LogLifecycle.compact`,
39
+ * which accepts a base at or above the gap; below `gap` nothing does. `fork()` from any point after it loads that
40
+ * checkpoint, so its refs stay reachable even after a later snapshot overwrote them.
41
+ * - the LATEST snapshot, which resume loads.
42
+ *
43
+ * Snapshots in between hold only checkpoint- or suffix-derived refs, and the suffix is
44
+ * scanned as log. Anything below the checkpoint is unreachable. The refs are PINNED because
45
+ * compaction folded away the event kinds, so the sweep cannot tell telemetry from evidence.
46
+ * They are released when retention deletes the execution, which deletes its snapshots too.
47
+ *
48
+ * `reclaimOrphans` REFUSES, before erasing anything:
49
+ * - without this option, when any scanned store can `truncatePrefix`;
50
+ * - when an execution looks compacted but no checkpoint is found. That covers a gap with no
51
+ * snapshot in `[gap, tip]`, and a sequence-0-only log with no snapshot, which is either
52
+ * compacted at its tip or a run that never got past its bootstrap. The two are
53
+ * indistinguishable, so both refuse.
54
+ */
55
+ snapshotStores?: readonly SnapshotLookup[];
56
+ /**
57
+ * ADR 0138 - the compaction archives (`LogLifecycle.compactionArchive`). Compaction MOVES the events
58
+ * it removes into the archive, so a ref only an archived event cites (a step input the fold no
59
+ * longer keeps, an old finding) is cited THERE and nowhere else. Every ref cited by an archived
60
+ * envelope is PINNED for as long as the archive holds it: the sweep never reclaims a blob the
61
+ * recoverable history still names. Not scanning them is the hazard: an erasing orphan pass without
62
+ * the archive of a compacted log erases blobs its audit trail still cites. Read through the same
63
+ * sealing store the lifecycle uses (an `EncryptingArchiveStore` yields plaintext; a crypto-shredded
64
+ * subject's archive is unreadable and cites nothing). The archive itself is never swept or pruned.
65
+ *
66
+ * REQUIRED (fail closed) by an ERASING `reclaimOrphans` pass whenever any scanned log is compacted
67
+ * (a gap below its tip, or only its bootstrap left): {@link ArchiveStoresRequiredError} otherwise,
68
+ * before anything is erased. Coverage is CHECKED for EVERY compacted log, whatever its checkpoint
69
+ * is called (the `ckpt-0138-` id mark is only used to label reports - it can be lost to pruning, so
70
+ * it never gates an erase): the log's removed range must be covered by the given archives' headers
71
+ * (legacy-checkpoint segments included), else {@link ArchiveStoresRequiredError} (`reason:
72
+ * "uncovered"`). `[]` alone therefore no longer passes a gapped log. The only way past it, for logs
73
+ * compacted before ADR 0138 (no archive exists), is {@link acknowledgeUnarchivedLogs}.
74
+ * The archive is ENUMERATED (`listExecutions`), so segments whose log is gone are scanned
75
+ * too. A segment that is still sealed (count > 0 with no envelopes: the RAW inner store of an
76
+ * `EncryptingArchiveStore`) throws {@link SealedArchiveScanError} on an erasing pass.
77
+ */
78
+ archiveStores?: readonly Pick<ArchiveStore, "read" | "listExecutions" | "headers">[];
79
+ /**
80
+ * Explicit opt-in for an erasing `reclaimOrphans` pass over compacted logs the given
81
+ * `archiveStores` do not cover: "these logs' compacted history is already gone (compacted before
82
+ * ADR 0138 and never re-compacted, or a fork child whose parent's archive is not keyed to it);
83
+ * erasing their orphan blobs is accepted." The affected executions are reported in
84
+ * `unarchivedAcknowledged`. Never a substitute for passing the real archive.
85
+ */
86
+ acknowledgeUnarchivedLogs?: boolean;
87
+ /**
88
+ * The blob store. With `deleteIfUntouchedSince` (SPI 1.3.0) every erase is conditional on
89
+ * the blob's touch time, which closes the re-cite race for a re-cite THROUGH `put` (ADR 0055
90
+ * D5); without it the sweep falls back to `delete` and reports `raceSafe: false`. A citation
91
+ * copied WITHOUT a `put` (a fork's `copyPrefix`, a ref an `append` keeps) is protected by SPI
92
+ * 1.4.0 `touch`, which `reclaimOrphans` requires; see `reclaimOrphans`. `listPage` enables it.
93
+ */
94
+ contentStore: Pick<ContentStore, "delete" | "deleteIfUntouchedSince" | "listPage" | "touchClock" | "touch">;
95
+ /** Event kinds whose cited refs are pinned — retained as long as the citing fact exists. */
96
+ pinnedEventTypes: Iterable<string>;
97
+ /**
98
+ * Reclaim an unpinned ref only when its effective citation time is older than this (ADR 0146:
99
+ * its latest citation, or, for a ref cited by an execution that has ENDED, that execution's end
100
+ * if later). EXACTLY ONE of `olderThanMs` / `policy` must be given.
101
+ */
102
+ olderThanMs?: number;
103
+ /**
104
+ * ADR 0145 - an app-owned retention policy (`retentionPolicy()` from `@loomwork/workflow-runtime`;
105
+ * typed structurally here so this package takes no dependency on it). `content.unpinnedAfterMs`
106
+ * is used as `olderThanMs`. A policy without a `content` section is an error, not a silent no-op.
107
+ * Re-validated structurally (see `resolvePolicyTtl`): unknown keys are refused, and
108
+ * `unpinnedAfterMs >= terminalTtlMs` when the policy carries one (an advisory consistency rule,
109
+ * ADR 0145 D2).
110
+ */
111
+ policy?: {
112
+ terminalTtlMs?: number;
113
+ snapshots?: unknown;
114
+ content?: {
115
+ unpinnedAfterMs: number;
116
+ };
117
+ };
118
+ /**
119
+ * LOG time, in epoch ms: the instant the TTL is measured back from, compared against event
120
+ * `timestamp`s (`now − effective citation time ≥ olderThanMs`, ADR 0146). It is NOT the store's clock. A
121
+ * simulated or replayed log may run far from the store's wall clock; the race guard uses
122
+ * `touchedBefore` for that reason.
123
+ */
124
+ now: number;
125
+ /**
126
+ * STORE time, in epoch ms, on the clock the content store stamps touch times with: the
127
+ * cutoff for the conditional delete and for orphan eligibility (ADR 0055 D5). Default: the
128
+ * store's `touchClock()` read ONCE at the sweep's start, minus `olderThanMs`. A store with
129
+ * `deleteIfUntouchedSince` but no `touchClock` requires it explicitly.
130
+ */
131
+ touchedBefore?: number;
132
+ /**
133
+ * DX-2 — report what WOULD be reclaimed without deleting anything (the
134
+ * `loom sweep content` operator default; `--apply` turns it off).
135
+ */
136
+ dryRun?: boolean;
137
+ /**
138
+ * ADR 0055 D7 — external root sets: content addresses held OUTSIDE the scanned logs (an app
139
+ * table such as a Postgres `experience_refs`). Every yielded address is pinned through both
140
+ * passes. Read once per sweep, after the mark phase. An app that records a new root for
141
+ * bytes the store already holds must `put` them (the put is the touch the conditional
142
+ * delete sees), exactly as a flow does.
143
+ */
144
+ roots?: () => AsyncIterable<string>;
145
+ /**
146
+ * ADR 0055 D6 — also reclaim ORPHANS: blobs no scanned event and no root cites (their
147
+ * execution was deleted with no archive, a crash between `put` and the append, a truncated
148
+ * prefix), once untouched for the TTL. Requires the store's `listPage` and
149
+ * `deleteIfUntouchedSince`; refuses otherwise.
150
+ *
151
+ * ACKNOWLEDGEMENT REQUIRED (ADR 0055 amendment, fail-closed): to ERASE, pass
152
+ * `{ completeScan: true }` (plus `snapshotsTouchContent: true` when `snapshotStores` is given, below). A bare `true` (or any other value) throws
153
+ * {@link ReclaimOrphansAckError} before anything is read or erased. A `dryRun` erases nothing
154
+ * and is how you check completeness, so under `dryRun: true` any truthy value (`true` included)
155
+ * is accepted; see {@link ContentSweepOptions} for the typing. The acknowledgement is your
156
+ * assertion that `eventStores`, `snapshotStores` and `roots` together cover EVERY place that
157
+ * cites content, scanned in the right ORDER: each live store BEFORE its archive (ADR 0055
158
+ * "Scan order"). `[archive, live]` can miss an execution that retention moves mid-sweep, and
159
+ * its blobs then look like orphans.
160
+ *
161
+ * HAZARD the sweep cannot detect, which is why the assertion exists: a citing store that was
162
+ * NOT passed in `eventStores` or `snapshotStores`. Every blob only that store cites is an
163
+ * orphan to this pass, and it is erased while still cited. Pass every archive
164
+ * (`retention.archive`) and each archive's snapshot store, and every app table through `roots`.
165
+ * Run with `dryRun: true` first and read the list.
166
+ *
167
+ * CONCURRENCY (ADR 0055 D6 amendment 2): D5's conditional delete only sees a touch. A `put`
168
+ * touches; a citation that enters a log WITHOUT one does not by itself:
169
+ * `ContentResolvingEventStore.copyPrefix` (a fork) copies refs verbatim, and an `append`
170
+ * keeps refs already inside its payload (recalled memory, `scriptRef`, refs passed between
171
+ * steps). SPI 1.4.0 `touch` closes that: those two paths touch the refs they carry, so a fork
172
+ * made mid-sweep makes `deleteIfUntouchedSince` refuse. To ERASE, the content store MUST
173
+ * implement `touch` or the sweep throws {@link ReclaimOrphansTouchError} (a `dryRun` need not).
174
+ * The log must be written through a `ContentResolvingEventStore` over that store: a writer
175
+ * that copies refs some other way is outside the guarantee.
176
+ *
177
+ * SNAPSHOTS (second path): a fork of a COMPACTED parent saves a seed snapshot whose state
178
+ * cites refs that exist nowhere else (the prefix was truncated). `ContentResolvingEventStore`
179
+ * cannot see it; only a snapshot store wrapped in `ContentTouchingSnapshotStore`
180
+ * (outside any `EncryptingSnapshotStore`, over the same content store) touches those refs.
181
+ * loom never builds either store for the host, so the wrapping is the host's and is not PROVEN
182
+ * from here (a sweep, and the CLI, read a RAW store over the tables a wrapped one wrote, so an
183
+ * unbranded store is no evidence the writer was unwrapped): an ERASING pass given `snapshotStores` must also pass
184
+ * `{ completeScan: true, snapshotsTouchContent: true }` or it throws
185
+ * {@link ReclaimOrphansSnapshotAckError}. Without `snapshotStores` it is not needed (such a
186
+ * pass already refuses a store that can `truncatePrefix`, and a fork of an uncompacted parent
187
+ * copies every event, so its seed cites nothing the copied events do not). A `dryRun` is exempt.
188
+ * The flag stays an assertion about the WRITER and is ALWAYS required, even when every store
189
+ * passed is a `ContentTouchingSnapshotStore` (it vouches for itself, not for the other stores the
190
+ * runtime saves to). Best-effort detection only WARNS: the report counts branded and unbranded
191
+ * stores (`snapshotStoresTouching` / `snapshotStoresUnbranded`), and a pass that is handed a MIX
192
+ * (some branded, some not: a host wrapped some writers and likely forgot others) emits one
193
+ * `LOOM_CONTENT_SNAPSHOT_UNWRAPPED` process warning. It never refuses, so the CLI's raw SQLite
194
+ * stores are not rejected; it never relaxes the flag either (fail-closed).
195
+ *
196
+ * THE GUARANTEE IS D5'S, NOT THE TOUCH'S. The cutoff is `touchClock() - olderThanMs`. The
197
+ * touch BEFORE a write is what makes a sweep whose cutoff is later refuse; so the race is
198
+ * closed while a write's touch-to-commit window is shorter than the margin
199
+ * `store-now - cutoff` (the TTL, or the distance of an explicit `touchedBefore` from the
200
+ * store's clock). The touch AFTER the write is a best-effort extension of that window (it
201
+ * is not retried and an error is reported through `onTouchError`, not raised); do not
202
+ * count on it. With a zero or tiny margin the window is unbounded: a fork whose before-touch
203
+ * is older than the cutoff and which commits after the scan has passed the child is erased
204
+ * under. So an ERASING orphan pass refuses `olderThanMs <= 0` and any cutoff closer to the
205
+ * store's clock than {@link MIN_ORPHAN_ERASE_MARGIN_MS} with {@link ReclaimOrphansMarginError},
206
+ * and a store with no `touchClock()` (the margin cannot be measured) with
207
+ * {@link ReclaimOrphansClockError}; a `dryRun` is unaffected. Pick a margin well above your longest write (a fork's copy of a
208
+ * long prefix included).
209
+ */
210
+ reclaimOrphans?: ReclaimOrphansAck | boolean;
211
+ }
212
+ /**
213
+ * Sweep options. `reclaimOrphans` and `dryRun` are tied by a union so the type says what the
214
+ * runtime enforces: a sweep that can ERASE (`dryRun` absent or `false`) accepts only the
215
+ * acknowledgement object (or `false`), so a bare `true` is a compile error there; a
216
+ * `dryRun: true` sweep also accepts a plain `true`, because it erases nothing and is how a host
217
+ * checks completeness. (A `dryRun` typed as a plain `boolean` falls in the first arm, which is
218
+ * the safe one.)
219
+ */
220
+ export type ContentSweepOptions = Omit<ContentSweepBase, "dryRun" | "reclaimOrphans"> & ({
221
+ dryRun?: false;
222
+ reclaimOrphans?: ReclaimOrphansAck | false;
223
+ } | {
224
+ dryRun: true;
225
+ reclaimOrphans?: ReclaimOrphansAck | boolean;
226
+ });
227
+ /**
228
+ * The caller's assertion that a `reclaimOrphans` sweep sees every citing store. The literal
229
+ * `true` type makes `{ completeScan: false }` a compile error; the runtime check also refuses
230
+ * untyped callers.
231
+ */
232
+ export interface ReclaimOrphansAck {
233
+ completeScan: true;
234
+ /**
235
+ * Required (checked at runtime) whenever an ERASING pass is given `snapshotStores`: the
236
+ * caller's assertion that every snapshot store the runtime SAVES to is wrapped in
237
+ * `ContentTouchingSnapshotStore` (outside any `EncryptingSnapshotStore`). Without it a
238
+ * fork of a compacted parent saves a seed snapshot citing refs that live nowhere else, and
239
+ * a sweep can erase them under the live child. See {@link ReclaimOrphansSnapshotAckError}.
240
+ */
241
+ snapshotsTouchContent?: true;
242
+ }
243
+ /** `reclaimOrphans` was set without `{ completeScan: true }`. Nothing was read or erased. */
244
+ export declare class ReclaimOrphansAckError extends TypeError {
245
+ constructor(received: unknown);
246
+ }
247
+ /**
248
+ * An ERASING `reclaimOrphans` pass was given `snapshotStores` without
249
+ * `{ snapshotsTouchContent: true }`. Nothing was read or erased. A compacted parent's prefix refs
250
+ * live only in snapshot state, and a fork copies that state into its seed snapshot with no `put`
251
+ * and no event; only a snapshot store wrapped in `ContentTouchingSnapshotStore` touches them.
252
+ * The sweep cannot see how the writing runtime's snapshot store was built, so, like D6's
253
+ * archive assertion, the host states it. A `dryRun` is exempt. With no `snapshotStores` the
254
+ * acknowledgement is not needed: the pass then refuses any store that can `truncatePrefix`, and
255
+ * a fork of an UNcompacted parent copies every event (refs touched by `copyPrefix`), so its seed
256
+ * state cites nothing the copied events do not.
257
+ */
258
+ export declare class ReclaimOrphansSnapshotAckError extends TypeError {
259
+ constructor();
260
+ }
261
+ /**
262
+ * `reclaimOrphans` was asked to ERASE through a content store without SPI 1.4.0 `touch`.
263
+ * Nothing was read or erased. Without `touch`, a citation that enters a log with no `put` (a
264
+ * fork's `copyPrefix`, a ref an `append` carries) cannot make the conditional delete refuse, so
265
+ * an orphan sweep concurrent with a fork or a delete can erase a blob a live log still cites
266
+ * (replay then throws `ContentMissingError`). This is a deletion safety boundary: it refuses
267
+ * rather than documenting the hazard. A `dryRun` is unaffected.
268
+ */
269
+ export declare class ReclaimOrphansTouchError extends TypeError {
270
+ constructor();
271
+ }
272
+ /**
273
+ * The smallest margin, in ms, between the content store's clock and the guard cutoff that an
274
+ * ERASING orphan pass accepts (ADR 0055 D6 amendment 2). A write's touch-to-commit window must
275
+ * be shorter than the margin for the touch to protect it; 60 s is far above an append and
276
+ * above a fork's prefix copy at any realistic size, and an operator who needs a bigger one
277
+ * simply passes a larger TTL.
278
+ */
279
+ export declare const MIN_ORPHAN_ERASE_MARGIN_MS = 60000;
280
+ /**
281
+ * An ERASING `reclaimOrphans` pass was configured with a zero/negative `olderThanMs`, or a
282
+ * cutoff (`touchedBefore`) closer to the content store's clock than
283
+ * {@link MIN_ORPHAN_ERASE_MARGIN_MS}. Nothing was read or erased. With no margin the
284
+ * touch-before-write has no window to protect: a write can touch, the sweep read a later cutoff,
285
+ * and the write commit after the scan, so a blob a live log cites is erased.
286
+ */
287
+ export declare class ReclaimOrphansMarginError extends TypeError {
288
+ readonly marginMs: number;
289
+ constructor(marginMs: number);
290
+ }
291
+ /**
292
+ * An ERASING `reclaimOrphans` pass over a content store with `deleteIfUntouchedSince` but no
293
+ * `touchClock()`. Nothing was read or erased. Without the store's clock the sweep cannot measure
294
+ * the margin between "now" and the cutoff (`touchedBefore` is an unchecked caller value, so
295
+ * `touchedBefore: Date.now()` would pass any `olderThanMs`), and the touch-to-commit window is
296
+ * then unbounded. A `dryRun` is exempt.
297
+ */
298
+ export declare class ReclaimOrphansClockError extends TypeError {
299
+ constructor();
300
+ }
301
+ /** Structural view of a snapshot store: just `loadLatest` (no snapshot-runtime dependency). */
302
+ export interface SnapshotLookup {
303
+ loadLatest(executionId: string): Promise<{
304
+ id?: string;
305
+ sequence: number;
306
+ state: unknown;
307
+ } | null>;
308
+ /** Exact-sequence load; used for a forked child's seed (optional — falls back to a walk). */
309
+ loadAt?(executionId: string, sequence: number): Promise<{
310
+ id?: string;
311
+ sequence: number;
312
+ state: unknown;
313
+ } | null>;
314
+ loadLatestBefore(executionId: string, sequence: number): Promise<{
315
+ id?: string;
316
+ sequence: number;
317
+ state: unknown;
318
+ } | null>;
319
+ }
320
+ /** Test-only: re-arm the once-per-process warning below. Not exported from the package index. */
321
+ export declare function resetSnapshotUnwrappedWarning(): void;
322
+ export interface ContentSweepReport {
323
+ executionsScanned: number;
324
+ refsSeen: number;
325
+ refsPinned: number;
326
+ /**
327
+ * ADR 0055 D4 - refs cited at least once anywhere but as a RECLAIMABLE claim-check envelope's
328
+ * `ref` (a digest counts as anywhere). Since ADR 0140 that includes a `retain: true` envelope
329
+ * whose ref sits at a trusted root (the top-level `output` of a STEP_COMPLETED, through a taint
330
+ * wrapper), and a R5 media source `{ kind: "ref", ref: { address } }`. Every address cited by an
331
+ * `archiveStores` segment is counted unconditionally. These are never reclaimed while any scanned
332
+ * event cites them. The set OVERLAPS `refsPinned` (a ref can be in both): the two are not disjoint
333
+ * partitions of `refsSeen`.
334
+ */
335
+ refsLoadBearing: number;
336
+ /**
337
+ * ADR 0146 - unpinned refs whose age clock came from a citing execution's END rather than from the
338
+ * citation: a ref cited by an execution that ended after the citation ages from the end. Counted
339
+ * on every sweep.
340
+ */
341
+ refsAgedByEnd: number;
342
+ /**
343
+ * ADR 0146 - DRY RUN ONLY (absent on an erasing sweep, which keeps no second map): refs the old
344
+ * citation clock would have reclaimed that the end clock keeps. Never reclaimed by the end clock
345
+ * that would not also have been by the old one, so this is the whole delta.
346
+ */
347
+ heldByEndClock?: string[];
348
+ /**
349
+ * Unpinned refs whose age clock stayed on the CITATION because the newest citing execution was
350
+ * RE-OPENED after a terminal event (`resumeAs` onto an ended run, ADR 0104 D1; ADR 0146's exception):
351
+ * they can be reclaimed while that execution's log is still live. Counted on every sweep. A plain
352
+ * running or parked execution that never ended is NOT counted. Decisions are unchanged: visibility only.
353
+ */
354
+ refsOfReopened: number;
355
+ /**
356
+ * DRY RUN ONLY (absent on an erasing sweep, which keeps no second list): of `reclaimed`, the addresses
357
+ * counted in `refsOfReopened`, so an operator sees them before `--apply`.
358
+ */
359
+ reclaimedWhileReopened?: string[];
360
+ /** Content addresses erased this sweep — or, under `dryRun`, that WOULD be. */
361
+ reclaimed: string[];
362
+ /**
363
+ * Eligible addresses (refs and orphans) whose conditional erase was REFUSED by the touch/race guard:
364
+ * `deleteIfUntouchedSince` returned false because the blob was touched after `touchCutoff` (a
365
+ * concurrent re-cite, or a content-store clock that is not the one the log's `now` runs on).
366
+ * ALWAYS 0 on a dry run, which never calls the store: a dry run can list N in `reclaimed` while the
367
+ * apply reports `reclaimed: []` and `keptByTouch: N`; when it does, compare `touchCutoff` with the
368
+ * store's touch stamps (see `touchedBefore`). Also 0 when the store is not `raceSafe` (plain delete).
369
+ */
370
+ keptByTouch: number;
371
+ /**
372
+ * The cutoff the guard compared touch stamps with, on the CONTENT STORE's clock (resolved from
373
+ * `touchedBefore`, else `touchClock() - olderThanMs`). Present on a dry run too (where it was not
374
+ * enforced); `undefined` when the store is not `raceSafe`.
375
+ */
376
+ touchCutoff?: number;
377
+ /**
378
+ * ADR 0055 D5 — every erase was conditional on the blob's touch time
379
+ * (`deleteIfUntouchedSince`), so a `put` that re-cited a blob during the sweep kept it.
380
+ * `false` = the store lacks it and the sweep used plain `delete`: such a re-cite is lost.
381
+ */
382
+ raceSafe: boolean;
383
+ /** ADR 0055 D6 — orphans erased (or, under `dryRun`, that WOULD be); `[]` unless `reclaimOrphans`. */
384
+ orphansReclaimed: string[];
385
+ /**
386
+ * Of `snapshotStores`, how many are a `ContentTouchingSnapshotStore` (branded). Informational:
387
+ * an unbranded store may be a raw reader over tables a wrapped writer filled.
388
+ */
389
+ snapshotStoresTouching: number;
390
+ /** Of `snapshotStores`, how many are NOT branded. See `snapshotStoresTouching`. */
391
+ snapshotStoresUnbranded: number;
392
+ /**
393
+ * Executions whose scanned snapshot `state` is a top-level `__loomEnc` envelope (an
394
+ * `EncryptingSnapshotStore({ sealState })` read RAW): the refs it cites are invisible to this
395
+ * scan. An erasing pass throws {@link SealedSnapshotScanError} instead; a `dryRun` lists them.
396
+ * The check looks at the TOP-LEVEL `__loomEnc` key of `state` only, not a nested envelope.
397
+ */
398
+ sealedSnapshots: string[];
399
+ /**
400
+ * Executions whose snapshot load threw `PayloadShreddedError` (their subject was crypto-shredded
401
+ * under a `sealState` store): the snapshot is erased, so it cites no readable refs and is skipped.
402
+ * Blobs another live owner cites stay pinned by it. Any OTHER load error still aborts the pass.
403
+ */
404
+ shreddedSnapshots: string[];
405
+ /**
406
+ * Executions whose compacted history is NOT (fully) in the given archives and whose orphan blobs
407
+ * this erasing pass erased anyway because `acknowledgeUnarchivedLogs: true` accepted it. Empty
408
+ * otherwise (and on a dry run).
409
+ */
410
+ unarchivedAcknowledged: string[];
411
+ }
412
+ /**
413
+ * An ERASING `reclaimOrphans` pass found compacted logs but was given no `archiveStores`: the events
414
+ * a compaction moved are cited only by the archive, so their blobs would look like orphans and be
415
+ * erased (ADR 0138). Nothing was erased.
416
+ */
417
+ export declare class ArchiveStoresRequiredError extends TypeError {
418
+ readonly executionIds: readonly string[];
419
+ /** `"none"`: no archiveStores given. `"uncovered"`: given, but they do not hold what was archived. */
420
+ readonly reason: "none" | "uncovered";
421
+ constructor(executionIds: readonly string[],
422
+ /** `"none"`: no archiveStores given. `"uncovered"`: given, but they do not hold what was archived. */
423
+ reason?: "none" | "uncovered");
424
+ }
425
+ /**
426
+ * An erasing sweep read an archive segment that is still sealed (count > 0, no envelopes): the RAW
427
+ * inner store of an `EncryptingArchiveStore`. The refs inside are invisible, so live blobs would
428
+ * look unreferenced. Pass the sealing store itself. Nothing was erased.
429
+ */
430
+ export declare class SealedArchiveScanError extends TypeError {
431
+ readonly executionIds: readonly string[];
432
+ constructor(executionIds: readonly string[]);
433
+ }
434
+ /**
435
+ * An erasing sweep read a snapshot whose `state` is one sealed envelope (a `sealState`
436
+ * `EncryptingSnapshotStore` given as its RAW inner store). The scan cannot see the refs inside, so
437
+ * it would treat live blobs as unreferenced and erase them. Nothing was erased.
438
+ */
439
+ export declare class SealedSnapshotScanError extends TypeError {
440
+ readonly executionIds: readonly string[];
441
+ constructor(executionIds: readonly string[]);
442
+ }
443
+ /**
444
+ * Mark: walk every event of every execution across the given stores, collecting
445
+ * each ref's latest citation timestamp, pinning refs cited from events of the
446
+ * designated kinds, and marking refs cited anywhere other than as a
447
+ * claim-check envelope's `ref` (a `digest` included) as load-bearing. Sweep:
448
+ * physically erase refs that are neither pinned nor load-bearing and whose
449
+ * effective citation time is older than the TTL — i.e. only refs cited solely as an
450
+ * envelope's `ref`. ADR 0146: a citing execution that has ENDED (newest kernel terminal event, not
451
+ * re-opened) ages its citations from `max(citation, end)`, so with a content TTL at least the log TTL a blob is
452
+ * never reclaimable before retention could delete the log citing it (exceptions: ADR 0146); a
453
+ * running execution (or a reserved stream) keeps the citation clock. Never earlier than the citation. Their erasure is invisible to replay by the claim-check
454
+ * contract (ADR 0054 — the fold never resolves); `resolveClaimCheck` readers get
455
+ * `null`. A bare ref is never reclaimed while cited (ADR 0055 D4): the resolving
456
+ * store would throw `ContentMissingError` on the next read. Idempotent:
457
+ * `ContentStore.delete` is idempotent and a reclaimed ref simply no longer
458
+ * resolves on the next sweep.
459
+ *
460
+ * Race-safe (ADR 0055 D5) when the store has `deleteIfUntouchedSince`: every erase is
461
+ * conditional on the blob being untouched since `now − olderThanMs`, so a `put` that re-cites
462
+ * old bytes between the scan and its own append keeps the blob. That holds while a flow's
463
+ * put→append window is shorter than the TTL. A citation copied without a `put` (a fork's
464
+ * `copyPrefix`, a ref kept inside an `append` payload) is covered by SPI 1.4.0 `touch`, which
465
+ * `reclaimOrphans` requires; see `reclaimOrphans`. `roots` (D7) pins addresses held outside the
466
+ * log; `reclaimOrphans` (D6) also erases stored blobs nothing scanned cites. Cost:
467
+ * O(events + blobs), all membership tests are Set/Map lookups.
468
+ */
469
+ export declare function sweepUnpinnedContent(given: ContentSweepOptions): Promise<ContentSweepReport>;
470
+ //# sourceMappingURL=gc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gc.d.ts","sourceRoot":"","sources":["../src/gc.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAQ7F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAqCnD;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,EACpC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,GACtB,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAI3C;AAED,4GAA4G;AAC5G,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;;;;;OAWG;IACH,WAAW,EAAE,SAAS,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,GAAG,mBAAmB,CAAC,EAAE,CAAC;IACzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,cAAc,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IAC3C;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,aAAa,CAAC,EAAE,SAAS,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAAC,EAAE,CAAC;IACrF;;;;;;OAMG;IACH,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC;;;;;;OAMG;IACH,YAAY,EAAE,IAAI,CAChB,YAAY,EACZ,QAAQ,GAAG,wBAAwB,GAAG,UAAU,GAAG,YAAY,GAAG,OAAO,CAC1E,CAAC;IACF,4FAA4F;IAC5F,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACnC;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE;QACP,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,SAAS,CAAC,EAAE,OAAO,CAAC;QACpB,OAAO,CAAC,EAAE;YAAE,eAAe,EAAE,MAAM,CAAA;SAAE,CAAC;KACvC,CAAC;IACF;;;;;OAKG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,aAAa,CAAC,MAAM,CAAC,CAAC;IACpC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgEG;IACH,cAAc,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC;CAC9C;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,gBAAgB,EAAE,QAAQ,GAAG,gBAAgB,CAAC,GACnF,CACI;IAAE,MAAM,CAAC,EAAE,KAAK,CAAC;IAAC,cAAc,CAAC,EAAE,iBAAiB,GAAG,KAAK,CAAA;CAAE,GAC9D;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,cAAc,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAA;CAAE,CACjE,CAAC;AAEJ;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,IAAI,CAAC;IACnB;;;;;;OAMG;IACH,qBAAqB,CAAC,EAAE,IAAI,CAAC;CAC9B;AAeD,6FAA6F;AAC7F,qBAAa,sBAAuB,SAAQ,SAAS;gBACvC,QAAQ,EAAE,OAAO;CAM9B;AAED;;;;;;;;;;GAUG;AACH,qBAAa,8BAA+B,SAAQ,SAAS;;CAO5D;AAED;;;;;;;GAOG;AACH,qBAAa,wBAAyB,SAAQ,SAAS;;CAOtD;AAED;;;;;;GAMG;AACH,eAAO,MAAM,0BAA0B,QAAS,CAAC;AAEjD;;;;;;GAMG;AACH,qBAAa,yBAA0B,SAAQ,SAAS;IAC1C,QAAQ,CAAC,QAAQ,EAAE,MAAM;gBAAhB,QAAQ,EAAE,MAAM;CAMtC;AAED;;;;;;GAMG;AACH,qBAAa,wBAAyB,SAAQ,SAAS;;CAOtD;AAED,+FAA+F;AAC/F,MAAM,WAAW,cAAc;IAC7B,UAAU,CACR,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IACrE,6FAA6F;IAC7F,MAAM,CAAC,CACL,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IACrE,gBAAgB,CACd,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;CACtE;AAGD,iGAAiG;AACjG,wBAAgB,6BAA6B,IAAI,IAAI,CAEpD;AAwBD,MAAM,WAAW,kBAAkB;IACjC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;;OAQG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;;OAKG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAC;IAClC,+EAA+E;IAC/E,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB;;;;;;;OAOG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,QAAQ,EAAE,OAAO,CAAC;IAClB,sGAAsG;IACtG,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B;;;OAGG;IACH,sBAAsB,EAAE,MAAM,CAAC;IAC/B,mFAAmF;IACnF,uBAAuB,EAAE,MAAM,CAAC;IAChC;;;;;OAKG;IACH,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;OAIG;IACH,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B;;;;OAIG;IACH,sBAAsB,EAAE,MAAM,EAAE,CAAC;CAClC;AAED;;;;GAIG;AACH,qBAAa,0BAA2B,SAAQ,SAAS;IAErD,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE;IACxC,sGAAsG;IACtG,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW;gBAF5B,YAAY,EAAE,SAAS,MAAM,EAAE;IACxC,sGAAsG;IAC7F,MAAM,GAAE,MAAM,GAAG,WAAoB;CAYjD;AAED;;;;GAIG;AACH,qBAAa,sBAAuB,SAAQ,SAAS;IACvC,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE;gBAA/B,YAAY,EAAE,SAAS,MAAM,EAAE;CASrD;AAED;;;;GAIG;AACH,qBAAa,uBAAwB,SAAQ,SAAS;IACxC,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE;gBAA/B,YAAY,EAAE,SAAS,MAAM,EAAE;CASrD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,kBAAkB,CAAC,CAc7B"}