@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/actions.ts ADDED
@@ -0,0 +1,564 @@
1
+ // Transaction/action log (scorecard R18) — the semantic correction that separates
2
+ // OBSERVED facts from LOCAL transaction commits.
3
+ //
4
+ // The observed-event log (`events.jsonl`) holds only what was observed upstream
5
+ // (connector pulls) or confirmed after a push. Local simulator/fork writes do NOT
6
+ // go there — they are transaction commits in `actions.jsonl`, projected OVER the
7
+ // observed mirror to produce the twin's current state. Undo is a `revert` commit;
8
+ // a push that succeeds appends a `confirm` commit mapping the local transaction
9
+ // to the observed event it produced, which SUPPRESSES the local projection (the
10
+ // fact is now carried by the observed log, so it must not be double-counted).
11
+ //
12
+ // Projection = observed mirror, then apply each `set` transaction in order,
13
+ // skipping any transaction that was reverted or confirmed.
14
+ import { withAncestryLock } from './ancestry.ts';
15
+ import { AsyncLocalStorage } from 'node:async_hooks';
16
+ import { randomUUID } from 'node:crypto';
17
+ import { dirname, join } from 'node:path';
18
+ import { canonicalJson, hashFieldValue, subjectKey } from './hash.ts';
19
+ import type { SubjectFields } from './hash.ts';
20
+ import { appendDurable, eventsLockPath, projectionLockPath, twinLog, withFileLock, worldPaths } from './storage.ts';
21
+ import { landAsPlaceholder, placeholderPullActive } from './placeholder-remote.ts';
22
+ import { aliasesFrom, dropCheckpoint, landedCopy, landedIds, parentEntries, readTree, type Receipt } from './log.ts';
23
+ import { WorldServiceEventSchema } from './schemas.ts';
24
+ import { getActiveWorldStore } from './world-store.ts';
25
+ import type { WorldServiceEvent } from './types.ts';
26
+ import type { TwinResource } from './serve.ts';
27
+
28
+ export type TwinActionOp = 'set' | 'revert';
29
+ export type TwinActionPreconditionOp = 'exists' | 'not_exists' | 'eq' | 'neq' | 'version_eq';
30
+ export type TwinActionPrecondition = {
31
+ subject: { type: string; id: string };
32
+ field: string;
33
+ op: TwinActionPreconditionOp;
34
+ value?: unknown;
35
+ };
36
+ export type TwinActionRevertSpec = {
37
+ strategy: 'inverse' | 'suppress' | 'compensating-action';
38
+ operation?: string;
39
+ fields?: SubjectFields;
40
+ };
41
+
42
+ // Multi-resource transaction projection (the twins architecture notes → "Local
43
+ // Transaction Commit Packet"). The flat single-subject `fields` overlay is the
44
+ // common shorthand; `projection` is the richer form for a transaction that
45
+ // atomically touches MORE THAN ONE resource (e.g. create a PR + a review), deletes
46
+ // a resource, or emits a delivery (webhook/notification) the twin should fire.
47
+ export type ProjectedResource = { type: string; id: string; fields: SubjectFields };
48
+ export type ProjectedResourcePatch = { type: string; id: string; fields: SubjectFields };
49
+ export type ProjectedResourceRef = { type: string; id: string };
50
+ export type ProjectedDelivery = { kind: 'webhook' | 'event' | 'notification'; target: string; payload: Record<string, unknown> };
51
+ export type ActionProjection = {
52
+ creates?: ProjectedResource[];
53
+ updates?: ProjectedResourcePatch[];
54
+ deletes?: ProjectedResourceRef[];
55
+ emits?: ProjectedDelivery[];
56
+ };
57
+
58
+ export type TwinAction = {
59
+ id: string;
60
+ service: string;
61
+ op: TwinActionOp;
62
+ subject: { type: string; id: string };
63
+ occurredAt: string;
64
+ actor?: { kind: 'agent' | 'human' | 'bot' | 'system'; id?: string };
65
+ /** Optional vendor operation name, e.g. `issue.update` or `message.send`. */
66
+ operation?: string;
67
+ /** The caller's own request key, when it asked for at-most-once. Provenance, AND the marker that
68
+ * makes replay identity ignore `occurredAt`: the whole point of a request key is that a retry
69
+ * arriving at a different millisecond is the SAME request, so comparing the two bodies must not
70
+ * fail on the timestamp. Without this, a caller that did not pin `occurredAt` got a thrown
71
+ * "Conflicting duplicate twin action" — a 500 where it had asked for idempotence. */
72
+ idempotencyKey?: string;
73
+ /** Raw operation input (provenance); not projected — `fields`/`projection` carry the state change. */
74
+ input?: Record<string, unknown>;
75
+ /** Preconditions are evaluated against the current projected twin state before append. */
76
+ preconditions?: TwinActionPrecondition[];
77
+ // op 'set': the local field changes to overlay on `subject` (single-resource shorthand).
78
+ fields?: SubjectFields;
79
+ // op 'set': the richer multi-resource transaction projection (optional; composes with `fields`).
80
+ projection?: ActionProjection;
81
+ // op 'revert': the prior action id being undone.
82
+ revertsActionId?: string;
83
+ /** A pending row a snapshot import planted (contract "A snapshot import never plants a push"):
84
+ * still local state the projection serves, never awaiting push until released on purpose. */
85
+ quarantined?: { at: string; by: string };
86
+ /** Optional machine-readable hint for how a UI/pack should construct a revert. */
87
+ revert?: TwinActionRevertSpec;
88
+ /**
89
+ * Request-scoped correlation id (D3 — the purpose-3 audit trail: "who reviewed the
90
+ * change that caused this real write"). Always present on an appended action —
91
+ * appendAction/appendActionIfAbsent generate one when the caller doesn't supply it —
92
+ * and threaded through to the push-ledger row(s) a push against this action produces
93
+ * (see pushLedger.ts), so an action row and its push-ledger row(s) join on this id
94
+ * alone, with no dependence on actionId/pushId naming conventions.
95
+ */
96
+ correlationId?: string;
97
+ /**
98
+ * The action's MERGE BASE against the remote (runtime contract R14, non-fast-forward
99
+ * rule): per touched subject, the observed mirror's remote ref (shadow.ts remoteRefs)
100
+ * at authoring time — null when the mirror had never seen the subject. Stamped by the
101
+ * appenders on every `set` action; pushTransaction compares these refs again at push
102
+ * time and REFUSES when any has moved (someone else changed the remote), demanding
103
+ * fetch + reconcile. Authoring metadata like correlationId: excluded from replay
104
+ * identity, so an identical retry converges on the first commit's (older, safer) basis.
105
+ */
106
+ };
107
+ export type TwinTransactionCommit = TwinAction;
108
+ export type TwinTransactionCommitOp = TwinActionOp;
109
+ export type TwinTransactionPrecondition = TwinActionPrecondition;
110
+ export type TwinTransactionRevertSpec = TwinActionRevertSpec;
111
+ export class TwinActionPreconditionError extends Error {
112
+ constructor(readonly actionId: string, readonly failed: TwinActionPrecondition) {
113
+ super(`Twin transaction precondition failed for ${actionId}: ${failed.subject.type}:${failed.subject.id}.${failed.field} ${failed.op}`);
114
+ this.name = 'TwinActionPreconditionError';
115
+ }
116
+ }
117
+
118
+ function actionsPath(service: string, root?: string): string {
119
+ return join(dirname(worldPaths(service, root).events), 'actions.jsonl');
120
+ }
121
+ const actionsLock = (service: string, root?: string): string => `${actionsPath(service, root)}.lock`;
122
+ const projectionLock = (service: string, root?: string): string => projectionLockPath(worldPaths(service, root));
123
+
124
+ /** Every appended action carries a correlationId — generate one when the caller
125
+ * hasn't supplied it, so downstream joins (push ledger, logs) always have an id
126
+ * to key on (D3). Preserves a caller-supplied id (e.g. propagated from an HTTP
127
+ * request id) so a whole call chain can share one. */
128
+ function withCorrelationId(action: TwinAction): TwinAction {
129
+ // The wire's request id (the addressed service stamps every response with one and threads it
130
+ // into the handler's async context) is the correlation when the caller supplies none — the
131
+ // join from a request on the wire to the action rows it caused, with no pack involved.
132
+ return action.correlationId ? action : { ...action, correlationId: currentCorrelationId() ?? randomUUID() };
133
+ }
134
+
135
+ /** Subjects a `set` action touches: its own subject, every projection resource, and every
136
+ * precondition subject — a precondition established the action's validity against that
137
+ * subject's state, so remote movement there is drift for this action too. */
138
+ export function touchedSubjects(action: TwinAction): Array<{ type: string; id: string }> {
139
+ const out = new Map<string, { type: string; id: string }>();
140
+ out.set(subjectKey(action.subject), action.subject);
141
+ for (const r of [
142
+ ...(action.projection?.creates ?? []),
143
+ ...(action.projection?.updates ?? []),
144
+ ...(action.projection?.deletes ?? []),
145
+ ]) out.set(subjectKey(r), { type: r.type, id: r.id });
146
+ for (const p of action.preconditions ?? []) out.set(subjectKey(p.subject), p.subject);
147
+ return [...out.values()];
148
+ }
149
+
150
+ // THE LOG'S IDS, MEMOIZED per store and per log file, as a read's tree is (log.ts): named by the file's size and
151
+ // mtime. An append's ordinal asks only which ids are taken, and answering it by re-reading and re-parsing the whole log
152
+ // made every append O(log) and a seed of N records O(N^2) (a 7,479-reading Timestream seed stalled its twin for
153
+ // minutes). The appender, holding the lock, adds its own id and re-keys; any other writer's append changes the key and
154
+ // the next caller reads afresh.
155
+ type IdMemo = { key: string; ids: Set<string> };
156
+ const idMemos = new WeakMap<object, Map<string, IdMemo>>();
157
+ function logKey(path: string): string { const s = getActiveWorldStore().stat(path); return s ? `${s.size}:${s.mtimeMs}` : '-'; }
158
+ function idMemoSlot(path: string): Map<string, IdMemo> {
159
+ const store = getActiveWorldStore();
160
+ let memos = idMemos.get(store);
161
+ if (!memos) { memos = new Map(); idMemos.set(store, memos); }
162
+ return memos;
163
+ }
164
+ function actionIds(service: string, root?: string): ReadonlySet<string> {
165
+ const path = actionsPath(service, root);
166
+ const memos = idMemoSlot(path);
167
+ const key = logKey(path);
168
+ const held = memos.get(path);
169
+ if (held && held.key === key) return held.ids;
170
+ const ids = new Set(listActions(service, root).map((a) => a.id));
171
+ memos.set(path, { key, ids });
172
+ return ids;
173
+ }
174
+
175
+ function appendActionRaw(action: TwinAction, root?: string): void {
176
+ const path = actionsPath(action.service, root);
177
+ getActiveWorldStore().mkdir(dirname(path));
178
+ const memos = idMemoSlot(path);
179
+ const held = memos.get(path);
180
+ const current = held !== undefined && held.key === logKey(path);
181
+ withAncestryLock(() => appendDurable(path, `${JSON.stringify(action)}\n`));
182
+ if (current) { held.ids.add(action.id); held.key = logKey(path); } else memos.delete(path);
183
+ twinLog('action.append', { service: action.service, id: action.id, op: action.op, correlationId: action.correlationId });
184
+ appendObserver?.(action, root);
185
+ }
186
+
187
+ /** ONE observer of appended entries (state-system.ts: the runtime's `deploy: auto` hook). Called
188
+ * under the append locks, so it must only schedule work, never do it. */
189
+ let appendObserver: ((action: TwinAction, root: string | undefined) => void) | undefined;
190
+ export function observeAppends(observer: ((action: TwinAction, root: string | undefined) => void) | undefined): void { appendObserver = observer; }
191
+
192
+ /** The placeholder remote (contract section of that name): while a service's placeholder-pull
193
+ * window is open, a `set` write is a pull, not a commit — it lands as observed events and no
194
+ * action row exists. Returns the action the caller hands back (its id names what landed). */
195
+ function landIfPlaceholderPull(action: TwinAction, root?: string): TwinAction | null {
196
+ if (action.op !== 'set' || !placeholderPullActive(action.service, root)) return null;
197
+ const { events } = landAsPlaceholder(action, root);
198
+ twinLog('action.placeholder', { service: action.service, id: action.id, op: action.op, events: events.length });
199
+ return { ...action, id: events.at(-1)?.id ?? action.id };
200
+ }
201
+
202
+ function replayBody(action: TwinAction): string {
203
+ const { correlationId: _correlationId, ...stampedBody } = action;
204
+ // A confirmation is content-addressed by the observed event ids. Its timestamp is observation
205
+ // metadata, just like the occurredAt/observedAt values that appendEvent ignores when replaying
206
+ // one content-addressed event. Two reconcilers confirming the same bytes at different wall-clock
207
+ // instants must therefore converge on the first durable confirmation rather than conflict.
208
+ // A KEYED write is the same request however long the retry took, so its timestamp is observation
209
+ // metadata too — exactly as a confirmation's is. Comparing it would turn the retry the key was
210
+ // asked for into a hard conflict.
211
+ const body = stampedBody.idempotencyKey !== undefined
212
+ ? (({ occurredAt: _occurredAt, ...rest }) => rest)(stampedBody)
213
+ : stampedBody;
214
+ return canonicalJson(body);
215
+ }
216
+
217
+ function existingExactAction(action: TwinAction, root?: string): TwinAction | undefined {
218
+ const existing = listActions(action.service, root).find((candidate) => candidate.id === action.id);
219
+ if (!existing) return undefined;
220
+ if (replayBody(existing) !== replayBody(action)) {
221
+ throw new Error(`Conflicting duplicate twin action: ${action.service}/${action.id}`);
222
+ }
223
+ return existing;
224
+ }
225
+
226
+ /** The request-scoped correlation id (D3): set by the kernel fetch adapter for the duration of one
227
+ * handler call from the wire's `x-twins-request-id`; read by appendAction as the default. */
228
+ // created on first use, never at import: a browser bundle of a mirror client carries this module and has
229
+ // no AsyncLocalStorage (see serve.ts identitySlot)
230
+ let correlationStore: AsyncLocalStorage<string> | undefined;
231
+ const correlationScope = (): AsyncLocalStorage<string> => (correlationStore ??= new AsyncLocalStorage<string>());
232
+ export function runWithCorrelationId<T>(id: string, fn: () => T): T { return correlationScope().run(id, fn); }
233
+ export function currentCorrelationId(): string | undefined { return correlationScope().getStore(); }
234
+
235
+ export function appendAction(action: TwinAction, root?: string): TwinAction {
236
+ // Evaluate and append under the service projection and actions locks. A precondition is a
237
+ // compare-and-set, not an advisory validation: checking outside either lock would let another
238
+ // writer invalidate it before this action lands. The shadow basis is stamped under the same
239
+ // lock so the recorded merge base is the mirror the preconditions were checked against.
240
+ return withFileLock(projectionLock(action.service, root), () => withFileLock(actionsLock(action.service, root), () => {
241
+ const stamped = withCorrelationId(action);
242
+ assertPreconditions(stamped, root);
243
+ const placeholder = landIfPlaceholderPull(stamped, root);
244
+ if (placeholder) return placeholder;
245
+ appendActionRaw(stamped, root);
246
+ return stamped;
247
+ }));
248
+ }
249
+
250
+ /** Append `action` only if no exact action with the same id already exists — the whole
251
+ * replay/precondition/append decision runs under the service projection + actions locks, so it's
252
+ * atomic across processes (two concurrent identical writes converge to ONE action; distinct
253
+ * writes both land). A reused id with different content fails loudly. */
254
+ export function appendActionIfAbsent(action: TwinAction, root?: string): { action: TwinAction; appended: boolean; placeholder?: true } {
255
+ return withFileLock(projectionLock(action.service, root), () => withFileLock(actionsLock(action.service, root), () => {
256
+ const identified = withCorrelationId(action);
257
+ // An exact retry is already committed. Resolve it before re-evaluating author-time
258
+ // preconditions against the state that first commit intentionally changed — and
259
+ // before paying the basis stamp's event-log read (the replayed path stays cheap).
260
+ const existing = existingExactAction(identified, root);
261
+ if (existing) return { action: existing, appended: false };
262
+ const stamped = identified;
263
+ assertPreconditions(stamped, root);
264
+ const placeholder = landIfPlaceholderPull(stamped, root);
265
+ if (placeholder) return { action: placeholder, appended: true, placeholder: true };
266
+ appendActionRaw(stamped, root);
267
+ return { action: stamped, appended: true };
268
+ }));
269
+ }
270
+
271
+ /**
272
+ * Append `build(n)` as an OCCURRENCE: the nth time this exact write has been made.
273
+ *
274
+ * A local vendor write is an occurrence, not a replay — two calls are two actions, even
275
+ * byte-identical in the same instant (docs/contributing/adding-a-twin.md#5-build-on-the-shared-kernel--dont-reinvent,
276
+ * "A local write is an occurrence"). `appendActionIfAbsent` cannot express that: its identity is content, and
277
+ * content provably cannot separate "the same request delivered twice" from "the same change made
278
+ * twice". So a caller wanting at-most-once passes an explicit key and uses that function; a caller
279
+ * recording what actually happened uses this one.
280
+ *
281
+ * The ordinal keeps every EXISTING action id byte-identical: occurrence 0 is `build(0)`'s own id,
282
+ * and only a genuine repeat becomes `<id>#1`, `#2`. It is derived under the same projection +
283
+ * actions locks as the append, so two processes racing take different ordinals rather than
284
+ * colliding, and it is deterministic — replaying one sequence of writes onto a fresh root yields
285
+ * the same ordinals, which is what serve-path determinism requires.
286
+ */
287
+ export function appendActionOccurrence(base: TwinAction, root?: string): { action: TwinAction; appended: boolean; placeholder?: true } {
288
+ return withFileLock(projectionLock(base.service, root), () => withFileLock(actionsLock(base.service, root), () => {
289
+ const stamped = occurrenceOf(base, actionIds(base.service, root), root);
290
+ assertPreconditions(stamped, root);
291
+ const placeholder = landIfPlaceholderPull(stamped, root);
292
+ if (placeholder) return { action: placeholder, appended: true, placeholder: true };
293
+ appendActionRaw(stamped, root);
294
+ return { action: stamped, appended: true };
295
+ }));
296
+ }
297
+
298
+ /** The occurrence rule, shared by both appenders that use it — the ordinal AND the merge base.
299
+ * Pure over (base, the log as it stands): the caller holds the lock. */
300
+ function occurrenceOf(base: TwinAction, taken: ReadonlySet<string>, root?: string): TwinAction {
301
+ {
302
+ let ordinal = 0;
303
+ while (taken.has(occurrenceId(base.id, ordinal))) ordinal += 1;
304
+ // an occurrence past the first is its own entry: the tree it changes is read at the head, and a
305
+ // push onto a moved parent is refused by position, not by a basis stamped here (v2)
306
+ return withCorrelationId({ ...base, id: occurrenceId(base.id, ordinal) });
307
+ }
308
+ }
309
+
310
+ /** Occurrence 0 IS the base id — so nothing that exists today changes shape. The appender owns
311
+ * this math; a caller passing an already-ordinalled id would stack them (`…#1#1`). */
312
+ export function occurrenceId(base: string, ordinal: number): string {
313
+ return ordinal === 0 ? base : `${base}#${ordinal}`;
314
+ }
315
+
316
+ export type AtomicActionDecision<T> =
317
+ | { kind: 'skip'; value: T }
318
+ | {
319
+ kind: 'append';
320
+ action: TwinAction;
321
+ value: T;
322
+ /** `'occurrence'` (default) appends every call, with the ordinal and the first occurrence's
323
+ * merge base. `'caller'` is at-most-once on the caller's own identity — what an
324
+ * `idempotencyKey` or `actionId` means. It rides on the DECISION rather than on the
325
+ * function's arguments because only the callback knows which write it chose. */
326
+ identity?: 'occurrence' | 'caller';
327
+ };
328
+
329
+ /**
330
+ * Evaluate current projected state and optionally append one action under ONE service projection
331
+ * transaction (the shared projection lock plus the action-log lock). This is the narrow
332
+ * state-dependent seam for vendor operations whose acceptance and resulting fields depend on the
333
+ * latest observed + local projection (for example, immutable version publish).
334
+ *
335
+ * The callback must remain synchronous and side-effect free: it computes a decision from the
336
+ * supplied snapshot. Durable state changes only through the returned action, which this helper
337
+ * appends before releasing the lock.
338
+ */
339
+ export function decideAndAppendAction<T>(
340
+ service: string,
341
+ decide: (resources: TwinResource[]) => AtomicActionDecision<T>,
342
+ root?: string,
343
+ ): { value: T; action?: TwinAction; appended: boolean; placeholder?: true } {
344
+ return withFileLock(projectionLock(service, root), () => withFileLock(actionsLock(service, root), () => {
345
+ const resources = projectResources(service, root);
346
+ const decision = decide(resources);
347
+ if (decision.kind === 'skip') return { value: decision.value, appended: false };
348
+ if (decision.action.service !== service) {
349
+ throw new Error(`Atomic action service mismatch: expected ${service}, got ${decision.action.service}`);
350
+ }
351
+ const identified = withCorrelationId(decision.action);
352
+ // This seam was left on content-dedupe when the occurrence ruling first landed, and 21 packs
353
+ // write through it — so they kept losing a write that returned a subject to a value it held
354
+ // one instant earlier, which is the whole defect.
355
+ if (decision.identity === 'caller') {
356
+ const existing = existingExactAction(identified, root);
357
+ if (existing) return { value: decision.value, action: existing, appended: false };
358
+ const stamped = identified;
359
+ assertPreconditionsAgainst(stamped, resources);
360
+ const placeholder = landIfPlaceholderPull(stamped, root);
361
+ if (placeholder) return { value: decision.value, action: placeholder, appended: true, placeholder: true };
362
+ appendActionRaw(stamped, root);
363
+ return { value: decision.value, action: stamped, appended: true };
364
+ }
365
+ const stamped = occurrenceOf(decision.action, actionIds(service, root), root);
366
+ assertPreconditionsAgainst(stamped, resources);
367
+ const placeholder = landIfPlaceholderPull(stamped, root);
368
+ if (placeholder) return { value: decision.value, action: placeholder, appended: true, placeholder: true };
369
+ appendActionRaw(stamped, root);
370
+ return { value: decision.value, action: stamped, appended: true };
371
+ }));
372
+ }
373
+
374
+ export function appendTransactionCommit(commit: TwinTransactionCommit, root?: string): TwinTransactionCommit {
375
+ return appendAction(commit, root);
376
+ }
377
+
378
+ export function listActions(service: string, root?: string): TwinAction[] {
379
+ const path = actionsPath(service, root);
380
+ return getActiveWorldStore()
381
+ .readLines(path)
382
+ .filter((line) => line.trim())
383
+ .map((line) => JSON.parse(line) as TwinAction);
384
+ }
385
+
386
+ const META = new Set(['id', 'type', 'updatedAt']);
387
+
388
+ function projectedField(resource: TwinResource | undefined, field: string): unknown {
389
+ if (!resource) return undefined;
390
+ if (field === 'id' || field === 'type' || field === 'updatedAt') return resource[field];
391
+ return resource[field];
392
+ }
393
+
394
+ /** The kernel's ONE deterministic check: does `actual` satisfy the precondition expression?
395
+ * Shared by write-time preconditions (here), plan conflict detection (plan.ts) and changeset
396
+ * verifiers (changeset.ts) — one evaluator, so a check means the same thing everywhere. */
397
+ export function checkPrecondition(precondition: TwinActionPrecondition, actual: unknown): boolean {
398
+ switch (precondition.op) {
399
+ case 'exists': return actual !== undefined;
400
+ case 'not_exists': return actual === undefined;
401
+ case 'eq': case 'version_eq': return Object.is(actual, precondition.value);
402
+ case 'neq': return !Object.is(actual, precondition.value);
403
+ default: return false;
404
+ }
405
+ }
406
+
407
+ /** Look a precondition subject's field up in projected resources (undefined = no resource or
408
+ * no field — exactly what `exists`/`not_exists` distinguish). */
409
+ export function projectedPreconditionValue(precondition: TwinActionPrecondition, resources: TwinResource[]): unknown {
410
+ const resource = resources.find((r) => r.type === precondition.subject.type && r.id === precondition.subject.id);
411
+ return projectedField(resource, precondition.field);
412
+ }
413
+
414
+ function assertPreconditionsAgainst(action: TwinAction, resources: TwinResource[]): void {
415
+ if (!action.preconditions?.length) return;
416
+ for (const precondition of action.preconditions) {
417
+ const actual = projectedPreconditionValue(precondition, resources);
418
+ if (!checkPrecondition(precondition, actual)) throw new TwinActionPreconditionError(action.id, precondition);
419
+ }
420
+ }
421
+
422
+ function assertPreconditions(action: TwinAction, root?: string): void {
423
+ // Most writes carry none: projecting (and cloning) the whole tree to check nothing cost every append O(tree).
424
+ if (!action.preconditions?.length) return;
425
+ assertPreconditionsAgainst(action, projectResources(action.service, root));
426
+ }
427
+
428
+ /**
429
+ * Project the action log over the observed mirror → current twin resources.
430
+ * `set` actions overlay fields (creating subjects that don't exist in the mirror);
431
+ * reverted and confirmed actions are skipped (confirmed facts come from the
432
+ * observed log instead, so they are not projected twice).
433
+ */
434
+ /** THE TREE (log.ts): what a read sees — the nearest checkpoint plus the entries since; the parent's
435
+ * entries, then this branch's, skipping any the parent already holds. `until` folds only the
436
+ * branch entries BEFORE that id — the state a write saw at its own append (the rebase's
437
+ * evaluation point). */
438
+ export function projectResources(service: string, root?: string, opts: { until?: string } = {}): TwinResource[] {
439
+ return readTree(service, root, opts);
440
+ }
441
+
442
+ /** The local → vendor id aliases the landed copies carry: a read by the id a caller was handed before
443
+ * its write was performed resolves to the row the vendor now owns. */
444
+ export function subjectAliases(service: string, root?: string): Map<string, string> {
445
+ const out = new Map<string, string>();
446
+ for (const [from, to] of aliasesFrom(parentEntries(service, root))) out.set(from, to);
447
+ return out;
448
+ }
449
+ /** Resolve a subject id through the aliases: the vendor's id when a confirm rebound it, else the id itself. */
450
+ export function resolveSubjectId(service: string, type: string, id: string, root?: string): string {
451
+ return subjectAliases(service, root).get(`${type}:${id}`) ?? id;
452
+ }
453
+
454
+ /**
455
+ * Confirm a local action after it was pushed to the real vendor (R18): record the
456
+ * confirmed fields as an OBSERVED event (origin 'external' — it's now real) and
457
+ * append a `confirm` action mapping the local action → that observed event id.
458
+ * Projection then drops the local action (the fact lives in the observed log), so
459
+ * the change is counted exactly once. Returns the observed event id.
460
+ */
461
+ export function confirmAction(opts: {
462
+ service: string;
463
+ actionId: string;
464
+ subject: { type: string; id: string };
465
+ fields: SubjectFields;
466
+ /** Additional observed resources landed by the same compound action, under the same lock. */
467
+ additionalObservations?: Array<{ subject: { type: string; id: string }; fields: SubjectFields }>;
468
+ /** The subject id the VENDOR minted for this write (the push outcome's externalId). When it
469
+ * differs from the local id, the landed copy carries the vendor's id and `aliasOf` names the
470
+ * local one, so a read by either id finds the row. */
471
+ vendorSubjectId?: string;
472
+ /** the receipt on the landed copy; `deployed` when omitted (the push performed it) */
473
+ receipt?: Partial<Receipt>;
474
+ occurredAt: string;
475
+ root?: string;
476
+ }): { observedEventId: string; observedEventIds: string[] } {
477
+ // LANDING (log.ts): the entry is copied to the parent log with its receipt — no confirm row, no
478
+ // suppression; the fold skips a branch entry the parent holds.
479
+ const paths = worldPaths(opts.service, opts.root);
480
+ const base = listActions(opts.service, opts.root).find((a) => a.id === opts.actionId)
481
+ ?? ({ id: opts.actionId, service: opts.service, op: 'set', subject: opts.subject, occurredAt: opts.occurredAt } as TwinAction);
482
+ const receipt: Receipt = { status: 'deployed', at: opts.occurredAt, ...(opts.vendorSubjectId ? { externalId: opts.vendorSubjectId } : {}), ...(opts.receipt ?? {}) };
483
+ const copies = [
484
+ ...(opts.additionalObservations ?? []).map((o) => landedCopy(base, receipt, { subject: o.subject, fields: o.fields })),
485
+ landedCopy(base, receipt, { subject: opts.subject, fields: opts.fields, ...(opts.vendorSubjectId ? { vendorSubjectId: opts.vendorSubjectId } : {}) }),
486
+ ];
487
+ withFileLock(projectionLock(opts.service, opts.root), () => {
488
+ withAncestryLock(() => withFileLock(eventsLockPath(paths), () => {
489
+ const held = landedIds(parentEntries(opts.service, opts.root));
490
+ for (const copy of copies) if (!held.has(copy.id)) appendDurable(paths.events, `${JSON.stringify(copy)}\n`);
491
+ }));
492
+ dropCheckpoint(opts.service, opts.root);
493
+ });
494
+ const observedEventIds = copies.map((c) => c.id);
495
+ return { observedEventId: observedEventIds.at(-1)!, observedEventIds };
496
+ }
497
+
498
+ export type RevertOutcome =
499
+ | { status: 'reverted'; revertId: string }
500
+ | { status: 'already-reverted' }
501
+ | { status: 'confirmed' }
502
+ | { status: 'not-found' }
503
+ | { status: 'not-revertable'; op: TwinActionOp };
504
+
505
+ /**
506
+ * Revert one pending `set` action — decision AND append under the service projection +
507
+ * actions locks, so a concurrent writer (a connector confirming the same action from
508
+ * another process) cannot land between the check and the revert: the whole read-decide-
509
+ * append is ONE critical section. Idempotent: a repeat answers 'already-reverted'.
510
+ * Only `set` rows are revertable — reverting a revert or a confirm is a category error.
511
+ */
512
+ export function revertAction(opts: { service: string; actionId: string; occurredAt: string; root?: string }): RevertOutcome {
513
+ return withFileLock(projectionLock(opts.service, opts.root), () => withFileLock(actionsLock(opts.service, opts.root), (): RevertOutcome => {
514
+ const all = listActions(opts.service, opts.root);
515
+ const target = all.find((a) => a.id === opts.actionId);
516
+ if (target === undefined) return { status: 'not-found' };
517
+ if (target.op !== 'set') return { status: 'not-revertable', op: target.op };
518
+ // landed and taken by the vendor: not revertable; a landed copy the vendor refused or failed is
519
+ const landedCopy = parentEntries(opts.service, opts.root).find((e) => e.id === opts.actionId || e.landsId === opts.actionId);
520
+ if (landedCopy && landedCopy.receipt?.status !== 'refused' && landedCopy.receipt?.status !== 'failed') return { status: 'confirmed' };
521
+ if (all.some((a) => a.op === 'revert' && a.revertsActionId === opts.actionId)) return { status: 'already-reverted' };
522
+ const revert: TwinAction = withCorrelationId({
523
+ id: `revert:${opts.actionId}`,
524
+ service: opts.service,
525
+ op: 'revert',
526
+ subject: target.subject,
527
+ occurredAt: opts.occurredAt,
528
+ revertsActionId: opts.actionId,
529
+ });
530
+ appendActionRaw(revert, opts.root);
531
+ return { status: 'reverted', revertId: revert.id };
532
+ }));
533
+ }
534
+
535
+ /** The LOCAL OVERLAY: pending actions (set, not reverted, not yet confirmed) — the divergence
536
+ * from the mirror, what a twin's projection folds over the observed state. A QUARANTINED row
537
+ * (contract "A snapshot import never plants a push") is still local state and stays here; it
538
+ * leaves only the PUSHABLE suffix below. */
539
+ export function pendingActions(service: string, root?: string): TwinAction[] {
540
+ const actions = listActions(service, root);
541
+ const reverted = new Set(actions.filter((a) => a.op === 'revert').map((a) => a.revertsActionId));
542
+ const landed = landedIds(parentEntries(service, root));
543
+ return actions.filter((a) => a.op === 'set' && !reverted.has(a.id) && !landed.has(a.id));
544
+ }
545
+
546
+ /** A twin's OWN bookkeeping in its action log — a subject type beginning with `_` (stripe's
547
+ * `_idempotency` store, for one): part of the overlay the twin serves from, never a change the
548
+ * app made. It is not in the log a user reads, not in a diff, not in a changeset, not pushed. */
549
+ export function isTwinBookkeeping(action: Pick<TwinAction, 'subject'>): boolean {
550
+ return action.subject.type.startsWith('_');
551
+ }
552
+
553
+ /** The PUSHABLE suffix — `log origin..HEAD` as the push arm, the pending door, plans and the
554
+ * unpushed count read it: the overlay minus quarantined rows (an import is a copy, not a
555
+ * decision; releasing a quarantined row is an explicit act, never a scheduler's) and minus the
556
+ * twin's own bookkeeping. */
557
+ export function pushablePendingActions(service: string, root?: string): TwinAction[] {
558
+ return pendingActions(service, root).filter((a) => a.quarantined === undefined && !isTwinBookkeeping(a));
559
+ }
560
+
561
+
562
+
563
+ export const listTransactionCommits = listActions;
564
+ export const pendingTransactionCommits = pendingActions;