@loomwork/ui-lineage 0.0.0-stage → 0.1.1

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 (88) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/README.md +471 -2
  4. package/dist/conformance.d.ts +4 -0
  5. package/dist/conformance.d.ts.map +1 -0
  6. package/dist/conformance.js +143 -0
  7. package/dist/conformance.js.map +1 -0
  8. package/dist/derive.d.ts +15 -0
  9. package/dist/derive.d.ts.map +1 -0
  10. package/dist/derive.js +35 -0
  11. package/dist/derive.js.map +1 -0
  12. package/dist/esm/conformance.js +140 -0
  13. package/dist/esm/conformance.js.map +1 -0
  14. package/dist/esm/derive.js +29 -0
  15. package/dist/esm/derive.js.map +1 -0
  16. package/dist/esm/events.js +101 -0
  17. package/dist/esm/events.js.map +1 -0
  18. package/dist/esm/index.js +12 -0
  19. package/dist/esm/index.js.map +1 -0
  20. package/dist/esm/lineage.js +1376 -0
  21. package/dist/esm/lineage.js.map +1 -0
  22. package/dist/esm/retention.js +73 -0
  23. package/dist/esm/retention.js.map +1 -0
  24. package/dist/esm/rootsFor.js +49 -0
  25. package/dist/esm/rootsFor.js.map +1 -0
  26. package/dist/esm/seen.js +733 -0
  27. package/dist/esm/seen.js.map +1 -0
  28. package/dist/esm/seenCrypto.js +197 -0
  29. package/dist/esm/seenCrypto.js.map +1 -0
  30. package/dist/esm/seenGc.js +122 -0
  31. package/dist/esm/seenGc.js.map +1 -0
  32. package/dist/esm/state.js +185 -0
  33. package/dist/esm/state.js.map +1 -0
  34. package/dist/esm/store.js +172 -0
  35. package/dist/esm/store.js.map +1 -0
  36. package/dist/events.d.ts +781 -0
  37. package/dist/events.d.ts.map +1 -0
  38. package/dist/events.js +106 -0
  39. package/dist/events.js.map +1 -0
  40. package/dist/index.d.ts +23 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +60 -0
  43. package/dist/index.js.map +1 -0
  44. package/dist/lineage.d.ts +394 -0
  45. package/dist/lineage.d.ts.map +1 -0
  46. package/dist/lineage.js +1383 -0
  47. package/dist/lineage.js.map +1 -0
  48. package/dist/retention.d.ts +45 -0
  49. package/dist/retention.d.ts.map +1 -0
  50. package/dist/retention.js +78 -0
  51. package/dist/retention.js.map +1 -0
  52. package/dist/rootsFor.d.ts +33 -0
  53. package/dist/rootsFor.d.ts.map +1 -0
  54. package/dist/rootsFor.js +52 -0
  55. package/dist/rootsFor.js.map +1 -0
  56. package/dist/seen.d.ts +375 -0
  57. package/dist/seen.d.ts.map +1 -0
  58. package/dist/seen.js +739 -0
  59. package/dist/seen.js.map +1 -0
  60. package/dist/seenCrypto.d.ts +72 -0
  61. package/dist/seenCrypto.d.ts.map +1 -0
  62. package/dist/seenCrypto.js +210 -0
  63. package/dist/seenCrypto.js.map +1 -0
  64. package/dist/seenGc.d.ts +73 -0
  65. package/dist/seenGc.d.ts.map +1 -0
  66. package/dist/seenGc.js +126 -0
  67. package/dist/seenGc.js.map +1 -0
  68. package/dist/state.d.ts +66 -0
  69. package/dist/state.d.ts.map +1 -0
  70. package/dist/state.js +190 -0
  71. package/dist/state.js.map +1 -0
  72. package/dist/store.d.ts +93 -0
  73. package/dist/store.d.ts.map +1 -0
  74. package/dist/store.js +177 -0
  75. package/dist/store.js.map +1 -0
  76. package/package.json +60 -3
  77. package/src/conformance.ts +148 -0
  78. package/src/derive.ts +57 -0
  79. package/src/events.ts +125 -0
  80. package/src/index.ts +79 -0
  81. package/src/lineage.ts +1655 -0
  82. package/src/retention.ts +118 -0
  83. package/src/rootsFor.ts +75 -0
  84. package/src/seen.ts +965 -0
  85. package/src/seenCrypto.ts +271 -0
  86. package/src/seenGc.ts +233 -0
  87. package/src/state.ts +268 -0
  88. package/src/store.ts +274 -0
@@ -0,0 +1,148 @@
1
+ import type { Finding } from "@loomwork/ui-core";
2
+ import type { LineageStore } from "./store";
3
+
4
+ // ── The LineageStore conformance kit ──────────────────────────────────────────
5
+ // Returns findings (a test asserts the list is empty), never throws: a store that throws on a case
6
+ // is reported as a finding, like `checkPlannerConformance`. Each case uses a fresh line id, so one
7
+ // `make()` store serves them all — but a backend that cannot isolate lines is exactly what the
8
+ // "lines are independent" case catches.
9
+
10
+ const rec = (n: number) => ({
11
+ type: "EXPERIENCE_COMPOSED",
12
+ payload: { n },
13
+ at: "2026-10-06T00:00:00.000Z",
14
+ });
15
+
16
+ export async function checkLineageStoreConformance(
17
+ make: () => LineageStore | Promise<LineageStore>
18
+ ): Promise<Finding[]> {
19
+ const findings: Finding[] = [];
20
+ const fail = (code: string, message: string) =>
21
+ findings.push({ code, severity: "error", message });
22
+ const store = await make();
23
+ const cases: Array<[string, () => Promise<void>]> = [
24
+ [
25
+ "an unknown line reads empty",
26
+ async () => {
27
+ const r = await store.read("c-empty");
28
+ if (r.length !== 0) fail("read-unknown", `an unknown line read ${r.length} records`);
29
+ },
30
+ ],
31
+ [
32
+ "append at tip 0 wins sequence 1; the sequence is dense",
33
+ async () => {
34
+ const a = await store.append("c-seq", 0, rec(1));
35
+ const b = await store.append("c-seq", 1, rec(2));
36
+ if (!a.ok || a.sequence !== 1 || !b.ok || b.sequence !== 2)
37
+ fail("append-sequence", `sequential appends returned ${JSON.stringify([a, b])}`);
38
+ const r = await store.read("c-seq");
39
+ if (r.map((x) => x.sequence).join() !== "1,2")
40
+ fail("read-order", `read returned sequences ${r.map((x) => x.sequence).join()}`);
41
+ },
42
+ ],
43
+ [
44
+ "a stale expectedSequence is refused with the real tip and writes nothing",
45
+ async () => {
46
+ await store.append("c-stale", 0, rec(1));
47
+ const s = await store.append("c-stale", 0, rec(2));
48
+ if (s.ok || s.tip !== 1)
49
+ fail("stale-append", `a stale append returned ${JSON.stringify(s)}`);
50
+ const ahead = await store.append("c-stale", 5, rec(3));
51
+ if (ahead.ok || ahead.tip !== 1)
52
+ fail("gap-append", `an append past the tip (a gap) returned ${JSON.stringify(ahead)}`);
53
+ const r = await store.read("c-stale");
54
+ if (r.length !== 1) fail("refused-wrote", `a refused append left ${r.length} records`);
55
+ },
56
+ ],
57
+ [
58
+ "of N concurrent appends at one tip exactly one wins",
59
+ async () => {
60
+ const outs = await Promise.all(
61
+ Array.from({ length: 8 }, (_, i) => store.append("c-race", 0, rec(i)))
62
+ );
63
+ const wins = outs.filter((o) => o.ok).length;
64
+ if (wins !== 1) fail("race", `${wins} of 8 concurrent appends at the same tip won`);
65
+ const r = await store.read("c-race");
66
+ if (r.length !== 1) fail("race-log", `the raced line holds ${r.length} records`);
67
+ },
68
+ ],
69
+ [
70
+ "lines are independent",
71
+ async () => {
72
+ await store.append("c-iso-a", 0, rec(1));
73
+ const b = await store.append("c-iso-b", 0, rec(2));
74
+ if (!b.ok)
75
+ fail("isolation", "an append to a second line was refused by the first line's tip");
76
+ if ((await store.read("c-iso-a")).length !== 1)
77
+ fail("isolation", "a line holds another's record");
78
+ },
79
+ ],
80
+ [
81
+ "payloads round-trip and neither side aliases the log",
82
+ async () => {
83
+ const payload = { nested: { list: [1, 2, { k: "v" }] } };
84
+ await store.append("c-copy", 0, {
85
+ type: "EXPERIENCE_COMPOSED",
86
+ payload,
87
+ at: "2026-10-06T00:00:00.000Z",
88
+ });
89
+ payload.nested.list.push(3);
90
+ const first = await store.read("c-copy");
91
+ if (
92
+ JSON.stringify(first[0]?.payload) !==
93
+ JSON.stringify({ nested: { list: [1, 2, { k: "v" }] } })
94
+ )
95
+ fail("copy-in", "mutating a payload after append changed the stored record");
96
+ (
97
+ first as unknown as Array<{ payload: { nested: { list: unknown[] } } }>
98
+ )[0]?.payload.nested.list.push(9);
99
+ const second = await store.read("c-copy");
100
+ if (JSON.stringify(second[0]?.payload).includes("9"))
101
+ fail("copy-out", "mutating a read result changed the stored record");
102
+ if (
103
+ second[0]?.type !== "EXPERIENCE_COMPOSED" ||
104
+ second[0]?.at !== "2026-10-06T00:00:00.000Z"
105
+ )
106
+ fail("fields", "type or at did not round-trip");
107
+ },
108
+ ],
109
+ [
110
+ "optional readTail / readAfter / listStreams agree with read (when implemented)",
111
+ async () => {
112
+ for (let i = 0; i < 5; i++) await store.append("c-opt", i, rec(i));
113
+ const all = await store.read("c-opt");
114
+ const seqs = (r: readonly { sequence: number }[]) => r.map((x) => x.sequence).join();
115
+ if (store.readTail !== undefined) {
116
+ if (seqs(await store.readTail("c-opt", 2)) !== "4,5")
117
+ fail("read-tail", "readTail(2) did not return the newest two records in order");
118
+ if (seqs(await store.readTail("c-opt", 99)) !== seqs(all))
119
+ fail("read-tail", "readTail past the length did not return the whole line");
120
+ if ((await store.readTail("c-opt-none", 3)).length !== 0)
121
+ fail("read-tail", "readTail of an unknown line was not empty");
122
+ }
123
+ if (store.readAfter !== undefined) {
124
+ if (seqs(await store.readAfter("c-opt", 3)) !== "4,5")
125
+ fail("read-after", "readAfter(3) did not return the records past sequence 3");
126
+ if (seqs(await store.readAfter("c-opt", 0, 2)) !== "1,2")
127
+ fail("read-after", "readAfter(0, 2) did not honour the limit");
128
+ if ((await store.readAfter("c-opt", 5)).length !== 0)
129
+ fail("read-after", "readAfter at the tip was not empty");
130
+ }
131
+ if (store.listStreams !== undefined) {
132
+ const ids: string[] = [];
133
+ for await (const id of store.listStreams()) ids.push(id);
134
+ if (!ids.includes("c-opt") || new Set(ids).size !== ids.length)
135
+ fail("list-streams", "listStreams missed a line or listed one twice");
136
+ }
137
+ },
138
+ ],
139
+ ];
140
+ for (const [name, run] of cases) {
141
+ try {
142
+ await run();
143
+ } catch (err) {
144
+ fail("store-threw", `${name}: ${err instanceof Error ? err.message : String(err)}`);
145
+ }
146
+ }
147
+ return findings;
148
+ }
package/src/derive.ts ADDED
@@ -0,0 +1,57 @@
1
+ import { type Experience, type Provenance, carryClasses } from "@loomwork/ui-core";
2
+
3
+ // ── Versions that are not "ops on a parent" ───────────────────────────────────
4
+ // A revert and a branch adopt an OLDER artifact's content, so they are not expressible as ops
5
+ // from the head. They are deterministic functions of two artifacts instead, which is what makes
6
+ // them verifiable: `Lineage.verify` recomputes them from the log and compares the content address.
7
+ //
8
+ // THE CLASS RULE (fail-closed): the artifact adopted is older than the line. Everything the line
9
+ // ever learned about how confidential a handle, tool or projection is lives in the sticky class
10
+ // maps of its LATEST version, and the adopted artifact predates some of it. Both functions
11
+ // therefore fold the classes of the artifact the line had reached (`head` for a revert, the source
12
+ // head at the fork for a branch) into the adopted one with `carryClasses` — `strictest`, never
13
+ // lower — INCLUDING classes only that artifact's live nodes carried (they would vanish with the
14
+ // nodes). A restored version can be stricter than it was when first written; it can never be laxer
15
+ // than what the line knew. The one op that lowers a class, a trusted `declassify*`, is a normal
16
+ // composed version: reverting past it re-raises the class (the old artifact still holds it), which
17
+ // is the safe direction; declassifying again is how an operator lowers it for good.
18
+
19
+ export interface Authored {
20
+ readonly by: Provenance;
21
+ readonly at: string;
22
+ }
23
+
24
+ /** The unsealed artifact a revert to `target` produces on top of `head`. Same line, same id. */
25
+ export function revertArtifact(head: Experience, target: Experience, a: Authored): Experience {
26
+ const adopted: Experience = {
27
+ ...target,
28
+ experienceId: head.experienceId,
29
+ versionHash: "",
30
+ parents: [head.versionHash],
31
+ meta: { author: authorOf(a.by), createdAt: a.at, sourceKind: "revert" },
32
+ };
33
+ return carryClasses(head, adopted);
34
+ }
35
+
36
+ /**
37
+ * The unsealed first artifact of the line `experienceId`, forked from `source` with the classes of
38
+ * `carriedFrom` (the source line's head at the fork) carried in. Its parent is `source` — a version
39
+ * of ANOTHER line, which is how a branch is told from a genesis.
40
+ */
41
+ export function branchArtifact(
42
+ source: Experience,
43
+ carriedFrom: Experience,
44
+ experienceId: string,
45
+ a: Authored
46
+ ): Experience {
47
+ const adopted: Experience = {
48
+ ...source,
49
+ experienceId,
50
+ versionHash: "",
51
+ parents: [source.versionHash],
52
+ meta: { author: authorOf(a.by), createdAt: a.at, sourceKind: "branch" },
53
+ };
54
+ return carryClasses(carriedFrom, adopted);
55
+ }
56
+
57
+ export const authorOf = (by: Provenance): string => by.agentId ?? by.ref ?? by.source;
package/src/events.ts ADDED
@@ -0,0 +1,125 @@
1
+ import { Operation, Provenance, Sensitivity } from "@loomwork/ui-core";
2
+ import { z } from "zod";
3
+
4
+ // ── The lineage event protocol ────────────────────────────────────────────────
5
+ // A lineage is the append-only log of ONE experience's versions. Every entry is an event whose
6
+ // type names what happened and whose payload carries the content address (`ref`) of the stored
7
+ // artifact plus enough to VERIFY it without trusting the blob:
8
+ //
9
+ // EXPERIENCE_COMPOSED a version produced by ops folded onto its parent (or the genesis, ops [])
10
+ // EXPERIENCE_REVERTED a version that restores an older artifact's content as a NEW version
11
+ // EXPERIENCE_BRANCHED the first version of a NEW lineage forked from a version of another
12
+ // EXPERIENCE_PROMOTED the app chose to SHOW a version (head is "latest", promoted is "shown")
13
+ //
14
+ // `EXPERIENCE_COMPOSED` is the same name and `{ ref, parents, ops }` core's `log.ts` declared as a
15
+ // userspace convention; here it gains a producer, a consumer and a validator. The extra fields
16
+ // (`experienceId`, `versionHash`, `by`) are additive. `EXPERIENCE_PROMOTED` is redeclared (not
17
+ // imported) because importing it from `@loomwork/ui-planner` would put the planner's whole graph —
18
+ // hydration, the sandbox — into a server-side store package; `lineage.test.ts` asserts the two
19
+ // spellings never drift.
20
+
21
+ export const EXPERIENCE_COMPOSED = "EXPERIENCE_COMPOSED";
22
+ export const EXPERIENCE_REVERTED = "EXPERIENCE_REVERTED";
23
+ export const EXPERIENCE_BRANCHED = "EXPERIENCE_BRANCHED";
24
+ export const EXPERIENCE_PROMOTED = "EXPERIENCE_PROMOTED";
25
+
26
+ export type LineageEventType =
27
+ | typeof EXPERIENCE_COMPOSED
28
+ | typeof EXPERIENCE_REVERTED
29
+ | typeof EXPERIENCE_BRANCHED
30
+ | typeof EXPERIENCE_PROMOTED;
31
+
32
+ const ContentRefSchema = z.object({ __loomContent: z.string().min(1) });
33
+ const Hash = z.string().min(1);
34
+
35
+ const RecordedOperationSchema = z.object({
36
+ op: Operation,
37
+ baseVersion: z.string().optional(),
38
+ by: Provenance,
39
+ at: z.string(),
40
+ });
41
+
42
+ const Common = {
43
+ experienceId: z.string().min(1),
44
+ versionHash: Hash,
45
+ ref: ContentRefSchema,
46
+ parents: z.array(Hash),
47
+ by: Provenance,
48
+ };
49
+
50
+ export const ComposedPayload = z.object({
51
+ ...Common,
52
+ ops: z.array(RecordedOperationSchema),
53
+ /**
54
+ * What the author said about the change (U7): UNTRUSTED text (a planner's model text), bounded
55
+ * and stripped by `boundSummary` on the way in AND on the way out of the fold, since the log is
56
+ * untrusted input. Optional, so every version written before it replays unchanged; it is not in
57
+ * the content address (nothing here is).
58
+ */
59
+ summary: z.string().optional(),
60
+ /**
61
+ * The strictest class of everything the author READ to write `summary` (the experience, tool
62
+ * samples, caller context), stamped by the host at compose. A summary is judged against it (and
63
+ * against the version's own class floor) before a viewer is shown it. Recorded on the event, not
64
+ * in the content address. Absent = unknown, and the floor alone gates the summary.
65
+ */
66
+ summaryClass: Sensitivity.optional(),
67
+ });
68
+ export const RevertedPayload = z.object({ ...Common, revertedTo: Hash });
69
+ export const BranchedPayload = z.object({
70
+ ...Common,
71
+ from: z.object({ experienceId: z.string().min(1), versionHash: Hash }),
72
+ /** The version of the SOURCE line whose sticky classes were carried in (its head at the fork). */
73
+ carriedFrom: Hash,
74
+ });
75
+ export const PromotedPayload = z.object({
76
+ ref: ContentRefSchema,
77
+ experienceId: z.string().min(1),
78
+ versionHash: Hash,
79
+ by: Provenance,
80
+ });
81
+
82
+ export type ComposedPayload = z.infer<typeof ComposedPayload>;
83
+ export type SummaryClass = NonNullable<ComposedPayload["summaryClass"]>;
84
+ export type RevertedPayload = z.infer<typeof RevertedPayload>;
85
+ export type BranchedPayload = z.infer<typeof BranchedPayload>;
86
+ export type PromotedPayload = z.infer<typeof PromotedPayload>;
87
+
88
+ export const PAYLOAD_SCHEMAS = {
89
+ [EXPERIENCE_COMPOSED]: ComposedPayload,
90
+ [EXPERIENCE_REVERTED]: RevertedPayload,
91
+ [EXPERIENCE_BRANCHED]: BranchedPayload,
92
+ [EXPERIENCE_PROMOTED]: PromotedPayload,
93
+ } as const;
94
+
95
+ export const isLineageEventType = (t: string): t is LineageEventType =>
96
+ Object.hasOwn(PAYLOAD_SCHEMAS, t);
97
+
98
+ /**
99
+ * The payload paths that carry host or model TEXT, per event type: the op props and labels, the
100
+ * author's `summary` and the `by` provenance (a user's own words and ids). These are what a host
101
+ * seals under its owner's key so erasing the owner makes the line unreadable
102
+ * (`registerLineageSensitivePaths`). Everything else on an event is an identifier (`experienceId`,
103
+ * `versionHash`, `ref`, `parents`, `from`, `carriedFrom`, `revertedTo`) or a class
104
+ * (`summaryClass`), and stays plain so the log's structure survives the key.
105
+ *
106
+ * `ops` is one path: it covers every nested op's `by`, props, labels and prompt text.
107
+ */
108
+ export const LINEAGE_SENSITIVE_PATHS: Readonly<Record<LineageEventType, readonly string[]>> = {
109
+ [EXPERIENCE_COMPOSED]: ["ops", "summary", "by"],
110
+ [EXPERIENCE_REVERTED]: ["by"],
111
+ [EXPERIENCE_BRANCHED]: ["by"],
112
+ [EXPERIENCE_PROMOTED]: ["by"],
113
+ };
114
+
115
+ /**
116
+ * Register {@link LINEAGE_SENSITIVE_PATHS} on a crypto event store (`EncryptingEventStore`'s
117
+ * `registerSensitive`). Call it once at composition, before the first lineage write: what was
118
+ * written before stays plaintext.
119
+ */
120
+ export function registerLineageSensitivePaths(store: {
121
+ registerSensitive(eventType: string, paths: readonly string[]): void;
122
+ }): void {
123
+ for (const [type, paths] of Object.entries(LINEAGE_SENSITIVE_PATHS))
124
+ store.registerSensitive(type, paths);
125
+ }
package/src/index.ts ADDED
@@ -0,0 +1,79 @@
1
+ export { LineErasedError, LineUnreadableError, Lineage, classifyLineFailure } from "./lineage";
2
+ export type { LineBlockedError, PinsExtra } from "./lineage";
3
+ export type {
4
+ Committed,
5
+ HeadMoved,
6
+ LineageOptions,
7
+ LineageResult,
8
+ PromotedMoved,
9
+ Refusal,
10
+ Refused,
11
+ } from "./lineage";
12
+ export { branchArtifact, revertArtifact } from "./derive";
13
+ export type { Authored } from "./derive";
14
+ export {
15
+ BranchedPayload,
16
+ ComposedPayload,
17
+ EXPERIENCE_BRANCHED,
18
+ EXPERIENCE_COMPOSED,
19
+ EXPERIENCE_PROMOTED,
20
+ EXPERIENCE_REVERTED,
21
+ LINEAGE_SENSITIVE_PATHS,
22
+ PromotedPayload,
23
+ RevertedPayload,
24
+ registerLineageSensitivePaths,
25
+ } from "./events";
26
+ export type { LineageEventType, SummaryClass } from "./events";
27
+ export { foldLineage } from "./state";
28
+ export type { Fold, LineageState, VersionKind, VersionRecord } from "./state";
29
+ export {
30
+ InMemoryLineageStore,
31
+ LINEAGE_STREAM_PREFIX,
32
+ LINEAGE_WORKFLOW_ID,
33
+ LOOM_UI_STREAM_NAMESPACE,
34
+ eventStoreLineageStore,
35
+ } from "./store";
36
+ export type {
37
+ AppendOutcome,
38
+ LineageRecord,
39
+ LineageStore,
40
+ PrefixedLineageStore,
41
+ StoredLineageRecord,
42
+ } from "./store";
43
+ export { checkLineageStoreConformance } from "./conformance";
44
+ export {
45
+ DEFAULT_MAX_SEEN_VIEWS,
46
+ DEFAULT_MAX_SKEW_MS,
47
+ MAX_SEEN_READ,
48
+ SEEN_STREAM_PREFIX,
49
+ SEEN_WORKFLOW_ID,
50
+ SeenLog,
51
+ VIEWER_ERASED,
52
+ VIEWS_RETAINED,
53
+ VIEW_RECORDED,
54
+ ViewRecordPayload,
55
+ seenStoreOptions,
56
+ seenStreamKey,
57
+ } from "./seen";
58
+ export type {
59
+ SeenLogOptions,
60
+ SeenRefusal,
61
+ SeenResult,
62
+ SeenRetention,
63
+ SeenUnreadable,
64
+ SeenView,
65
+ } from "./seen";
66
+ export {
67
+ InMemorySeenKeyStore,
68
+ SEALED_REF_BLOCK,
69
+ checkSeenKeyStoreConformance,
70
+ seenKeySubject,
71
+ } from "./seenCrypto";
72
+ export type { SeenKeyStore } from "./seenCrypto";
73
+ export { collectSeenGarbage, collectSeenRoots } from "./seenGc";
74
+ export type { SeenGcOptions, SeenGcReport, SeenGcResult } from "./seenGc";
75
+
76
+ export { DEFAULT_KEEP_LAST, retainedVersions, validateRetentionPolicy } from "./retention";
77
+ export type { Retained, RetentionPolicy } from "./retention";
78
+ export { rootsFor } from "./rootsFor";
79
+ export type { RootsForOptions } from "./rootsFor";