@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,118 @@
1
+ import type { LineageState, VersionRecord } from "./state";
2
+
3
+ // ── Version retention: which versions a line must keep (U17) ──────────────────────────────────
4
+ // Every version stores its WHOLE artifact (`putExperience`), so a line grows O(versions x nodes) and,
5
+ // because each version's event cites its artifact by a bare content ref, loom's `sweepUnpinnedContent`
6
+ // pins every one of them for ever while the lineage's streams are scanned. Retention is the policy
7
+ // that says which versions are still wanted; `Lineage.pins` turns it into the root set a collector
8
+ // marks, and everything NOT in it stops being pinned by the lineage.
9
+ //
10
+ // This module is the pure half: a policy over one line's folded state. It never reads a blob.
11
+
12
+ export interface RetentionPolicy {
13
+ /**
14
+ * Keep the newest N versions of each line, counted in append order and including the head.
15
+ * Default {@link DEFAULT_KEEP_LAST}. The head is ALWAYS kept, whatever this says (`0` keeps only
16
+ * what the other rules keep). `Infinity` keeps every version. Anything else that is not a
17
+ * non-negative integer THROWS: this policy decides what gets deleted, so a typo is never read as
18
+ * "keep nothing".
19
+ */
20
+ readonly keepLast?: number;
21
+ /**
22
+ * Also keep every version authored at or after this instant: an ISO-8601 date or date-time string
23
+ * (`2026-10-01`, `2026-10-01T00:00:00Z`), a `Date`, or epoch milliseconds. Anything else THROWS
24
+ * (lenient parsing would read `"1"` as the year 2001).
25
+ */
26
+ readonly keepSince?: string | number | Date;
27
+ /**
28
+ * Keep the line's first version (default `true`): the genesis anchors `verify`'s replay and is the
29
+ * one version every other is derived from.
30
+ */
31
+ readonly keepGenesis?: boolean;
32
+ /** Keep versions the host names (its own audit pins, a customer's "approved" marks). Unknown hashes are ignored. */
33
+ readonly keep?: (version: VersionRecord) => boolean;
34
+ }
35
+
36
+ export const DEFAULT_KEEP_LAST = 20;
37
+
38
+ const ISO_8601 = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
39
+
40
+ /** `keepSince` as epoch milliseconds; NaN when it is not a strict ISO-8601 string, a valid Date or a finite number. */
41
+ function keepSinceMs(v: unknown): number {
42
+ if (typeof v === "string") return ISO_8601.test(v) ? Date.parse(v) : Number.NaN;
43
+ if (v instanceof Date) return v.getTime();
44
+ if (typeof v === "number") return Number.isFinite(v) ? v : Number.NaN;
45
+ return Number.NaN;
46
+ }
47
+
48
+ /**
49
+ * Refuse a policy that does not mean what its author meant. A deletion policy must not coerce: NaN,
50
+ * a negative or fractional `keepLast`, an unparseable `keepSince` or a non-boolean/-function rule
51
+ * all throw. `Infinity` is the one non-integer allowed (keep everything).
52
+ */
53
+ export function validateRetentionPolicy(policy: RetentionPolicy): void {
54
+ const bad = (m: string): never => {
55
+ throw new RangeError(`RetentionPolicy: ${m}`);
56
+ };
57
+ const { keepLast, keepSince, keepGenesis, keep } = policy;
58
+ if (keepLast !== undefined) {
59
+ if (typeof keepLast !== "number") bad(`keepLast must be a number, got ${typeof keepLast}`);
60
+ else if (
61
+ keepLast !== Number.POSITIVE_INFINITY &&
62
+ !(Number.isSafeInteger(keepLast) && keepLast >= 0)
63
+ )
64
+ bad(
65
+ `keepLast must be a non-negative integer or Infinity (keep all), got ${String(keepLast)}`
66
+ );
67
+ }
68
+ if (keepSince !== undefined && Number.isNaN(keepSinceMs(keepSince)))
69
+ bad(
70
+ `keepSince must be an ISO-8601 date/time string, a valid Date or epoch milliseconds, got ${String(keepSince)}`
71
+ );
72
+ if (keepGenesis !== undefined && typeof keepGenesis !== "boolean")
73
+ bad("keepGenesis must be a boolean");
74
+ if (keep !== undefined && typeof keep !== "function") bad("keep must be a function");
75
+ }
76
+
77
+ export interface Retained {
78
+ /** Version hashes the policy keeps. */
79
+ readonly keep: ReadonlySet<string>;
80
+ /** Every other version of the line, oldest first: nothing the policy wants still needs its artifact. */
81
+ readonly reclaimable: readonly string[];
82
+ }
83
+
84
+ /**
85
+ * What to keep of one line. Always kept, regardless of the policy: the head, the promoted version (it
86
+ * is what viewers are shown), and every version another line branched from (`branchSources`: a fork is
87
+ * only as sound as its source, and `verify` rebuilds the source up to the fork point). Total: an empty
88
+ * line keeps nothing. Throws on a malformed policy ({@link validateRetentionPolicy}).
89
+ */
90
+ export function retainedVersions(
91
+ state: LineageState,
92
+ policy: RetentionPolicy = {},
93
+ branchSources: ReadonlySet<string> = new Set()
94
+ ): Retained {
95
+ validateRetentionPolicy(policy);
96
+ const keep = new Set<string>();
97
+ const order = state.order;
98
+ if (state.head !== null) keep.add(state.head);
99
+ if (state.promoted !== null) keep.add(state.promoted);
100
+ for (const h of branchSources) if (state.versions.has(h)) keep.add(h);
101
+
102
+ const last = policy.keepLast ?? DEFAULT_KEEP_LAST;
103
+ const n = last === Number.POSITIVE_INFINITY ? order.length : last;
104
+ for (let i = Math.max(0, order.length - n); i < order.length; i++) keep.add(order[i] as string);
105
+
106
+ if ((policy.keepGenesis ?? true) && order.length > 0) keep.add(order[0] as string);
107
+
108
+ const since = policy.keepSince === undefined ? Number.NaN : keepSinceMs(policy.keepSince);
109
+ for (const h of order) {
110
+ const v = state.versions.get(h) as VersionRecord;
111
+ if (!Number.isNaN(since)) {
112
+ const at = Date.parse(v.at);
113
+ if (!Number.isNaN(at) && at >= since) keep.add(h);
114
+ }
115
+ if (policy.keep?.(v) === true) keep.add(h);
116
+ }
117
+ return { keep, reclaimable: order.filter((h) => !keep.has(h)) };
118
+ }
@@ -0,0 +1,75 @@
1
+ import type { Lineage, PinsExtra } from "./lineage";
2
+ import { type RetentionPolicy, validateRetentionPolicy } from "./retention";
3
+ import type { SeenLog } from "./seen";
4
+ import { collectSeenRoots } from "./seenGc";
5
+
6
+ // ── The complete root set for a shared ContentStore (U17) ─────────────────────────────────────
7
+ // Loom's `sweepUnpinnedContent({ roots, completeScan: true })` erases every blob no root names, so
8
+ // `roots` must come from EVERY user of the store. Passing ONE lineage's `pins()` silently loses every
9
+ // other lineage's artifacts; this is the one place that composes them.
10
+
11
+ export interface RootsForOptions {
12
+ /** EVERY lineage sharing the content store, each with the retention policy that decides what it keeps. */
13
+ readonly lineages: ReadonlyArray<{
14
+ readonly lineage: Lineage;
15
+ readonly policy?: RetentionPolicy;
16
+ }>;
17
+ /**
18
+ * EVERY seen log sharing the store. Each one's live sealed value blobs are rooted, and the versions
19
+ * its views still read are kept by the lineage it was built over (`log.lineageOf`), which MUST be
20
+ * one of `lineages` (THROWS otherwise: its versions would have no policy to be kept under).
21
+ */
22
+ readonly seenLogs?: readonly SeenLog[];
23
+ /** Passed to each `collectSeenRoots`: sweep what an unreadable live window cites instead of refusing. Default: refuse. */
24
+ readonly allowUnreadable?: boolean;
25
+ /** Other users of the store (app tables, anything loom's own scan does not see). */
26
+ readonly extra?: AsyncIterable<string> | Iterable<string>;
27
+ }
28
+
29
+ type VersionRef = { readonly experienceId: string; readonly versionHash: string };
30
+
31
+ /**
32
+ * One async iterable of every address to keep: each lineage's `pins(policy)` (with the versions the
33
+ * seen logs over THAT lineage still read), each seen log's sealed value blobs, and `extra`; deduplicated.
34
+ *
35
+ * FAIL CLOSED: policies and wiring are validated before anything is read, and the whole set is
36
+ * established BEFORE the first address is yielded, so any source throwing makes the iterator throw
37
+ * without yielding a partial set (loom consumes roots fully before it erases; this holds for any
38
+ * consumer). Memory is one string per distinct live address.
39
+ *
40
+ * roots: () => rootsFor({ lineages: [{ lineage: a, policy }, { lineage: b }], seenLogs: [log] })
41
+ */
42
+ export async function* rootsFor(opts: RootsForOptions): AsyncGenerator<string, void, undefined> {
43
+ const entries = [...opts.lineages];
44
+ const seen = new Set<Lineage>();
45
+ for (const e of entries) {
46
+ validateRetentionPolicy(e.policy ?? {});
47
+ if (seen.has(e.lineage))
48
+ throw new Error("rootsFor: the same Lineage is listed twice (give it one policy)");
49
+ seen.add(e.lineage);
50
+ }
51
+ for (const log of opts.seenLogs ?? [])
52
+ if (!seen.has(log.lineageOf))
53
+ throw new Error(
54
+ "rootsFor: a seen log is built over a Lineage that is not in `lineages`; list it, or its versions have no policy to be kept under"
55
+ );
56
+
57
+ const addresses = new Set<string>();
58
+ const versionsOf = new Map<Lineage, VersionRef[]>();
59
+ for (const log of opts.seenLogs ?? []) {
60
+ const r = await collectSeenRoots(log, {
61
+ ...(opts.allowUnreadable !== undefined && { allowUnreadable: opts.allowUnreadable }),
62
+ });
63
+ for await (const a of r.keepAddresses) addresses.add(a);
64
+ const list = versionsOf.get(log.lineageOf) ?? [];
65
+ list.push(...r.keepVersions);
66
+ versionsOf.set(log.lineageOf, list);
67
+ }
68
+ for (const { lineage, policy } of entries) {
69
+ const extra: PinsExtra = { keepVersions: versionsOf.get(lineage) ?? [] };
70
+ for await (const a of lineage.pins(policy ?? {}, extra)) addresses.add(a);
71
+ }
72
+ if (opts.extra !== undefined) for await (const a of opts.extra) addresses.add(a);
73
+
74
+ for (const a of addresses) yield a;
75
+ }