@volter/world-core 2.0.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 (180) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +29 -0
  3. package/app-route.cjs +154 -0
  4. package/app-route.d.cts +7 -0
  5. package/attach.cjs +80 -0
  6. package/dist/app-route.cjs +154 -0
  7. package/dist/app-route.d.cts +7 -0
  8. package/dist/attach.cjs +80 -0
  9. package/dist/generated/pack-facts.json +4306 -0
  10. package/dist/inject.cjs +1097 -0
  11. package/dist/network-policy.cjs +92 -0
  12. package/dist/network-policy.d.cts +10 -0
  13. package/dist/src/actions.d.ts +276 -0
  14. package/dist/src/actions.js +436 -0
  15. package/dist/src/ancestry.d.ts +22 -0
  16. package/dist/src/ancestry.js +238 -0
  17. package/dist/src/args.d.ts +3 -0
  18. package/dist/src/args.js +12 -0
  19. package/dist/src/blob-store.d.ts +55 -0
  20. package/dist/src/blob-store.js +186 -0
  21. package/dist/src/brand-tokens.d.ts +2 -0
  22. package/dist/src/brand-tokens.js +17 -0
  23. package/dist/src/changeset.d.ts +431 -0
  24. package/dist/src/changeset.js +0 -0
  25. package/dist/src/client-bundle.d.ts +1 -0
  26. package/dist/src/client-bundle.js +28 -0
  27. package/dist/src/credential.d.ts +38 -0
  28. package/dist/src/credential.js +114 -0
  29. package/dist/src/derived-core.d.ts +452 -0
  30. package/dist/src/derived-core.js +782 -0
  31. package/dist/src/derived.d.ts +84 -0
  32. package/dist/src/derived.js +122 -0
  33. package/dist/src/emit.d.ts +106 -0
  34. package/dist/src/emit.js +157 -0
  35. package/dist/src/executor.d.ts +120 -0
  36. package/dist/src/executor.js +387 -0
  37. package/dist/src/file-response.d.ts +3 -0
  38. package/dist/src/file-response.js +22 -0
  39. package/dist/src/fork.d.ts +26 -0
  40. package/dist/src/fork.js +68 -0
  41. package/dist/src/git/history.d.ts +36 -0
  42. package/dist/src/git/history.js +298 -0
  43. package/dist/src/git/index.d.ts +6 -0
  44. package/dist/src/git/index.js +6 -0
  45. package/dist/src/git/inflate.d.ts +11 -0
  46. package/dist/src/git/inflate.js +194 -0
  47. package/dist/src/git/objects.d.ts +64 -0
  48. package/dist/src/git/objects.js +161 -0
  49. package/dist/src/git/pack.d.ts +14 -0
  50. package/dist/src/git/pack.js +199 -0
  51. package/dist/src/git/refs.d.ts +19 -0
  52. package/dist/src/git/refs.js +35 -0
  53. package/dist/src/git/smart-http.d.ts +45 -0
  54. package/dist/src/git/smart-http.js +223 -0
  55. package/dist/src/hash.d.ts +38 -0
  56. package/dist/src/hash.js +48 -0
  57. package/dist/src/head.d.ts +140 -0
  58. package/dist/src/head.js +313 -0
  59. package/dist/src/history.d.ts +76 -0
  60. package/dist/src/history.js +322 -0
  61. package/dist/src/index.d.ts +73 -0
  62. package/dist/src/index.js +98 -0
  63. package/dist/src/lifecycle.d.ts +1 -0
  64. package/dist/src/lifecycle.js +8 -0
  65. package/dist/src/log.d.ts +254 -0
  66. package/dist/src/log.js +801 -0
  67. package/dist/src/mirror-shell.d.ts +2 -0
  68. package/dist/src/mirror-shell.js +13 -0
  69. package/dist/src/observe.d.ts +49 -0
  70. package/dist/src/observe.js +148 -0
  71. package/dist/src/pack-assets.d.ts +30 -0
  72. package/dist/src/pack-assets.js +88 -0
  73. package/dist/src/packRegistry.d.ts +374 -0
  74. package/dist/src/packRegistry.js +142 -0
  75. package/dist/src/placeholder-remote.d.ts +22 -0
  76. package/dist/src/placeholder-remote.js +86 -0
  77. package/dist/src/proxy.d.ts +25 -0
  78. package/dist/src/proxy.js +155 -0
  79. package/dist/src/rateBudget.d.ts +367 -0
  80. package/dist/src/rateBudget.js +925 -0
  81. package/dist/src/references.d.ts +18 -0
  82. package/dist/src/references.js +27 -0
  83. package/dist/src/remote-execute.d.ts +22 -0
  84. package/dist/src/remote-execute.js +1 -0
  85. package/dist/src/resource-blob.d.ts +10 -0
  86. package/dist/src/resource-blob.js +56 -0
  87. package/dist/src/scenario.d.ts +197 -0
  88. package/dist/src/scenario.js +425 -0
  89. package/dist/src/schemas.d.ts +78 -0
  90. package/dist/src/schemas.js +50 -0
  91. package/dist/src/serve-http.d.ts +48 -0
  92. package/dist/src/serve-http.js +340 -0
  93. package/dist/src/serve.d.ts +147 -0
  94. package/dist/src/serve.js +507 -0
  95. package/dist/src/shared-blob-index.d.ts +4 -0
  96. package/dist/src/shared-blob-index.js +126 -0
  97. package/dist/src/state-system.d.ts +70 -0
  98. package/dist/src/state-system.js +90 -0
  99. package/dist/src/storage.d.ts +101 -0
  100. package/dist/src/storage.js +337 -0
  101. package/dist/src/twin-fetch.d.ts +64 -0
  102. package/dist/src/twin-fetch.js +91 -0
  103. package/dist/src/types.d.ts +40 -0
  104. package/dist/src/types.js +1 -0
  105. package/dist/src/v1-removed.d.ts +159 -0
  106. package/dist/src/v1-removed.js +124 -0
  107. package/dist/src/volter-home.d.ts +5 -0
  108. package/dist/src/volter-home.js +10 -0
  109. package/dist/src/world-clock.d.ts +4 -0
  110. package/dist/src/world-clock.js +32 -0
  111. package/dist/src/world-env.d.ts +3 -0
  112. package/dist/src/world-env.js +22 -0
  113. package/dist/src/world-store-sql.d.ts +27 -0
  114. package/dist/src/world-store-sql.js +86 -0
  115. package/dist/src/world-store.d.ts +168 -0
  116. package/dist/src/world-store.js +475 -0
  117. package/dist/src/worldConfig.d.ts +9 -0
  118. package/dist/src/worldConfig.js +17 -0
  119. package/dist/stream-bridge.cjs +80 -0
  120. package/dist/vendor-hosts.cjs +200 -0
  121. package/generated/pack-facts.json +4306 -0
  122. package/inject.cjs +1097 -0
  123. package/network-policy.cjs +92 -0
  124. package/network-policy.d.cts +10 -0
  125. package/package.json +103 -0
  126. package/src/actions.ts +564 -0
  127. package/src/ancestry.ts +213 -0
  128. package/src/args.ts +14 -0
  129. package/src/blob-store.ts +185 -0
  130. package/src/brand-tokens.ts +17 -0
  131. package/src/changeset.ts +1032 -0
  132. package/src/client-bundle.ts +29 -0
  133. package/src/credential.ts +140 -0
  134. package/src/derived-core.ts +1004 -0
  135. package/src/derived.ts +176 -0
  136. package/src/emit.ts +242 -0
  137. package/src/executor.ts +431 -0
  138. package/src/file-response.ts +22 -0
  139. package/src/fork.ts +89 -0
  140. package/src/git/history.ts +177 -0
  141. package/src/git/index.ts +6 -0
  142. package/src/git/inflate.ts +125 -0
  143. package/src/git/objects.ts +110 -0
  144. package/src/git/pack.ts +105 -0
  145. package/src/git/refs.ts +25 -0
  146. package/src/git/smart-http.ts +149 -0
  147. package/src/hash.ts +66 -0
  148. package/src/head.ts +318 -0
  149. package/src/history.ts +246 -0
  150. package/src/index.ts +323 -0
  151. package/src/lifecycle.ts +8 -0
  152. package/src/log.ts +793 -0
  153. package/src/mirror-shell.ts +15 -0
  154. package/src/observe.ts +130 -0
  155. package/src/pack-assets.ts +81 -0
  156. package/src/packRegistry.ts +408 -0
  157. package/src/placeholder-remote.ts +81 -0
  158. package/src/proxy.ts +183 -0
  159. package/src/rateBudget.ts +1115 -0
  160. package/src/references.ts +46 -0
  161. package/src/remote-execute.ts +26 -0
  162. package/src/resource-blob.ts +57 -0
  163. package/src/scenario.ts +479 -0
  164. package/src/schemas.ts +56 -0
  165. package/src/serve-http.ts +299 -0
  166. package/src/serve.ts +618 -0
  167. package/src/shared-blob-index.ts +108 -0
  168. package/src/state-system.ts +115 -0
  169. package/src/storage.ts +407 -0
  170. package/src/twin-fetch.ts +147 -0
  171. package/src/types.ts +50 -0
  172. package/src/v1-removed.ts +172 -0
  173. package/src/volter-home.ts +11 -0
  174. package/src/world-clock.ts +33 -0
  175. package/src/world-env.ts +18 -0
  176. package/src/world-store-sql.ts +118 -0
  177. package/src/world-store.ts +572 -0
  178. package/src/worldConfig.ts +27 -0
  179. package/stream-bridge.cjs +80 -0
  180. package/vendor-hosts.cjs +200 -0
package/src/log.ts ADDED
@@ -0,0 +1,793 @@
1
+ // THE LOG (docs/concepts/the-model.md). One kind of entry, one log per
2
+ // branch, and a branch is a parent plus a position in the parent's log:
3
+ //
4
+ // parent log `events.jsonl` what the branch inherited — the origin's history as fetched, the
5
+ // placeholder's default data, and every entry of this branch that
6
+ // LANDED on the parent (a push, or a deploy at a root), with its
7
+ // receipt. Written only by fetch, refresh and landing; never by a
8
+ // write the app made.
9
+ // branch log `actions.jsonl` this branch's own entries, in order: every write the app made,
10
+ // recorded as it was served. Written only by the write path.
11
+ // branch.json the parent: the placeholder, a base world root at a position,
12
+ // or a served world's URL. Absent = an unbranched world whose
13
+ // parent is whatever its parent log holds.
14
+ // views/<hash>.json immutable verified segment ranges and projection layout.
15
+ // checkpoints/latest.json the tree bound to the exact inherited view: a read is
16
+ // the checkpoint plus the entries since, never a replay from zero.
17
+ //
18
+ // Landing replaces confirm-and-suppress: an entry that lands is COPIED to the parent log with its
19
+ // receipt (and under the vendor's id when the vendor minted one, `aliasOf` naming the local id),
20
+ // and the fold skips a branch entry whose id the parent already holds. Nothing is suppressed,
21
+ // rebound or quarantined by a bookkeeping row; `unpushed` is the branch log minus what the parent
22
+ // holds. The parent log accepts the v1 observed-event rows too (`toEntry` reads a delta event as
23
+ // an entry; a non-delta observation folds nothing and is kept for `listEvents`), so a protocol 1
24
+ // pack's own fold of the logs keeps working while it is deprecated.
25
+ import { captureHistory, historyChanges, historyEntryTime, historyEntries, historyLength, historyPrefix, inheritedHistory, inheritedHistoryKey, originHead, readHistoryView, replaceHistoryOrigin, saveHistoryView, validateHistoryOrigin, type HistoryLayout } from './history.ts';
26
+ import { dirname, join } from 'node:path';
27
+ import { canonicalStatePath, checkParent, commitParentPin, pinParent, withAncestryLock } from './ancestry.ts';
28
+ import { resolveReferences } from './references.ts';
29
+ import { hashFieldValue } from './hash.ts';
30
+ import type { SubjectFields } from './hash.ts';
31
+ import { worldPaths } from './storage.ts';
32
+ import { getActiveWorldStore } from './world-store.ts';
33
+ import type { WorldServiceEvent } from './types.ts';
34
+ import type { TwinResource } from './serve.ts';
35
+
36
+ /** The vendor's answer to one entry, on the entry itself. `landed` = on the parent, waiting;
37
+ * `deployed` = performed against the vendor; `refused` = a check said no; `failed` = the vendor
38
+ * said no; `skipped` = after a failure. */
39
+ export type Receipt = {
40
+ status: 'landed' | 'deployed' | 'refused' | 'failed' | 'skipped';
41
+ at: string;
42
+ /** the id the vendor minted, when it differs from the local one */
43
+ externalId?: string;
44
+ url?: string;
45
+ /** the check that refused, or the vendor's words */
46
+ reason?: string;
47
+ data?: Record<string, unknown>;
48
+ };
49
+
50
+ export type EntryOp = 'set' | 'revert';
51
+
52
+ /** One line of a log. `TwinAction` in actions.ts is this type by another name; every field a
53
+ * write records lives here. */
54
+ export type Entry = {
55
+ /** where the entry came from when not this branch's own write: the placeholder origin (the default data) */
56
+ provenance?: 'placeholder';
57
+ /** the observation this entry was folded in (a refresh): one id per look; no position falls inside one */
58
+ batch?: string;
59
+ id: string;
60
+ service: string;
61
+ op: EntryOp;
62
+ subject: { type: string; id: string };
63
+ occurredAt: string;
64
+ operation?: string;
65
+ actor?: { kind: 'agent' | 'human' | 'bot' | 'system'; id?: string };
66
+ fields?: SubjectFields;
67
+ projection?: { creates?: Array<{ type: string; id: string; fields: SubjectFields }>; updates?: Array<{ type: string; id: string; fields: SubjectFields }>; deletes?: Array<{ type: string; id: string }>; emits?: Array<{ kind: 'webhook' | 'event' | 'notification'; target: string; payload: Record<string, unknown> }> };
68
+ revertsActionId?: string;
69
+ /** a landed copy: the local entry id this row lands (its subject may carry the vendor's id) */
70
+ landsId?: string;
71
+ /** a landed copy under the vendor's id: the local subject id it stands for */
72
+ aliasOf?: string;
73
+ receipt?: Receipt;
74
+ /** a v1 observed row kept verbatim for `listEvents`; folds nothing unless `fields` was derived */
75
+ event?: WorldServiceEvent;
76
+ [extra: string]: unknown;
77
+ };
78
+
79
+ /** A BRANCH IS A POINTER (contract "Just like Neon", 1): its parent — a world at a path, or an origin at
80
+ * a URL — and the POSITION in the parent's whole log this branch starts from. A path parent is read
81
+ * through its immutable view, never its mutable current pointer; a URL parent uses a retained
82
+ * cache view. Only rebase or acknowledged push moves the selected view and position. */
83
+ export type BranchMeta = {
84
+ parent?: { at: string; position: number; view?: string; remoteView?: string; generation?: string; directory?: string; viewDirectory?: string };
85
+ branchedAt?: string;
86
+ origin?: import('./history.ts').HistoryOrigin;
87
+ fetchedOrigin?: import('./history.ts').HistoryOrigin;
88
+ previousParents?: Array<{ directory: string; generation: string }>;
89
+ };
90
+
91
+ export const CHECKPOINT_EVERY = 100;
92
+ export const DELTA_SUFFIX = '.delta';
93
+
94
+ export function parentLogPath(service: string, root?: string): string { return worldPaths(service, root).events; }
95
+ export function branchLogPath(service: string, root?: string): string { return join(dirname(worldPaths(service, root).events), 'actions.jsonl'); }
96
+ export function branchMetaPath(service: string, root?: string): string { return join(dirname(worldPaths(service, root).events), 'branch.json'); }
97
+ /** The fetched cache of a URL parent's whole log — what `fetch` extends and a read folds up to the position. */
98
+ export function originLogPath(service: string, root?: string): string { return join(dirname(worldPaths(service, root).events), 'origin.jsonl'); }
99
+ /** A parent is at a URL when it is not a path on this machine. */
100
+ export function isUrlParent(at: string): boolean { return /^[a-z][a-z0-9+.-]*:\/\//i.test(at); }
101
+ export function checkpointPath(service: string, root?: string): string { return join(dirname(worldPaths(service, root).events), 'checkpoints', 'latest.json'); }
102
+
103
+ // A log only grows by appends, so each store keeps the rows it last parsed per path and parses only what
104
+ // was appended since: a long walk reads its logs on every request, and re-parsing them made each request's
105
+ // cost grow with the World's age. Rows are shared between readers and never mutated. On a store that can
106
+ // read a byte range, only the appended bytes are read: from the start of the last entry parsed, which must
107
+ // still be that entry (every entry carries its own id), so a log rewritten or replaced is read afresh (an edit in
108
+ // place that kept the last entry at its byte offset would read as appends; no writer edits a log in place). The
109
+ // whole file per write, a new string a little longer each time with the last one held, grew a twin's
110
+ // native memory without bound (a PlanetScale twin: 3.2 GB in a 19-minute walk).
111
+ type ParsedLog = { rows: unknown[]; lines: number; content?: string; bytes?: number; tail?: string };
112
+ const parsedRows = new WeakMap<object, Map<string, ParsedLog>>();
113
+
114
+ function parseLines<T>(path: string, lines: string[], offset: number, out: T[]): void {
115
+ for (const [i, line] of lines.entries()) {
116
+ if (!line.trim()) continue;
117
+ try { out.push(JSON.parse(line) as T); } catch (error) { throw new Error(`${path}:${offset + i + 1}: invalid JSONL row: ${(error as Error).message}`); }
118
+ }
119
+ }
120
+
121
+ /** A log's content from the start of its last non-blank line to its end: what must still stand at that byte offset
122
+ * for the log to have only grown since. `undefined` when the content does not end a line or holds no entry. */
123
+ function tailOf(content: string): string | undefined {
124
+ if (!content.endsWith('\n')) return undefined;
125
+ // blank as parseLines reads it: a line that trims to nothing (a CRLF's `\r`, spaces) is no entry
126
+ let start = content.lastIndexOf('\n', content.length - 2) + 1;
127
+ while (start > 0 && !content.slice(start, content.indexOf('\n', start)).trim()) start = content.lastIndexOf('\n', start - 2) + 1;
128
+ if (!content.slice(start, content.indexOf('\n', start)).trim()) return undefined;
129
+ return content.slice(start);
130
+ }
131
+
132
+ function readRows<T>(path: string): T[] {
133
+ const store = getActiveWorldStore();
134
+ const cache = parsedRows.get(store) ?? parsedRows.set(store, new Map()).get(store)!;
135
+ const held = cache.get(path);
136
+ if (store.readRange && held?.bytes !== undefined && held.tail !== undefined) {
137
+ const chunk = store.readRange(path, held.bytes - Buffer.byteLength(held.tail));
138
+ if (chunk === null) { cache.delete(path); return []; }
139
+ const appended = chunk.startsWith(held.tail) ? chunk.slice(held.tail.length) : undefined;
140
+ if (appended === '') return held.rows.slice() as T[];
141
+ // only whole appended lines extend the rows; a torn last line is read (and refused) as the whole file reads it
142
+ if (appended !== undefined && appended.endsWith('\n')) {
143
+ const rows = held.rows.slice();
144
+ const lines = appended.split('\n');
145
+ parseLines(path, lines, held.lines, rows);
146
+ const tail = tailOf(held.tail + appended)!;
147
+ cache.set(path, { rows, lines: held.lines + lines.length - 1, bytes: held.bytes + Buffer.byteLength(appended), tail });
148
+ return rows.slice() as T[];
149
+ }
150
+ }
151
+ const content = store.read(path);
152
+ if (content === null) { cache.delete(path); return []; }
153
+ if (!store.readRange && held?.content !== undefined) {
154
+ if (held.content === content) return held.rows.slice() as T[];
155
+ // an append after a whole row extends the rows already parsed; anything else is parsed afresh
156
+ if (held.content.endsWith('\n') && content.startsWith(held.content)) {
157
+ const rows = held.rows.slice();
158
+ parseLines(path, content.slice(held.content.length).split('\n'), held.lines, rows);
159
+ cache.set(path, { content, rows, lines: held.lines + content.slice(held.content.length).split('\n').length - 1 });
160
+ return rows.slice() as T[];
161
+ }
162
+ }
163
+ const rows: T[] = [];
164
+ const lines = content.split('\n');
165
+ parseLines(path, lines, 0, rows);
166
+ const tail = store.readRange ? tailOf(content) : undefined;
167
+ cache.set(path, tail !== undefined ? { rows, lines: lines.length - 1, bytes: Buffer.byteLength(content), tail } : { content, rows, lines: lines.length - 1 });
168
+ return rows.slice();
169
+ }
170
+
171
+ export function readBranchMeta(service: string, root?: string): BranchMeta | null {
172
+ const raw = getActiveWorldStore().read(branchMetaPath(service, root));
173
+ if (raw === null) return null;
174
+ const meta = JSON.parse(raw) as BranchMeta & { parent?: { root?: string; parentCount?: number; branchCount?: number } };
175
+ // a pointer written before positions were one number: the parent's whole log is parent then branch
176
+ if (meta.parent && meta.parent.at === undefined && meta.parent.root !== undefined) meta.parent = { at: meta.parent.root, position: (meta.parent.parentCount ?? 0) + (meta.parent.branchCount ?? 0) };
177
+ return meta as BranchMeta;
178
+ }
179
+
180
+ export function writeBranchMeta(service: string, meta: BranchMeta, root?: string): void {
181
+ withAncestryLock(() => {
182
+ if (meta.parent && (!Number.isInteger(meta.parent.position) || meta.parent.position < 0)) throw new Error('Invalid branch position');
183
+ const path = branchMetaPath(service, root);
184
+ const old = readBranchMeta(service, root);
185
+ // Tracking can name a different owner from the immediate fork parent. Retain its immutable
186
+ // view through the existing view dependency mechanism, including empty origin snapshots.
187
+ for (const origin of [meta.origin, meta.fetchedOrigin]) if (origin) {
188
+ validateHistoryOrigin(origin);
189
+ checkParent(origin.directory, origin.generation);
190
+ const descriptor = historyPrefix(readHistoryView(origin.directory, origin.view), origin.position);
191
+ saveHistoryView(worldPaths(service, root).dir, { ...descriptor, origin: { ...origin, depth: 0 } });
192
+ }
193
+ const previousParents = [...(old?.previousParents ?? [])];
194
+ if (old?.parent?.directory && old.parent.generation) previousParents.push({ directory: old.parent.directory, generation: old.parent.generation });
195
+ if (previousParents.length) meta = { ...meta, previousParents: [...new Map(previousParents.map(p => [p.directory, p])).values()] };
196
+ if (meta.parent && !isUrlParent(meta.parent.at)) {
197
+ const directory = canonicalStatePath(worldPaths(service, meta.parent.at).dir);
198
+ const view = meta.parent.view ?? captureHistory(service, meta.parent.at).view;
199
+ const viewDirectory = meta.parent.viewDirectory ?? directory;
200
+ if (meta.parent.viewDirectory && canonicalStatePath(viewDirectory) !== canonicalStatePath(worldPaths(service, root).dir)) throw new Error('Composed history view must belong to this branch');
201
+ const descriptor = readHistoryView(viewDirectory, view);
202
+ historyPrefix(descriptor, meta.parent.position);
203
+ const parent = meta.parent;
204
+ const generation = pinParent(directory, path, parent.generation);
205
+ meta = { ...meta, parent: { ...parent, view, at: canonicalStatePath(parent.at), directory, generation } };
206
+ }
207
+ if (meta.parent && isUrlParent(meta.parent.at) && !meta.parent.view) {
208
+ const head = originHead(service, root);
209
+ if (head) meta = { ...meta, parent: { ...meta.parent, view: head.view, remoteView: head.remoteView } };
210
+ else if (meta.parent.position === 0) {
211
+ const empty = captureHistory(service, root);
212
+ meta = { ...meta, parent: { ...meta.parent, view: empty.view } };
213
+ } else throw new Error('Origin has no completed immutable history view');
214
+ }
215
+ getActiveWorldStore().mkdir(dirname(path));
216
+ getActiveWorldStore().writeAtomic(path, `${JSON.stringify(meta, null, 2)}\n`);
217
+ if (meta.parent?.directory && !isUrlParent(meta.parent.at)) commitParentPin(meta.parent.directory, path);
218
+ });
219
+ }
220
+
221
+ /** A v1 observed row as an entry: a delta event folds its `changed.*.after`; anything else folds
222
+ * nothing and rides along for `listEvents`. An entry row is itself. */
223
+ export function toEntry(row: Record<string, unknown>): Entry {
224
+ if (typeof row.op === 'string') return row as Entry;
225
+ const event = row as unknown as WorldServiceEvent;
226
+ const data = (event.data ?? {}) as Record<string, unknown>;
227
+ const changed = data.changed as Record<string, { after?: unknown }> | undefined;
228
+ const isDelta = typeof event.type === 'string' && event.type.endsWith(DELTA_SUFFIX) && changed !== undefined && typeof changed === 'object';
229
+ const isEgress = typeof event.type === 'string' && (event.type.endsWith('.write.intent') || event.type.endsWith('.write.result'));
230
+ // a delta row folds what changed; a flat observed row (a connector's snapshot, a fold of a webhook)
231
+ // folds its data as the subject's fields — and a `changed` map riding on it (`{ field: { after } }`)
232
+ // folds its afters over them, so the shape means the same thing on any row; an egress record folds nothing
233
+ const deltaShaped = changed !== undefined && changed !== null && typeof changed === 'object' && !Array.isArray(changed)
234
+ && Object.values(changed).every((v) => v !== null && typeof v === 'object' && ('after' in v || 'before' in v));
235
+ const afters = (): SubjectFields => Object.fromEntries(Object.entries(changed!).map(([k, v]) => [k, v?.after]));
236
+ const { changed: _changed, ...flat } = data;
237
+ const fields: SubjectFields | undefined = isEgress ? undefined : isDelta ? afters() : deltaShaped ? { ...(flat as SubjectFields), ...afters() } : (data as SubjectFields);
238
+ return {
239
+ id: event.id, service: event.service, op: 'set', subject: event.subject, occurredAt: event.occurredAt,
240
+ ...(fields ? { fields } : {}), event,
241
+ // the default data's rows are the placeholder origin's (docs/concepts/the-model.md#a-branch-is-a-position-not-a-copy, "default data"): a read hides them from the log
242
+ ...(event.origin === 'virtual' || (data as { provenance?: unknown }).provenance === 'placeholder' ? { provenance: 'placeholder' as const } : {}),
243
+ };
244
+ }
245
+
246
+ /** An entry as a v1 observed row: its own row when it came from one, else a delta event. */
247
+ export function toEvent(entry: Entry): WorldServiceEvent {
248
+ if (entry.event) return entry.event;
249
+ const fields = entry.fields ?? {};
250
+ return {
251
+ id: entry.id, service: entry.service, type: `${entry.service}.${entry.subject.type}${DELTA_SUFFIX}`, schemaVersion: 1,
252
+ idempotencyKey: entry.id, occurredAt: entry.occurredAt, observedAt: entry.receipt?.at ?? entry.occurredAt,
253
+ origin: entry.receipt ? 'external' : 'connector', subject: entry.subject,
254
+ ...(entry.actor ? { actor: entry.actor } : {}),
255
+ ...(entry.receipt?.externalId ? { external: { provider: entry.service, id: entry.receipt.externalId, ...(entry.receipt.url ? { url: entry.receipt.url } : {}) } } : {}),
256
+ data: { changed: Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, { after: v }])), ...(entry.receipt ? { receipt: entry.receipt } : {}) },
257
+ } as WorldServiceEvent;
258
+ }
259
+
260
+ /** This root's own parent-log rows, as entries. */
261
+ function ownParentEntries(service: string, root?: string): Entry[] {
262
+ return readRows<Record<string, unknown>>(parentLogPath(service, root)).map(toEntry);
263
+ }
264
+
265
+ /** This root's own branch-log rows. */
266
+ export function branchEntries(service: string, root?: string): Entry[] {
267
+ return readRows<Entry>(branchLogPath(service, root));
268
+ }
269
+
270
+ /** The parent as this branch sees it: the base world's view at the branch position (its parent
271
+ * log to `parentCount`, its branch log to `branchCount`), then everything this root's own parent
272
+ * log holds (fetched, refreshed or landed since). An unbranched root is just its own parent log. */
273
+ export function parentEntries(service: string, root?: string, seen: Set<string> = new Set()): Entry[] {
274
+ const view = inheritedHistory(service, root);
275
+ return [...(view ? historyEntries(view) : []), ...ownParentEntries(service, root)];
276
+ }
277
+
278
+ /** The fetched cache of a URL parent's whole log (every entry fetched so far, past the position too). */
279
+ export function originEntries(service: string, root?: string): Entry[] {
280
+ const head = originHead(service, root);
281
+ return head ? historyEntries(readHistoryView(worldPaths(service, root).dir, head.view)) : readRows<Record<string, unknown>>(originLogPath(service, root)).map(toEntry);
282
+ }
283
+
284
+ /** Extend the cache with one of the parent's entries; deduplicated by id. */
285
+ export function appendOriginEntry(entry: Entry, root?: string): { appended: boolean } {
286
+ const path = originLogPath(entry.service, root);
287
+ const store = getActiveWorldStore();
288
+ store.mkdir(dirname(path));
289
+ return withAncestryLock(() => store.withLock(`${path}.lock`, () => {
290
+ if (readRows<{ id?: string }>(path).some((r) => r.id === entry.id)) return { appended: false };
291
+ store.append(path, `${JSON.stringify(entry)}\n`);
292
+ return { appended: true };
293
+ }));
294
+ }
295
+
296
+ /** Where the parent stands now: the length of its whole log (live at a path, the cache for a URL). */
297
+ export function parentPosition(service: string, root?: string): number | null {
298
+ const meta = readBranchMeta(service, root);
299
+ if (!meta?.parent) return null;
300
+ return isUrlParent(meta.parent.at) ? originEntries(service, root).length : wholeLog(service, meta.parent.at).length;
301
+ }
302
+
303
+ /** Ids the parent holds, including the local ids its landed copies stand for. */
304
+ export function landedIds(parent: Entry[]): Set<string> {
305
+ const out = new Set<string>();
306
+ for (const e of parent) { out.add(e.id); if (e.landsId) out.add(e.landsId); }
307
+ return out;
308
+ }
309
+
310
+ /** The vendor-minted id per local subject: every landed copy that carries `aliasOf`. A local id the
311
+ * vendor itself holds names the vendor's subject, never the alias: a World that minted a local id
312
+ * before it had observed the vendor's own subject under that id (its tree was behind) must not fold
313
+ * the vendor's subject onto the landed one, nor send a caller who addresses the vendor's id there. */
314
+ export function aliasesFrom(parent: Entry[]): Map<string, string> {
315
+ const vendorHeld = new Set<string>();
316
+ for (const e of parent) if (e.batch !== undefined || e.operation === 'observed' || e.event !== undefined || e.landsId !== undefined) vendorHeld.add(`${e.subject.type}:${e.subject.id}`);
317
+ const out = new Map<string, string>();
318
+ for (const e of parent) {
319
+ if (!e.aliasOf || e.aliasOf === e.subject.id) continue;
320
+ const from = `${e.subject.type}:${e.aliasOf}`;
321
+ if (!vendorHeld.has(from)) out.set(from, e.subject.id);
322
+ }
323
+ return out;
324
+ }
325
+
326
+ const META = new Set(['id', 'type', 'updatedAt']);
327
+ type Subject = { type: string; id: string; updatedAt: string; fields: SubjectFields };
328
+ export type Tree = Map<string, Subject>;
329
+
330
+ /** Last write per field. `deleted: true` is the tombstone a delete carries; it holds until the subject
331
+ * is written again (a create under the same address resurrects it — the write need not say so). */
332
+ function overlay(tree: Tree, type: string, id: string, fields: SubjectFields, at: string): void {
333
+ const key = `${type}:${id}`;
334
+ const existing = tree.get(key) ?? { type, id, updatedAt: at, fields: {} };
335
+ const merged: SubjectFields = { ...existing.fields, ...fields };
336
+ if (existing.fields.deleted === true && fields.deleted !== true) delete merged.deleted;
337
+ tree.set(key, { ...existing, updatedAt: at, fields: merged });
338
+ }
339
+
340
+ /** Fold entries onto a tree, in order: `set` overlays its fields and applies its projection; a
341
+ * reverted entry folds nothing; a `confirm` row (v1) folds nothing. `skip` names entries the
342
+ * parent already holds. */
343
+ export function foldEntries(tree: Tree, entries: Entry[], skip: Set<string> = new Set()): void {
344
+ const reverted = new Set(entries.filter((e) => e.op === 'revert' && e.revertsActionId).map((e) => e.revertsActionId!));
345
+ for (const e of entries) {
346
+ if (e.op !== 'set' || reverted.has(e.id) || skip.has(e.id)) continue;
347
+ // a landed copy the vendor did not take (refused by a check, failed at the vendor) is on the log with
348
+ // its receipt, and not in the tree: the tree holds what the vendor holds or what is still queued
349
+ if (e.landsId && (e.receipt?.status === 'refused' || e.receipt?.status === 'failed')) continue;
350
+ if (e.fields) overlay(tree, e.subject.type, e.subject.id, e.fields, e.occurredAt);
351
+ if (e.projection) {
352
+ for (const c of e.projection.creates ?? []) overlay(tree, c.type, c.id, c.fields, e.occurredAt);
353
+ for (const u of e.projection.updates ?? []) overlay(tree, u.type, u.id, u.fields, e.occurredAt);
354
+ for (const d of e.projection.deletes ?? []) tree.delete(`${d.type}:${d.id}`);
355
+ }
356
+ }
357
+ }
358
+
359
+ /** Fold aliases: whatever still sits under a local id (a later local update against the id the
360
+ * caller holds) folds onto the vendor's row. */
361
+ export function foldAliases(tree: Tree, aliases: Map<string, string>, service?: string, only?: Set<string>): void {
362
+ for (const [fromKey, toId] of aliases) {
363
+ const stray = tree.get(fromKey);
364
+ if (!stray) continue;
365
+ overlay(tree, stray.type, toId, stray.fields, stray.updatedAt);
366
+ tree.delete(fromKey);
367
+ }
368
+ // declared references follow the adoption (docs/contributing/architecture.md#alias-aware-lookup-at-the-request-boundary), whatever order the entries folded in
369
+ if (service !== undefined && aliases.size > 0) {
370
+ // `only`: an extension's touched subjects, the only ones an earlier fold has not already resolved
371
+ for (const s of only ? [...only].map((k) => tree.get(k)).filter((held): held is Subject => held !== undefined) : tree.values()) {
372
+ const resolved = resolveReferences(service, s.type, s.fields, aliases);
373
+ if (resolved !== s.fields) s.fields = resolved;
374
+ }
375
+ }
376
+ }
377
+
378
+ /** The tree as resources: `id`, `type` and `updatedAt` are the SUBJECT's. A resource whose own fields
379
+ * carry a different `id`, `type` or `updatedAt` (a Jira issue has a key and a numeric id; a Stripe
380
+ * price has a `type` of its own) keeps them BESIDE the row, on a non-enumerable property: a spread,
381
+ * `Object.entries`, JSON and a conformance sweep never see them, so a pack that serves the row as it
382
+ * stands serves exactly the vendor's fields it wrote; `ownFields` restores them for a pack that wants
383
+ * the vendor's own values in the vendor's positions. */
384
+ const OWN = Symbol.for('volter.resource.own');
385
+ export function treeResources(tree: Tree): TwinResource[] {
386
+ return [...tree.values()].map((s) => {
387
+ const out: Record<string, unknown> = { id: s.id, type: s.type, updatedAt: s.updatedAt };
388
+ const own: Record<string, unknown> = {};
389
+ for (const [k, v] of Object.entries(s.fields)) { if (!META.has(k)) out[k] = v; else if (v !== undefined && v !== (out[k] as unknown)) own[k] = v; }
390
+ if (Object.keys(own).length) Object.defineProperty(out, OWN, { value: own, enumerable: false });
391
+ return out as TwinResource;
392
+ });
393
+ }
394
+
395
+ /** A copy of a resource a reader may change: its fields, and the vendor's own id/type/updatedAt it keeps beside them. */
396
+ export function copyResource(r: TwinResource): TwinResource {
397
+ const out = { ...r };
398
+ const own = (r as unknown as Record<symbol, unknown>)[OWN];
399
+ if (own !== undefined) Object.defineProperty(out, OWN, { value: own, enumerable: false });
400
+ return out;
401
+ }
402
+
403
+ /** A resource's own fields, the vendor's way: the subject's address stripped, its own `id`/`type`/`updatedAt` restored when it had them. */
404
+ export function ownFields(r: TwinResource): Record<string, unknown> {
405
+ const { id: _id, type: _type, updatedAt: _at, ...rest } = r;
406
+ const own = ((r as unknown as Record<symbol, unknown>)[OWN] ?? {}) as { id?: unknown; type?: unknown; updatedAt?: unknown };
407
+ // restored first: a vendor's object leads with its id, and a reader's grep may count on the order
408
+ return { ...(own.id !== undefined ? { id: own.id } : {}), ...(own.type !== undefined ? { type: own.type } : {}), ...rest, ...(own.updatedAt !== undefined ? { updatedAt: own.updatedAt } : {}) };
409
+ }
410
+
411
+ /** `facts`, when present, is the parent and branch logs' size and version with branch.json at the cut
412
+ * (parentKeyOf/statKey): a cold read whose facts match trusts the cut without re-hashing the logs. */
413
+ type Checkpoint = { parentDigest: string; branchDigest: string; branchCount: number; subjects: Subject[]; facts?: string };
414
+
415
+ export function dropCheckpoint(service: string, root?: string): void { getActiveWorldStore().remove(checkpointPath(service, root)); }
416
+
417
+ /** Preserve each ancestor's observed-then-local overlay, including landed IDs and aliases. */
418
+ export function foldHistory(entries: Entry[], layout: HistoryLayout, service?: string): Tree {
419
+ const baseCount = layout.base ? historyLength(layout.base) : 0;
420
+ const tree: Tree = layout.base ? foldHistory(entries.slice(0, baseCount), layout.base, service) : new Map();
421
+ const parent = entries.slice(0, baseCount + layout.parent);
422
+ foldEntries(tree, parent.slice(baseCount));
423
+ foldEntries(tree, entries.slice(baseCount + layout.parent, baseCount + layout.parent + layout.branch), landedIds(parent));
424
+ foldAliases(tree, aliasesFrom(parent), service);
425
+ return tree;
426
+ }
427
+ export function readTree(service: string, root?: string, opts: { until?: string; at?: number; view?: string } = {}): TwinResource[] {
428
+ return treeResources(readTreeMap(service, root, opts));
429
+ }
430
+
431
+ /** The upstream view for observation diffing, without this root's local overlay. Inherited
432
+ * history keeps its own layout: an ancestor's local writes are part of our pinned base. */
433
+ export function readParentTreeMap(service: string, root?: string): Tree {
434
+ return withAncestryLock(() => {
435
+ const inherited = inheritedHistory(service, root);
436
+ const own = ownParentEntries(service, root);
437
+ return foldHistory([...(inherited ? historyEntries(inherited) : []), ...own], {
438
+ ...(inherited ? { base: inherited.layout } : {}), parent: own.length, branch: 0,
439
+ }, service);
440
+ });
441
+ }
442
+
443
+ // A READ'S TREE, MEMOIZED per store and per process or isolate. What a read folds is named by cheap
444
+ // facts: the parent log's size and version with branch.json (whose parent names an immutable history
445
+ // view and a position), and the branch log's size and version (WorldStore.stat: every append or
446
+ // rewrite by any writer changes them). While they stand, the fold is the same, and a hit hands out a
447
+ // clone, as the checkpoint path does. When only the branch log grew by appends (the common write), the
448
+ // memoized tree is extended in place with just the new entries (the memo's subjects are never handed
449
+ // out: every read gets a clone), and the extension records which subjects it touched, so a pack's own
450
+ // projection of the tree can follow a write without refolding (`treeChangesSince`). Anything else is the
451
+ // full read below. Without it every read re-parsed and re-hashed the World's whole log to validate its
452
+ // checkpoint (a 6448-event Jira World: seconds per request).
453
+ /** One extension of a memo: the subjects it touched, and those it put at the tree's end (a subject new to the tree,
454
+ * or deleted and made again), in the order they now stand there. */
455
+ type TreeStep = { from: string; to: string; keys: Set<string>; appended: string[] };
456
+ type TreeMemo = { parentKey: string; branchKey: string; branchCount: number; lastBranchId: string | null; landed: Set<string>; aliases: Map<string, string>; tree: Tree; steps: TreeStep[] };
457
+ const treeMemos = new WeakMap<object, Map<string, TreeMemo>>();
458
+ /** How many extensions a memo remembers the touched subjects of; a projection further behind refolds. */
459
+ const TREE_STEPS_KEPT = 64;
460
+ function statKey(path: string): string { const s = getActiveWorldStore().stat(path); return s ? `${s.size}:${s.mtimeMs}` : '-'; }
461
+ /** What a service's PARENT side at `root` is folded from (its parent log, branch.json and inherited history), as one
462
+ * string: while it stands, its aliases and landed ids are the same, whatever the branch's own log does. */
463
+ export function parentStamp(service: string, root?: string): string { return parentKeyOf(service, root); }
464
+ function parentKeyOf(service: string, root?: string): string { return `${statKey(parentLogPath(service, root))}|${getActiveWorldStore().read(branchMetaPath(service, root)) ?? ''}|${inheritedHistoryKey(service, root)}`; }
465
+ /** What a service's tree (and every subject's history) at `root` is folded from, as one string: while it
466
+ * stands, nothing was written. A pack that projects the tree into its own state memoizes on it. */
467
+ export function treeStamp(service: string, root?: string): string { return `${parentKeyOf(service, root)}#${statKey(branchLogPath(service, root))}`; }
468
+ const cloneTree = (tree: Tree): Tree => new Map([...tree].map(([k, s]) => [k, { ...s, fields: { ...s.fields } }]));
469
+ const memoStamp = (m: TreeMemo): string => `${m.parentKey}#${m.branchKey}`;
470
+
471
+ /** The subject keys entries write (as foldEntries folds them), and the vendor rows an alias moves a local one onto. */
472
+ function touchedKeys(entries: Entry[], aliases: Map<string, string>): Set<string> {
473
+ const keys = new Set<string>();
474
+ for (const e of entries) {
475
+ if (e.fields) keys.add(`${e.subject.type}:${e.subject.id}`);
476
+ for (const c of e.projection?.creates ?? []) keys.add(`${c.type}:${c.id}`);
477
+ for (const u of e.projection?.updates ?? []) keys.add(`${u.type}:${u.id}`);
478
+ for (const d of e.projection?.deletes ?? []) keys.add(`${d.type}:${d.id}`);
479
+ }
480
+ for (const key of [...keys]) { const to = aliases.get(key); if (to !== undefined) keys.add(`${key.slice(0, key.indexOf(':'))}:${to}`); }
481
+ return keys;
482
+ }
483
+
484
+ /** The keys `fold` inserts into `tree` (a Map puts an inserted key at its end, so a key deleted and set again moves
485
+ * there too), in the order they then stand, each once. */
486
+ function insertionsDuring(tree: Tree, fold: () => void): string[] {
487
+ const order = new Map<string, true>();
488
+ const own = tree as Tree & { set: Tree['set'] };
489
+ own.set = (key, value) => { if (!tree.has(key)) { order.delete(key); order.set(key, true); } return Map.prototype.set.call(tree, key, value) as Tree; };
490
+ try { fold(); } finally { delete (own as { set?: unknown }).set; }
491
+ return [...order.keys()].filter((k) => tree.has(k));
492
+ }
493
+
494
+ export function readTreeMap(service: string, root?: string, opts: { until?: string; at?: number; view?: string } = {}): Tree {
495
+ if (opts.until !== undefined || opts.at !== undefined || opts.view !== undefined) return readTreeMapFull(service, root, opts);
496
+ const read = currentTree(service, root);
497
+ return read.memo ? cloneTree(read.tree) : read.tree;
498
+ }
499
+
500
+ /** The current tree: the memo's own (never to be handed out) when the memo holds it, else a fresh fold. */
501
+ function currentTree(service: string, root?: string): { tree: Tree; memo?: TreeMemo } {
502
+ const store = getActiveWorldStore();
503
+ let memos = treeMemos.get(store);
504
+ if (!memos) { memos = new Map(); treeMemos.set(store, memos); }
505
+ const slot = `${service}\u0000${root ?? ''}`;
506
+ // A hit needs no coordinator: a memo is only ever stored from a fold made under it, and while the
507
+ // facts still match that fold, nothing was written since. A writer mid-batch has moved them.
508
+ const unlocked = memos.get(slot);
509
+ if (unlocked && unlocked.parentKey === parentKeyOf(service, root) && unlocked.branchKey === statKey(branchLogPath(service, root))) return { tree: unlocked.tree, memo: unlocked };
510
+ return withAncestryLock(() => {
511
+ const parentKey = parentKeyOf(service, root); const branchKey = statKey(branchLogPath(service, root));
512
+ const held = memos.get(slot);
513
+ if (held && held.parentKey === parentKey && held.branchKey === branchKey) return { tree: held.tree, memo: held };
514
+ const branch = branchEntries(service, root);
515
+ const last = (entries: Entry[]): string | null => (entries.length ? entries[entries.length - 1]!.id : null);
516
+ if (held && held.parentKey === parentKey && branch.length >= held.branchCount
517
+ && (held.branchCount === 0 ? held.lastBranchId === null : branch[held.branchCount - 1]?.id === held.lastBranchId)
518
+ && !branch.slice(held.branchCount).some((e) => e.op === 'revert')) {
519
+ const fresh = branch.slice(held.branchCount);
520
+ const keys = touchedKeys(fresh, held.aliases);
521
+ const from = memoStamp(held);
522
+ let appended: string[];
523
+ try {
524
+ appended = insertionsDuring(held.tree, () => {
525
+ foldEntries(held.tree, fresh, held.landed);
526
+ foldAliases(held.tree, held.aliases, service, keys);
527
+ });
528
+ } catch (error) { memos.delete(slot); throw error; } // a half-folded memo is never read again
529
+ Object.assign(held, { branchKey, branchCount: branch.length, lastBranchId: last(branch) });
530
+ held.steps.push({ from, to: memoStamp(held), keys, appended });
531
+ if (held.steps.length > TREE_STEPS_KEPT) held.steps.splice(0, held.steps.length - TREE_STEPS_KEPT);
532
+ return { tree: held.tree, memo: held };
533
+ }
534
+ const tree = readTreeMapFull(service, root);
535
+ const parent = parentEntries(service, root);
536
+ // the facts are read again after the fold: a write that raced it leaves nothing memoized
537
+ if (parentKeyOf(service, root) === parentKey && statKey(branchLogPath(service, root)) === branchKey) {
538
+ const memo: TreeMemo = { parentKey, branchKey, branchCount: branch.length, lastBranchId: last(branch), landed: landedIds(parent), aliases: aliasesFrom(parent), tree, steps: [] };
539
+ memos.set(slot, memo);
540
+ return { tree, memo };
541
+ }
542
+ return { tree };
543
+ });
544
+ }
545
+
546
+ /** What changed in a service's tree since `since` (a `treeStamp` an earlier read was made at): the subjects
547
+ * written since, as resources; the keys (`type:id`) of those no longer in the tree; and, of the changed, the keys
548
+ * now at the tree's end in the order they stand there (new to the tree, or deleted and made again), which a
549
+ * projection kept in the tree's order moves to its end in that order, updating the rest where they stand.
550
+ * `undefined` when this process cannot say (no memo, the parent moved, a revert, or `since` older than the memo
551
+ * remembers): the caller refolds. `stamp` is the state the answer brings the caller to. */
552
+ export function treeChangesSince(service: string, root: string | undefined, since: string): { stamp: string; changed: TwinResource[]; removed: string[]; appended: string[] } | undefined {
553
+ const { tree, memo } = currentTree(service, root);
554
+ if (!memo) return undefined;
555
+ const stamp = memoStamp(memo);
556
+ if (since === stamp) return { stamp, changed: [], removed: [], appended: [] };
557
+ const first = memo.steps.findIndex((s) => s.from === since);
558
+ if (first < 0) return undefined;
559
+ const keys = new Set<string>(); const order = new Map<string, true>();
560
+ for (let i = first; i < memo.steps.length; i++) {
561
+ if (i > first && memo.steps[i]!.from !== memo.steps[i - 1]!.to) return undefined;
562
+ for (const k of memo.steps[i]!.keys) keys.add(k);
563
+ for (const k of memo.steps[i]!.appended) { order.delete(k); order.set(k, true); }
564
+ }
565
+ if (memo.steps[memo.steps.length - 1]!.to !== stamp) return undefined;
566
+ const present: Tree = new Map(); const removed: string[] = [];
567
+ for (const k of keys) { const s = tree.get(k); if (s) present.set(k, s); else removed.push(k); }
568
+ return { stamp, changed: treeResources(present), removed, appended: [...order.keys()].filter((k) => tree.has(k)) };
569
+ }
570
+
571
+ function readTreeMapFull(service: string, root?: string, opts: { until?: string; at?: number; view?: string } = {}): Tree {
572
+ return withAncestryLock(() => {
573
+ if (opts.at !== undefined || opts.view !== undefined) {
574
+ const view = opts.view ? readHistoryView(worldPaths(service, root).dir, opts.view) : captureHistory(service, root).descriptor;
575
+ const at = opts.at ?? historyLength(view.layout);
576
+ const entries = historyEntries(view);
577
+ if (at > 0 && at < entries.length && entries[at]?.batch && entries[at]?.batch === entries[at - 1]?.batch) {
578
+ let from = at; let to = at; const batch = entries[at]!.batch;
579
+ while (from > 0 && entries[from - 1]?.batch === batch) from--;
580
+ while (to < entries.length && entries[to]?.batch === batch) to++;
581
+ throw new Error(`position ${at} falls inside observation ${batch} — use ${from} or ${to}`);
582
+ }
583
+ const prefix = historyPrefix(view, at);
584
+ return foldHistory(entries.slice(0, at), prefix.layout, service);
585
+ }
586
+ // a cold read (no memo) trusts a checkpoint cut at exactly these log facts without re-hashing the logs
587
+ const facts = opts.until === undefined ? `${parentKeyOf(service, root)}#${statKey(branchLogPath(service, root))}` : null;
588
+ if (facts !== null) {
589
+ const held = getActiveWorldStore().read(checkpointPath(service, root));
590
+ const cut = held === null ? null : JSON.parse(held) as Checkpoint;
591
+ if (cut?.facts === facts) return new Map(cut.subjects.map(s => [`${s.type}:${s.id}`, { ...s, fields: { ...s.fields } }]));
592
+ }
593
+ const inherited = inheritedHistory(service, root);
594
+ const base = inherited ? historyEntries(inherited) : [];
595
+ const own = ownParentEntries(service, root);
596
+ const parent = [...base, ...own];
597
+ const branchAll = branchEntries(service, root);
598
+ const until = opts.until === undefined ? -1 : branchAll.findIndex(e => e.id === opts.until);
599
+ const branch = until < 0 ? branchAll : branchAll.slice(0, until);
600
+ const parentDigest = hashFieldValue({ inherited, own }); const branchDigest = hashFieldValue(branch);
601
+ const raw = opts.until === undefined ? getActiveWorldStore().read(checkpointPath(service, root)) : null;
602
+ const cp = raw === null ? null : JSON.parse(raw) as Checkpoint;
603
+ if (cp?.parentDigest === parentDigest && cp.branchDigest === branchDigest) return new Map(cp.subjects.map(s => [`${s.type}:${s.id}`, { ...s, fields: { ...s.fields } }]));
604
+ const layout: HistoryLayout = { ...(inherited ? { base: inherited.layout } : {}), parent: own.length, branch: branch.length };
605
+ const incremental = cp?.parentDigest === parentDigest && cp.branchCount <= branch.length && hashFieldValue(branch.slice(0, cp.branchCount)) === cp.branchDigest && !branch.slice(cp.branchCount).some(e => e.op === 'revert');
606
+ const tree: Tree = incremental ? new Map(cp.subjects.map(s => [`${s.type}:${s.id}`, { ...s, fields: { ...s.fields } }])) : foldHistory([...parent, ...branch], layout, service);
607
+ if (incremental) { foldEntries(tree, branch.slice(cp.branchCount), landedIds(parent)); foldAliases(tree, aliasesFrom(parent), service); }
608
+ if (opts.until === undefined && parent.length + branch.length > CHECKPOINT_EVERY) {
609
+ // the facts are the ones this fold read; recorded only if no write moved them while it folded
610
+ const now = `${parentKeyOf(service, root)}#${statKey(branchLogPath(service, root))}`;
611
+ getActiveWorldStore().writeAtomic(checkpointPath(service, root), JSON.stringify({ parentDigest, branchDigest, branchCount: branch.length, subjects: [...tree.values()], ...(now === facts ? { facts } : {}) }));
612
+ }
613
+ return tree;
614
+ });
615
+ }
616
+ export function cutCheckpoint(service: string, root?: string): void {
617
+ return withAncestryLock(() => {
618
+ dropCheckpoint(service, root);
619
+ const subjects = [...readTreeMap(service, root).values()];
620
+ const inherited = inheritedHistory(service, root); const own = ownParentEntries(service, root);
621
+ getActiveWorldStore().writeAtomic(checkpointPath(service, root), JSON.stringify({ parentDigest: hashFieldValue({ inherited, own }), branchDigest: hashFieldValue(branchEntries(service, root)), branchCount: branchEntries(service, root).length, subjects }));
622
+ });
623
+ }
624
+
625
+ /** The branch's entries the parent does not hold: `set` rows, not reverted, not landed. */
626
+ export function unpushedEntries(service: string, root?: string): Entry[] {
627
+ const landed = landedIds(parentEntries(service, root));
628
+ const branch = branchEntries(service, root);
629
+ const reverted = new Set(branch.filter((e) => e.op === 'revert' && e.revertsActionId).map((e) => e.revertsActionId!));
630
+ return branch.filter((e) => e.op === 'set' && !reverted.has(e.id) && !landed.has(e.id));
631
+ }
632
+
633
+ /** The position a branch is cut at: how far the parent view and the branch log reach right now. */
634
+ /** THE POSITION (contract "Just like Neon", 2): where this twin's whole log stands — one number, the
635
+ * count of entries, parent view then branch. Durable cuts pair it with an immutable view ID. */
636
+ export function position(service: string, root?: string): number {
637
+ return parentEntries(service, root).length + branchEntries(service, root).length;
638
+ }
639
+
640
+ /** The position of a twin's log at an instant: the entries that had occurred by then. */
641
+ export function positionAt(service: string, instant: string, root?: string, view?: string): number {
642
+ const t = Date.parse(instant);
643
+ const log = view ? historyEntries(readHistoryView(worldPaths(service, root).dir, view)) : wholeLog(service, root);
644
+ let n = 0;
645
+ for (const e of log) { if (historyEntryTime(e) <= t) n += 1; else break; }
646
+ if (log.slice(n).some(e => historyEntryTime(e) <= t)) throw new Error('This instant selects noncontiguous history; use historyAtInstant to capture a view instead of a numeric offset');
647
+ // an observation is atomic: an instant inside one look resolves to the look's end
648
+ while (n > 0 && n < log.length && log[n]!.batch !== undefined && log[n]!.batch === log[n - 1]!.batch) n += 1;
649
+ return n;
650
+ }
651
+
652
+ /** The batch a position would split, if any: `position` entries taken, and the next entry belongs to the
653
+ * same observation as the last one taken. Answers the boundaries a caller may use instead. */
654
+ export function splitsBatch(service: string, position: number, root?: string): { batch: string; from: number; to: number } | null {
655
+ const log = wholeLog(service, root);
656
+ if (position <= 0 || position >= log.length) return null;
657
+ const batch = log[position]!.batch;
658
+ if (batch === undefined || log[position - 1]!.batch !== batch) return null;
659
+ let from = position - 1; while (from > 0 && log[from - 1]!.batch === batch) from -= 1;
660
+ let to = position; while (to < log.length && log[to]!.batch === batch) to += 1;
661
+ return { batch, from, to };
662
+ }
663
+ /** Refuse a position inside an observation, naming the boundaries. */
664
+ export function assertBatchBoundary(service: string, position: number, root?: string): void {
665
+ const split = splitsBatch(service, position, root);
666
+ if (split) throw new Error(`position ${position} falls inside observation ${split.batch} (entries ${split.from + 1}–${split.to}); an observation is atomic — use ${split.from} or ${split.to}`);
667
+ }
668
+
669
+ /** A landed copy of `entry` for the parent log: the receipt on it, the vendor's id as its subject
670
+ * when one was minted. Deterministic id, so landing twice is the same row. */
671
+ export function landedCopy(entry: Entry, receipt: Receipt, opts: { vendorSubjectId?: string; fields?: SubjectFields; subject?: { type: string; id: string } } = {}): Entry {
672
+ const subject = opts.subject ?? entry.subject;
673
+ const adopted = opts.vendorSubjectId !== undefined && opts.vendorSubjectId !== '' && opts.vendorSubjectId !== subject.id ? { type: subject.type, id: opts.vendorSubjectId } : subject;
674
+ const fields = opts.fields ?? entry.fields ?? {};
675
+ const contentHash = hashFieldValue({ landsId: entry.id, subject: adopted, fields, status: receipt.status });
676
+ const { event: _event, receipt: _receipt, aliasOf: _aliasOf, landsId: _landsId, ...body } = entry;
677
+ return {
678
+ ...body,
679
+ id: `landed:${entry.service}:${adopted.type}:${adopted.id}:${entry.id}:${contentHash}`,
680
+ op: 'set', subject: adopted, fields, landsId: entry.id,
681
+ ...(adopted.id !== subject.id ? { aliasOf: subject.id } : {}),
682
+ receipt,
683
+ };
684
+ }
685
+
686
+ /** Append an entry to this root's own parent log — what fetch does with what origin sends, and what
687
+ * a served world does with nothing (its pushes land on its branch log). Deduplicated by id. */
688
+ // The parent log's entry ids, per store and path, valid while the file's size and version are the ones
689
+ // recorded (WorldStore.stat changes on any writer's append): a batch of appends dedupes against an index,
690
+ // not by re-parsing the whole log each time.
691
+ const parentIdIndex = new WeakMap<object, Map<string, { key: string; ids: Set<string> }>>();
692
+ export function appendParentEntry(entry: Entry, root?: string): { appended: boolean } {
693
+ const path = parentLogPath(entry.service, root);
694
+ const store = getActiveWorldStore();
695
+ store.mkdir(dirname(path));
696
+ return withAncestryLock(() => store.withLock(`${path}.lock`, () => {
697
+ let indexes = parentIdIndex.get(store);
698
+ if (!indexes) { indexes = new Map(); parentIdIndex.set(store, indexes); }
699
+ let index = indexes.get(path);
700
+ if (!index || index.key !== statKey(path)) {
701
+ index = { key: statKey(path), ids: new Set(readRows<{ id?: string }>(path).flatMap((r) => (typeof r.id === 'string' ? [r.id] : []))) };
702
+ indexes.set(path, index);
703
+ }
704
+ if (index.ids.has(entry.id)) return { appended: false };
705
+ store.append(path, `${JSON.stringify(entry)}\n`);
706
+ index.ids.add(entry.id);
707
+ index.key = statKey(path);
708
+ return { appended: true };
709
+ }));
710
+ }
711
+
712
+ /** The whole log of a twin as a served world hands it out: the parent view, then the branch's own
713
+ * entries, one sequence with a position. A clone's parent is exactly this. */
714
+ export function wholeLog(service: string, root?: string): Entry[] {
715
+ return [...parentEntries(service, root), ...branchEntries(service, root)];
716
+ }
717
+
718
+ /**
719
+ * REBASE a branch onto its base's current position (contract: "rebase replays the branch's
720
+ * entries over a newer base, conflicts per subject and field"). A branch is a pointer, so the
721
+ * base moving is invisible until this: the pointer advances to the base's present position, the
722
+ * branch's own entries stay in place and now fold over the moved base, and every entry whose
723
+ * preconditions no longer hold against the new parent tree is named as a conflict — data on the
724
+ * result, never a stop. An unbranched world (no pointer) has nothing to rebase.
725
+ */
726
+ export function rebaseBranch(service: string, root?: string, opts: { origin?: boolean } = {}): { moved: boolean; from: number; to: number; conflicts: Array<{ entryId: string; subject: { type: string; id: string }; field: string; op: string; expected?: unknown; actual?: unknown }> } {
727
+ return withAncestryLock(() => {
728
+ const meta = readBranchMeta(service, root);
729
+ if (!meta?.parent) return { moved: false, from: 0, to: 0, conflicts: [] };
730
+ if (opts.origin && !meta.origin) throw new Error('This local fork has no tracked origin; clone the origin before branching');
731
+ const from = meta.parent.position;
732
+ let tracked = meta.origin && (opts.origin || meta.origin.depth === 0) ? meta.fetchedOrigin ?? { ...meta.origin, depth: 0 } : undefined;
733
+ if (tracked && !opts.origin && !isUrlParent(tracked.at)) {
734
+ const live = captureHistory(service, tracked.at);
735
+ tracked = { ...tracked, directory: live.descriptor.owner.directory, generation: live.descriptor.owner.generation, view: live.view, position: live.position };
736
+ }
737
+ if (tracked && !meta.origin!.incomplete && tracked.directory === meta.origin!.directory && tracked.view === meta.origin!.view && tracked.position === meta.origin!.position && tracked.remoteView === meta.origin!.remoteView) {
738
+ if (meta.fetchedOrigin) writeBranchMeta(service, { ...meta, fetchedOrigin: undefined }, root);
739
+ return { moved: false, from, to: from, conflicts: [] };
740
+ }
741
+ const head = tracked ?? (isUrlParent(meta.parent.at) ? originHead(service, root) : captureHistory(service, meta.parent.at));
742
+ if (!head) throw new Error('Origin has no completed immutable history view');
743
+ const directory = isUrlParent(meta.parent.at) ? worldPaths(service, root).dir : worldPaths(service, meta.parent.at).dir;
744
+ const previous = inheritedHistory(service, root)!;
745
+ const next = tracked ? replaceHistoryOrigin(previous, meta.origin!, tracked) : readHistoryView(directory, head.view);
746
+ const nextView = tracked ? saveHistoryView(worldPaths(service, root).dir, next) : head.view;
747
+ const to = historyLength(next.layout);
748
+ const moved = to !== from || nextView !== meta.parent.view;
749
+ // Compare the evaluated old and new views, including removals and reordered layers.
750
+ const before = historyEntries(previous);
751
+ const since = historyChanges(service, previous, next);
752
+ // only a subject that EXISTED at this branch's position can conflict: two branches each minting the
753
+ // same local id for different records is not a disagreement (the vendor mints the real id)
754
+ const existed = new Set(before.filter((e) => e.op === 'set').flatMap((e) => [`${e.subject.type}:${e.subject.id}`, ...(e.aliasOf ? [`${e.subject.type}:${e.aliasOf}`] : [])]));
755
+ // conflicts: a field this branch set that the parent set since, to something else — named by
756
+ // entry, subject and field (contract "Drift and rebase in v2")
757
+ const stable = (v: unknown): string => JSON.stringify(v) ?? 'undefined';
758
+ // a field both sides set to an INSTANT (updated_at, created) is when each wrote, not what: never a conflict
759
+ const instant = (v: unknown): boolean => typeof v === 'string' && /^\d{4}-\d\d-\d\dT\d\d:\d\d/.test(v) && Number.isFinite(Date.parse(v));
760
+ const currentAliases = aliasesFrom(historyEntries(next));
761
+ const conflicts: Array<{ entryId: string; subject: { type: string; id: string }; field: string; op: string; expected?: unknown; actual?: unknown }> = [];
762
+ for (const e of branchEntries(service, root)) {
763
+ if (e.op !== 'set' || !e.fields || !existed.has(`${e.subject.type}:${e.subject.id}`)) continue;
764
+ const fields = resolveReferences(service, e.subject.type, e.fields, currentAliases);
765
+ for (const p of since) {
766
+ if (p.op !== 'set' || !p.fields || p.subject.type !== e.subject.type || (p.subject.id !== e.subject.id && p.aliasOf !== e.subject.id)) continue;
767
+ if (p.fields.deleted === true && fields.deleted !== true) conflicts.push({ entryId: e.id, subject: e.subject, field: 'deleted', op: 'exists', expected: true, actual: false });
768
+ for (const [field, value] of Object.entries(p.fields)) if (field in fields && stable(fields[field]) !== stable(value) && !(instant(value) && instant(fields[field]))) conflicts.push({ entryId: e.id, subject: e.subject, field, op: 'set', expected: fields[field], actual: value });
769
+ }
770
+ }
771
+ if (moved) {
772
+ const { viewDirectory: _oldViewDirectory, ...parent } = meta.parent;
773
+ writeBranchMeta(service, { ...meta, fetchedOrigin: undefined, origin: tracked ? { ...tracked, depth: meta.origin!.depth } : next.origin, parent: { ...parent, position: to, view: nextView, ...(tracked ? { viewDirectory: canonicalStatePath(worldPaths(service, root).dir) } : {}), ...('remoteView' in head ? { remoteView: head.remoteView } : {}) } }, root);
774
+ dropCheckpoint(service, root);
775
+ }
776
+ return { moved, from, to, conflicts };
777
+ });
778
+ }
779
+
780
+ /**
781
+ * HISTORY through the kernel: every entry that touched a subject (or every subject of a type), parent
782
+ * log then branch log, in order, with reverted entries dropped and landed copies shown once. A pack
783
+ * that serves a changelog (Jira's), or must never reuse an id it once minted, reads this — never a
784
+ * log file. Each entry's `fields` is what it set; the fold is the reader's.
785
+ */
786
+ export function subjectHistory(service: string, subject: { type: string; id?: string }, root?: string): Entry[] {
787
+ const parent = parentEntries(service, root);
788
+ const held = landedIds(parent);
789
+ const branch = branchEntries(service, root);
790
+ const reverted = new Set(branch.filter((e) => e.op === 'revert' && e.revertsActionId).map((e) => e.revertsActionId!));
791
+ const touches = (e: Entry): boolean => e.op === 'set' && e.subject.type === subject.type && (subject.id === undefined || e.subject.id === subject.id || e.aliasOf === subject.id);
792
+ return [...parent.filter((e) => touches(e) && e.fields !== undefined), ...branch.filter((e) => touches(e) && !reverted.has(e.id) && !held.has(e.id))];
793
+ }