foottrace 0.0.0-stage → 1.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 (196) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -2
  3. package/dist/esm/index.d.ts +27 -0
  4. package/dist/esm/index.js +12 -0
  5. package/dist/esm/lib/capture/freeze.d.ts +91 -0
  6. package/dist/esm/lib/capture/freeze.js +313 -0
  7. package/dist/esm/lib/capture/ownData.d.ts +18 -0
  8. package/dist/esm/lib/capture/ownData.js +35 -0
  9. package/dist/esm/lib/capture/valueKinds.d.ts +57 -0
  10. package/dist/esm/lib/capture/valueKinds.js +141 -0
  11. package/dist/esm/lib/ids/runtimeStageId.d.ts +173 -0
  12. package/dist/esm/lib/ids/runtimeStageId.js +193 -0
  13. package/dist/esm/lib/memory/EventLog.d.ts +105 -0
  14. package/dist/esm/lib/memory/EventLog.js +147 -0
  15. package/dist/esm/lib/memory/RecordFrame.d.ts +232 -0
  16. package/dist/esm/lib/memory/RecordFrame.js +295 -0
  17. package/dist/esm/lib/memory/SharedMemory.d.ts +46 -0
  18. package/dist/esm/lib/memory/SharedMemory.js +87 -0
  19. package/dist/esm/lib/memory/TransactionBuffer.d.ts +521 -0
  20. package/dist/esm/lib/memory/TransactionBuffer.js +946 -0
  21. package/dist/esm/lib/memory/admission.d.ts +81 -0
  22. package/dist/esm/lib/memory/admission.js +152 -0
  23. package/dist/esm/lib/memory/backtrack.d.ts +286 -0
  24. package/dist/esm/lib/memory/backtrack.js +456 -0
  25. package/dist/esm/lib/memory/commitLogUtils.d.ts +217 -0
  26. package/dist/esm/lib/memory/commitLogUtils.js +383 -0
  27. package/dist/esm/lib/memory/deltaEncoding.d.ts +66 -0
  28. package/dist/esm/lib/memory/deltaEncoding.js +121 -0
  29. package/dist/esm/lib/memory/equality.d.ts +93 -0
  30. package/dist/esm/lib/memory/equality.js +261 -0
  31. package/dist/esm/lib/memory/eventPosition.d.ts +18 -0
  32. package/dist/esm/lib/memory/eventPosition.js +1 -0
  33. package/dist/esm/lib/memory/honesty.d.ts +93 -0
  34. package/dist/esm/lib/memory/honesty.js +91 -0
  35. package/dist/esm/lib/memory/keyPaths.d.ts +125 -0
  36. package/dist/esm/lib/memory/keyPaths.js +226 -0
  37. package/dist/esm/lib/memory/logModel.d.ts +76 -0
  38. package/dist/esm/lib/memory/logModel.js +363 -0
  39. package/dist/esm/lib/memory/merge.d.ts +26 -0
  40. package/dist/esm/lib/memory/merge.js +51 -0
  41. package/dist/esm/lib/memory/pathOps.d.ts +118 -0
  42. package/dist/esm/lib/memory/pathOps.js +292 -0
  43. package/dist/esm/lib/memory/paths.d.ts +56 -0
  44. package/dist/esm/lib/memory/paths.js +60 -0
  45. package/dist/esm/lib/memory/placeholders.d.ts +39 -0
  46. package/dist/esm/lib/memory/placeholders.js +39 -0
  47. package/dist/esm/lib/memory/recordCommit.d.ts +45 -0
  48. package/dist/esm/lib/memory/recordCommit.js +60 -0
  49. package/dist/esm/lib/memory/scrub.d.ts +52 -0
  50. package/dist/esm/lib/memory/scrub.js +72 -0
  51. package/dist/esm/lib/memory/types.d.ts +209 -0
  52. package/dist/esm/lib/memory/types.js +9 -0
  53. package/dist/esm/lib/memory/utils.d.ts +73 -0
  54. package/dist/esm/lib/memory/utils.js +150 -0
  55. package/dist/esm/lib/memory/verbs.d.ts +333 -0
  56. package/dist/esm/lib/memory/verbs.js +426 -0
  57. package/dist/esm/lib/recorder/CommitRangeIndex.d.ts +147 -0
  58. package/dist/esm/lib/recorder/CommitRangeIndex.js +206 -0
  59. package/dist/esm/lib/slice/elementProvenance.d.ts +81 -0
  60. package/dist/esm/lib/slice/elementProvenance.js +189 -0
  61. package/dist/esm/lib/slice/forwardSliceForKey.d.ts +88 -0
  62. package/dist/esm/lib/slice/forwardSliceForKey.js +242 -0
  63. package/dist/esm/lib/slice/index.d.ts +26 -0
  64. package/dist/esm/lib/slice/index.js +25 -0
  65. package/dist/esm/lib/slice/keyIndex.d.ts +137 -0
  66. package/dist/esm/lib/slice/keyIndex.js +292 -0
  67. package/dist/esm/lib/slice/keyTimeline.d.ts +49 -0
  68. package/dist/esm/lib/slice/keyTimeline.js +112 -0
  69. package/dist/esm/lib/slice/keysReadSources.d.ts +51 -0
  70. package/dist/esm/lib/slice/keysReadSources.js +94 -0
  71. package/dist/esm/lib/slice/serialize.d.ts +78 -0
  72. package/dist/esm/lib/slice/serialize.js +277 -0
  73. package/dist/esm/lib/slice/sliceForKey.d.ts +40 -0
  74. package/dist/esm/lib/slice/sliceForKey.js +78 -0
  75. package/dist/esm/lib/slice/types.d.ts +567 -0
  76. package/dist/esm/lib/slice/types.js +24 -0
  77. package/dist/esm/lib/time-travel/axis.d.ts +119 -0
  78. package/dist/esm/lib/time-travel/axis.js +178 -0
  79. package/dist/esm/lib/time-travel/bundles.d.ts +77 -0
  80. package/dist/esm/lib/time-travel/bundles.js +135 -0
  81. package/dist/esm/lib/time-travel/chain.d.ts +93 -0
  82. package/dist/esm/lib/time-travel/chain.js +187 -0
  83. package/dist/esm/lib/time-travel/commitStops.d.ts +55 -0
  84. package/dist/esm/lib/time-travel/commitStops.js +201 -0
  85. package/dist/esm/lib/time-travel/index.d.ts +22 -0
  86. package/dist/esm/lib/time-travel/index.js +20 -0
  87. package/dist/esm/lib/time-travel/stateAt.d.ts +102 -0
  88. package/dist/esm/lib/time-travel/stateAt.js +152 -0
  89. package/dist/esm/lib/time-travel/tagStops.d.ts +72 -0
  90. package/dist/esm/lib/time-travel/tagStops.js +101 -0
  91. package/dist/esm/lib/time-travel/timeTravel.d.ts +70 -0
  92. package/dist/esm/lib/time-travel/timeTravel.js +359 -0
  93. package/dist/esm/lib/time-travel/types.d.ts +470 -0
  94. package/dist/esm/lib/time-travel/types.js +22 -0
  95. package/dist/esm/package.json +1 -0
  96. package/dist/esm/paths.d.ts +4 -0
  97. package/dist/esm/paths.js +4 -0
  98. package/dist/esm/write.d.ts +49 -0
  99. package/dist/esm/write.js +47 -0
  100. package/dist/index.js +67 -0
  101. package/dist/lib/capture/freeze.js +319 -0
  102. package/dist/lib/capture/ownData.js +40 -0
  103. package/dist/lib/capture/valueKinds.js +145 -0
  104. package/dist/lib/ids/runtimeStageId.js +209 -0
  105. package/dist/lib/memory/EventLog.js +151 -0
  106. package/dist/lib/memory/RecordFrame.js +299 -0
  107. package/dist/lib/memory/SharedMemory.js +91 -0
  108. package/dist/lib/memory/TransactionBuffer.js +950 -0
  109. package/dist/lib/memory/admission.js +158 -0
  110. package/dist/lib/memory/backtrack.js +462 -0
  111. package/dist/lib/memory/commitLogUtils.js +398 -0
  112. package/dist/lib/memory/deltaEncoding.js +127 -0
  113. package/dist/lib/memory/equality.js +265 -0
  114. package/dist/lib/memory/eventPosition.js +2 -0
  115. package/dist/lib/memory/honesty.js +94 -0
  116. package/dist/lib/memory/keyPaths.js +242 -0
  117. package/dist/lib/memory/logModel.js +371 -0
  118. package/dist/lib/memory/merge.js +55 -0
  119. package/dist/lib/memory/pathOps.js +308 -0
  120. package/dist/lib/memory/paths.js +66 -0
  121. package/dist/lib/memory/placeholders.js +42 -0
  122. package/dist/lib/memory/recordCommit.js +64 -0
  123. package/dist/lib/memory/scrub.js +77 -0
  124. package/dist/lib/memory/types.js +10 -0
  125. package/dist/lib/memory/utils.js +169 -0
  126. package/dist/lib/memory/verbs.js +443 -0
  127. package/dist/lib/recorder/CommitRangeIndex.js +210 -0
  128. package/dist/lib/slice/elementProvenance.js +194 -0
  129. package/dist/lib/slice/forwardSliceForKey.js +246 -0
  130. package/dist/lib/slice/index.js +42 -0
  131. package/dist/lib/slice/keyIndex.js +312 -0
  132. package/dist/lib/slice/keyTimeline.js +116 -0
  133. package/dist/lib/slice/keysReadSources.js +100 -0
  134. package/dist/lib/slice/serialize.js +285 -0
  135. package/dist/lib/slice/sliceForKey.js +82 -0
  136. package/dist/lib/slice/types.js +25 -0
  137. package/dist/lib/time-travel/axis.js +183 -0
  138. package/dist/lib/time-travel/bundles.js +143 -0
  139. package/dist/lib/time-travel/chain.js +192 -0
  140. package/dist/lib/time-travel/commitStops.js +205 -0
  141. package/dist/lib/time-travel/index.js +31 -0
  142. package/dist/lib/time-travel/stateAt.js +159 -0
  143. package/dist/lib/time-travel/tagStops.js +105 -0
  144. package/dist/lib/time-travel/timeTravel.js +363 -0
  145. package/dist/lib/time-travel/types.js +23 -0
  146. package/dist/paths.js +14 -0
  147. package/dist/types/index.d.ts +27 -0
  148. package/dist/types/lib/capture/freeze.d.ts +91 -0
  149. package/dist/types/lib/capture/ownData.d.ts +18 -0
  150. package/dist/types/lib/capture/valueKinds.d.ts +57 -0
  151. package/dist/types/lib/ids/runtimeStageId.d.ts +173 -0
  152. package/dist/types/lib/memory/EventLog.d.ts +105 -0
  153. package/dist/types/lib/memory/RecordFrame.d.ts +232 -0
  154. package/dist/types/lib/memory/SharedMemory.d.ts +46 -0
  155. package/dist/types/lib/memory/TransactionBuffer.d.ts +521 -0
  156. package/dist/types/lib/memory/admission.d.ts +81 -0
  157. package/dist/types/lib/memory/backtrack.d.ts +286 -0
  158. package/dist/types/lib/memory/commitLogUtils.d.ts +217 -0
  159. package/dist/types/lib/memory/deltaEncoding.d.ts +66 -0
  160. package/dist/types/lib/memory/equality.d.ts +93 -0
  161. package/dist/types/lib/memory/eventPosition.d.ts +18 -0
  162. package/dist/types/lib/memory/honesty.d.ts +93 -0
  163. package/dist/types/lib/memory/keyPaths.d.ts +125 -0
  164. package/dist/types/lib/memory/logModel.d.ts +76 -0
  165. package/dist/types/lib/memory/merge.d.ts +26 -0
  166. package/dist/types/lib/memory/pathOps.d.ts +118 -0
  167. package/dist/types/lib/memory/paths.d.ts +56 -0
  168. package/dist/types/lib/memory/placeholders.d.ts +39 -0
  169. package/dist/types/lib/memory/recordCommit.d.ts +45 -0
  170. package/dist/types/lib/memory/scrub.d.ts +52 -0
  171. package/dist/types/lib/memory/types.d.ts +209 -0
  172. package/dist/types/lib/memory/utils.d.ts +73 -0
  173. package/dist/types/lib/memory/verbs.d.ts +333 -0
  174. package/dist/types/lib/recorder/CommitRangeIndex.d.ts +147 -0
  175. package/dist/types/lib/slice/elementProvenance.d.ts +81 -0
  176. package/dist/types/lib/slice/forwardSliceForKey.d.ts +88 -0
  177. package/dist/types/lib/slice/index.d.ts +26 -0
  178. package/dist/types/lib/slice/keyIndex.d.ts +137 -0
  179. package/dist/types/lib/slice/keyTimeline.d.ts +49 -0
  180. package/dist/types/lib/slice/keysReadSources.d.ts +51 -0
  181. package/dist/types/lib/slice/serialize.d.ts +78 -0
  182. package/dist/types/lib/slice/sliceForKey.d.ts +40 -0
  183. package/dist/types/lib/slice/types.d.ts +567 -0
  184. package/dist/types/lib/time-travel/axis.d.ts +119 -0
  185. package/dist/types/lib/time-travel/bundles.d.ts +77 -0
  186. package/dist/types/lib/time-travel/chain.d.ts +93 -0
  187. package/dist/types/lib/time-travel/commitStops.d.ts +55 -0
  188. package/dist/types/lib/time-travel/index.d.ts +22 -0
  189. package/dist/types/lib/time-travel/stateAt.d.ts +102 -0
  190. package/dist/types/lib/time-travel/tagStops.d.ts +72 -0
  191. package/dist/types/lib/time-travel/timeTravel.d.ts +70 -0
  192. package/dist/types/lib/time-travel/types.d.ts +470 -0
  193. package/dist/types/paths.d.ts +4 -0
  194. package/dist/types/write.d.ts +49 -0
  195. package/dist/write.js +56 -0
  196. package/package.json +48 -4
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ /**
3
+ * utils.ts — nested-object helpers for the heap, and the memory leaves' one
4
+ * re-export surface.
5
+ *
6
+ * The helpers here (`setNestedValue`, `updateNestedValue`, `updateValue`,
7
+ * `getNestedValue`) walk a nested object by path with
8
+ * prototype-pollution protection. Everything else this file used to hold has an
9
+ * owner of its own and is re-exported, so no importer moved: the path codec
10
+ * (`paths.ts`), structural equality (`equality.ts`) and the union merge
11
+ * (`merge.ts`) at L0, and the verb law (`verbs.ts`) at L1 — `applySmartMerge`,
12
+ * `nextGeneration`, `applySmartMergeInto`, `dryFold`, `supersededByNextSet`.
13
+ * The log's scrub, `redactPatch`, moved out in 9.33.0; it is the record's own, `scrub.ts` (C4).
14
+ * The writers take an ADDRESS, a path prefix their caller computed, and never
15
+ * name one (C2: the engine decides where a stage writes, `runAddress.ts · runAddress`).
16
+ * Zero external dependencies.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.getNestedValue = exports.updateValue = exports.updateNestedValue = exports.setNestedValue = exports.isDeniedWritePath = exports.getRunAndGlobalPaths = exports.supersededByNextSet = exports.nextGeneration = exports.dryFold = exports.applySmartMergeInto = exports.applySmartMerge = exports.pathSegments = exports.normalisePath = exports.DELIM = exports.deepSmartMerge = exports.deepEqual = void 0;
20
+ const ownData_js_1 = require("../capture/ownData.js");
21
+ const pathOps_js_1 = require("./pathOps.js");
22
+ var equality_js_1 = require("./equality.js");
23
+ Object.defineProperty(exports, "deepEqual", { enumerable: true, get: function () { return equality_js_1.deepEqual; } });
24
+ var merge_js_1 = require("./merge.js");
25
+ Object.defineProperty(exports, "deepSmartMerge", { enumerable: true, get: function () { return merge_js_1.deepSmartMerge; } });
26
+ var paths_js_1 = require("./paths.js");
27
+ Object.defineProperty(exports, "DELIM", { enumerable: true, get: function () { return paths_js_1.DELIM; } });
28
+ Object.defineProperty(exports, "normalisePath", { enumerable: true, get: function () { return paths_js_1.normalisePath; } });
29
+ Object.defineProperty(exports, "pathSegments", { enumerable: true, get: function () { return paths_js_1.pathSegments; } });
30
+ var verbs_js_1 = require("./verbs.js");
31
+ Object.defineProperty(exports, "applySmartMerge", { enumerable: true, get: function () { return verbs_js_1.applySmartMerge; } });
32
+ Object.defineProperty(exports, "applySmartMergeInto", { enumerable: true, get: function () { return verbs_js_1.applySmartMergeInto; } });
33
+ Object.defineProperty(exports, "dryFold", { enumerable: true, get: function () { return verbs_js_1.dryFold; } });
34
+ Object.defineProperty(exports, "nextGeneration", { enumerable: true, get: function () { return verbs_js_1.nextGeneration; } });
35
+ Object.defineProperty(exports, "supersededByNextSet", { enumerable: true, get: function () { return verbs_js_1.supersededByNextSet; } });
36
+ /**
37
+ * The two places a value at `path` can sit: under `address` (`runPath`, absent
38
+ * for the root address `[]`) and at the root (`globalPath`). `address` is a
39
+ * path prefix the caller computed — for the engine, a frame's run namespace
40
+ * (`runAddress.ts · runAddress`, C2), held by its record frame (`RecordFrame · address`, C3).
41
+ */
42
+ function getRunAndGlobalPaths(address = [], path = []) {
43
+ return {
44
+ runPath: addressOf(address).length > 0 ? pathUnder(address, path) : undefined,
45
+ globalPath: [...path],
46
+ };
47
+ }
48
+ exports.getRunAndGlobalPaths = getRunAndGlobalPaths;
49
+ /** An address is an array. A run id still passed in its place (C2) is refused, never spread into its characters. */
50
+ function addressOf(address) {
51
+ if (Array.isArray(address))
52
+ return address;
53
+ throw new TypeError(`footprintjs: an address is an array path prefix, got ${address === null ? 'null' : typeof address}`);
54
+ }
55
+ /**
56
+ * `address` then `path`, as one new array. An index loop, because the double
57
+ * spread `[...address, ...path]` costs twice as much and a frame builds one on
58
+ * every read it serves from live state (a fork child reading a root key).
59
+ */
60
+ function pathUnder(address, path) {
61
+ const out = new Array(address.length + path.length);
62
+ for (let i = 0; i < address.length; i++)
63
+ out[i] = address[i];
64
+ for (let i = 0; i < path.length; i++)
65
+ out[address.length + i] = path[i];
66
+ return out;
67
+ }
68
+ /**
69
+ * Refuse the WHOLE selector before creating a container or copying a generation.
70
+ * The address's array contract is checked even when another selector is denied.
71
+ * Payload keys are data, not selectors, and are deliberately not inspected.
72
+ * @internal Shared by the nested writers and SharedMemory's pre-copy boundary.
73
+ */
74
+ function isDeniedWritePath(address = [], path = [], field) {
75
+ const prefix = addressOf(address);
76
+ return prefix.some(pathOps_js_1.isDeniedSegment) || path.some(pathOps_js_1.isDeniedSegment) || (0, pathOps_js_1.isDeniedSegment)(field);
77
+ }
78
+ exports.isDeniedWritePath = isDeniedWritePath;
79
+ /**
80
+ * The container at `address` + `path` inside `obj`, each missing one created
81
+ * on the way: the one AT the address from `defaultValues` when given, every
82
+ * other one empty. The one walk both nested writers share.
83
+ */
84
+ function containerAt(obj, address = [], path = [], defaultValues) {
85
+ const segments = pathUnder(addressOf(address), path);
86
+ let current = obj;
87
+ for (let i = 0; i < segments.length; i++) {
88
+ const key = segments[i];
89
+ if (!Object.prototype.hasOwnProperty.call(current, key)) {
90
+ (0, ownData_js_1.setOwnValue)(current, key, i === address.length - 1 && defaultValues ? defaultValues : {});
91
+ }
92
+ current = current[key];
93
+ }
94
+ return current;
95
+ }
96
+ /**
97
+ * Sets a value at a nested path under `address`, creating intermediate objects as needed.
98
+ * A denied address/path/field leaves `obj` unchanged, including its prototype.
99
+ */
100
+ function setNestedValue(obj, address, path, field, value, defaultValues) {
101
+ if (isDeniedWritePath(address, path, field))
102
+ return obj;
103
+ (0, ownData_js_1.setOwnValue)(containerAt(obj, address, path, defaultValues), field, value);
104
+ return obj;
105
+ }
106
+ exports.setNestedValue = setNestedValue;
107
+ /**
108
+ * Updates a value in the object at a nested path under `address`.
109
+ * A denied address/path/field leaves `obj` unchanged, including its prototype.
110
+ * - Arrays: concatenate
111
+ * - Objects: shallow merge at each level
112
+ * - Primitives: replace
113
+ */
114
+ function updateNestedValue(obj, address, path, field, value, defaultValues) {
115
+ if (isDeniedWritePath(address, path, field))
116
+ return obj;
117
+ updateValue(containerAt(obj, address, path, defaultValues), field, value);
118
+ return obj;
119
+ }
120
+ exports.updateNestedValue = updateNestedValue;
121
+ /**
122
+ * In-place value update with merge semantics.
123
+ * - Arrays (non-empty): concatenate onto existing
124
+ * - Arrays (empty): direct replace — writing `[]` clears the field
125
+ * - Objects (non-empty): shallow merge (spread)
126
+ * - Objects (empty): direct replace — writing `{}` clears the field
127
+ * - Primitives: direct assignment
128
+ *
129
+ * Note on empty arrays: both `value && Array.isArray(value)` and
130
+ * `Array.isArray(value)` evaluate the same for arrays — `[]` is truthy in
131
+ * JavaScript, so the `&&` guard was never the issue. The actual bug was the
132
+ * concat path: `[...cur, ...[]]` silently returned `cur` unchanged when `value`
133
+ * was `[]`, making `updateValue(obj, 'tags', [])` a no-op instead of a clear.
134
+ * The fix is the explicit `value.length === 0` early-return branch.
135
+ */
136
+ function updateValue(object, key, value) {
137
+ if ((0, pathOps_js_1.isDeniedSegment)(key))
138
+ return;
139
+ if (Array.isArray(value)) {
140
+ if (value.length === 0) {
141
+ (0, ownData_js_1.setOwnValue)(object, key, value); // clear: [] replaces whatever was there
142
+ }
143
+ else {
144
+ const cur = Object.prototype.hasOwnProperty.call(object, key) ? object[key] : undefined;
145
+ (0, ownData_js_1.setOwnValue)(object, key, cur === undefined ? value : [...cur, ...value]);
146
+ }
147
+ }
148
+ else if (value && typeof value === 'object' && Object.keys(value).length) {
149
+ const cur = Object.prototype.hasOwnProperty.call(object, key) ? object[key] : undefined;
150
+ (0, ownData_js_1.setOwnValue)(object, key, cur === undefined ? value : { ...cur, ...value });
151
+ }
152
+ else {
153
+ (0, ownData_js_1.setOwnValue)(object, key, value);
154
+ }
155
+ }
156
+ exports.updateValue = updateValue;
157
+ /**
158
+ * Gets a value at a nested path with prototype-pollution protection.
159
+ */
160
+ function getNestedValue(root, path, field) {
161
+ const node = path && path.length > 0 ? (0, pathOps_js_1.nativeGet)(root, path) : root;
162
+ if (field === undefined || node === undefined)
163
+ return node;
164
+ if (node !== null && typeof node === 'object' && Object.prototype.hasOwnProperty.call(node, field)) {
165
+ return node[field];
166
+ }
167
+ return undefined;
168
+ }
169
+ exports.getNestedValue = getNestedValue;
@@ -0,0 +1,443 @@
1
+ "use strict";
2
+ /**
3
+ * verbs.ts — THE VERB LAW (L1): the one place a commit row becomes a value.
4
+ *
5
+ * A row of a commit bundle's `trace` carries one of four verbs —
6
+ * `set | merge | append | delete` — and ONE step turns it into a value:
7
+ * {@link applyVerb}. State is a left fold of that step (the event-sourcing
8
+ * reducer): {@link foldRows} folds one bundle's rows into a state, and
9
+ * {@link foldKey} folds ONE KEY across a log — every row under the key's
10
+ * top-level key, read at the key (the value rule, `keyPaths.ts`). Every reader
11
+ * of the log runs one of the two —
12
+ *
13
+ * - the live commit and the redacted mirror (`nextGeneration`),
14
+ * - the public replay (`applySmartMerge`), the read-side folds
15
+ * (`applySmartMergeInto` — `EventLog.materialise`, `stateAt`) and the
16
+ * admitted record's comparison (`dryFold`): all {@link foldRows},
17
+ * - `commitValueAt`, and `arrayProvenance` as an OBSERVER of the same fold:
18
+ * {@link foldKey}.
19
+ *
20
+ * Three questions that used to be answered once per reader are answered here
21
+ * once: what a verb does to the value before it ({@link applyVerb}), how the
22
+ * result is placed ({@link placeVerb}), and how the log's own values are
23
+ * treated on the way in (the clone discipline — a PARAMETER of the fold, see
24
+ * {@link Discipline} and {@link RecordedPayload}). A new verb is one new arm of
25
+ * `applyVerb` and one row of the traits table; the compiler refuses either
26
+ * missing ({@link isTotal}, {@link recordsTail}, {@link VERBS}).
27
+ *
28
+ * An UNKNOWN verb is refused with {@link UnknownVerbError}, never folded as a
29
+ * `merge` (the old silent default). Engine-written logs carry the four, so
30
+ * every engine run folds byte-for-byte as before.
31
+ */
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.foldKey = exports.dryFold = exports.applySmartMergeInto = exports.nextGeneration = exports.applySmartMerge = exports.foldRows = exports.supersededByNextSet = exports.placeVerb = exports.applyVerb = exports.RecordedPayload = exports.ABSENT = exports.UnknownVerbError = exports.recordsTail = exports.isTotal = exports.isVerb = exports.VERBS = void 0;
34
+ const merge_js_1 = require("./merge.js");
35
+ const pathOps_js_1 = require("./pathOps.js");
36
+ const paths_js_1 = require("./paths.js");
37
+ /**
38
+ * The vocabulary: each verb with the two traits readers branch on, in the
39
+ * order the contract names them. A `Record` over the union — the compiler
40
+ * refuses a table that misses (or invents) a verb, so a verb cannot be added
41
+ * without saying what it is.
42
+ *
43
+ * `total` — the row decides the value ALONE; it reads nothing from what came
44
+ * before it ({@link isTotal}).
45
+ * `tail` — the row records ONLY what it adds, not the whole value
46
+ * ({@link recordsTail}).
47
+ */
48
+ const TRAITS = {
49
+ set: { total: true, tail: false },
50
+ merge: { total: false, tail: false },
51
+ append: { total: false, tail: true },
52
+ delete: { total: true, tail: false },
53
+ };
54
+ /** The four verbs, in the order the contract names them. */
55
+ exports.VERBS = Object.freeze(Object.keys(TRAITS));
56
+ /** Is `value` one of the four? The question every reader of a foreign log asks first. */
57
+ function isVerb(value) {
58
+ return typeof value === 'string' && Object.prototype.hasOwnProperty.call(TRAITS, value);
59
+ }
60
+ exports.isVerb = isVerb;
61
+ /**
62
+ * Does the verb decide the value ALONE — read nothing from what came before
63
+ * it? `set` and `delete`. A reader that only needs the final value may start
64
+ * at the last such row ({@link foldKey} `anchored`); one that tracks how the
65
+ * value grew (provenance) starts at the first. Pinned against
66
+ * {@link applyVerb} itself, so a new verb cannot misreport it.
67
+ */
68
+ function isTotal(verb) {
69
+ return TRAITS[verb].total;
70
+ }
71
+ exports.isTotal = isTotal;
72
+ /**
73
+ * Does the row record ONLY what it adds — a tail — rather than the whole
74
+ * value? `append`. What the row added is then known exactly: a reader that
75
+ * tracks where array elements came from attributes them to this row without
76
+ * comparing arrays, where every other verb has to be inferred from.
77
+ */
78
+ function recordsTail(verb) {
79
+ return TRAITS[verb].tail;
80
+ }
81
+ exports.recordsTail = recordsTail;
82
+ /**
83
+ * Thrown by the readers of a commit log — {@link foldRows} (behind
84
+ * `applySmartMerge`), `commitValueAt`, `arrayProvenance` — when a row carries a
85
+ * verb outside {@link VERBS}: a foreign or corrupted log fails loudly instead
86
+ * of being replayed as a `merge`. `verb`, `path`, `row` and `commit` name the
87
+ * row (`path` / `row` / `commit` are absent when the caller handed over a
88
+ * bare verb); `path` is the trace's own DELIM-joined form — take it apart with
89
+ * `pathSegments`. The message quotes `verb` and `path`, so a hostile log cannot
90
+ * reshape it.
91
+ */
92
+ class UnknownVerbError extends Error {
93
+ verb;
94
+ path;
95
+ row;
96
+ commit;
97
+ constructor(verb, site) {
98
+ const named = typeof verb === 'string' ? JSON.stringify(verb) : verb === undefined ? 'undefined' : `of type ${typeof verb}`;
99
+ // The verb and the path come from a log nobody vetted: quoted (JSON), so a
100
+ // control character in either cannot reshape the message.
101
+ const where = site === undefined
102
+ ? ''
103
+ : ` on trace row ${site.row} (path ${JSON.stringify((0, paths_js_1.pathSegments)(site.path).join(' › '))}${site.commit === undefined ? '' : `, commit ${site.commit}`})`;
104
+ const one = exports.VERBS.join(' | ');
105
+ super(`unknown verb ${named}${where}: a commit row is one of ${one} — the log is refused, not replayed as a merge`);
106
+ this.name = 'UnknownVerbError';
107
+ this.verb = verb;
108
+ if (site !== undefined) {
109
+ this.path = site.path;
110
+ this.row = site.row;
111
+ if (site.commit !== undefined)
112
+ this.commit = site.commit;
113
+ }
114
+ }
115
+ }
116
+ exports.UnknownVerbError = UnknownVerbError;
117
+ function refuseVerb(verb) {
118
+ throw new UnknownVerbError(verb);
119
+ }
120
+ // ─── The step ────────────────────────────────────────────────────────────────
121
+ /**
122
+ * What a `delete` row leaves behind: the KEY IS GONE — which is not the value
123
+ * `undefined` (a root replay removes the key; a per-path fold reads it as
124
+ * `undefined`). {@link applyVerb} returns it, {@link placeVerb} acts on it.
125
+ */
126
+ exports.ABSENT = Symbol('footprintjs.verbs.absent');
127
+ /**
128
+ * One bundle's recorded payload, read by path under a clone discipline — the
129
+ * values {@link applyVerb} reads out of the log.
130
+ *
131
+ * `detach` — `true` for every fold that keeps its result: a `set` value or an
132
+ * `append` tail is a clone of what the log holds and the merge delta comes from
133
+ * ONE detached copy of `updates`, so the result never aliases the log. `false`
134
+ * only for the comparison fold ({@link dryFold}), which places recorded values
135
+ * as recorded and drops its result.
136
+ *
137
+ * The delta is detached ONCE per bundle, lazily — a bundle with no `merge` row
138
+ * pays nothing. `deepSmartMerge` dedups an array union BY REFERENCE and two
139
+ * `merge` rows of one bundle replay the same accumulated delta, so the rows
140
+ * must keep seeing the same objects; a clone per row duplicated their elements
141
+ * (the differential caught it in the replay; `commitValueAt` carried the bug
142
+ * until it folded through here). `only` narrows the copy to ONE path — a
143
+ * per-path fold reads nothing else, and must not pay for the rest of the
144
+ * bundle.
145
+ */
146
+ class RecordedPayload {
147
+ updates;
148
+ overwrite;
149
+ detach;
150
+ only;
151
+ deltas;
152
+ constructor(updates, overwrite, detach, only) {
153
+ this.updates = updates;
154
+ this.overwrite = overwrite;
155
+ this.detach = detach;
156
+ this.only = only;
157
+ }
158
+ /** What a `set` places and an `append` adds: `overwrite[path]`, cloned when detaching. */
159
+ value(segs) {
160
+ const recorded = (0, pathOps_js_1.nativeGet)(this.overwrite, segs);
161
+ return this.detach ? structuredClone(recorded) : recorded;
162
+ }
163
+ /** The accumulated merge delta at `path`: `updates[path]`, from the bundle's one copy. */
164
+ delta(segs) {
165
+ if (this.deltas === undefined) {
166
+ const source = this.only === undefined ? this.updates : atPath(this.updates, this.only);
167
+ this.deltas = this.detach ? structuredClone(source) : source;
168
+ }
169
+ return (0, pathOps_js_1.nativeGet)(this.deltas, segs);
170
+ }
171
+ }
172
+ exports.RecordedPayload = RecordedPayload;
173
+ /** `tree` reduced to ONE path: that sub-tree, by reference, under fresh containers. */
174
+ function atPath(tree, segs) {
175
+ const leaf = (0, pathOps_js_1.nativeGet)(tree, segs);
176
+ return leaf === undefined ? {} : (0, pathOps_js_1.nativeSet)({}, segs, leaf);
177
+ }
178
+ /**
179
+ * ONE row, ONE step: the value at the row's path AFTER the row, given the
180
+ * value BEFORE it and the bundle's recorded payload. THE verb law — no other
181
+ * function in the library has an arm per verb.
182
+ *
183
+ * - `'set'` — the recorded value (`overwrite[path]`, the full final value).
184
+ * - `'merge'` — `deepSmartMerge` of the accumulated `updates[path]` delta onto
185
+ * the value before.
186
+ * - `'append'` — (delta mode) `overwrite[path]` holds ONLY the tail: concatenate
187
+ * it onto the value before. When either is not an array (an out-of-order
188
+ * replay base, or a REDACTED tail — `scrubPatch` replaces matched payloads
189
+ * with the `'REDACTED'` string) the tail BECOMES the value — the same
190
+ * terminal value a redacted or corrupt `set` produces.
191
+ * - `'delete'` — {@link ABSENT}: the key is removed. The path stays enumerated
192
+ * in `overwrite` (value `undefined`) for key-set consumers; the step ignores
193
+ * that value.
194
+ *
195
+ * Pure: it reads the payload and never writes. `verb` must be a {@link Verb}
196
+ * (the folds check with {@link isVerb} first and name the row); anything else
197
+ * that reaches it is refused with {@link UnknownVerbError}.
198
+ */
199
+ function applyVerb(verb, before, at, segs) {
200
+ if (verb === 'set')
201
+ return at.value(segs);
202
+ if (verb === 'append') {
203
+ const tail = at.value(segs);
204
+ return Array.isArray(before) && Array.isArray(tail) ? [...before, ...tail] : tail;
205
+ }
206
+ if (verb === 'delete')
207
+ return exports.ABSENT;
208
+ if (verb === 'merge')
209
+ return (0, merge_js_1.deepSmartMerge)(before, at.delta(segs));
210
+ return refuseVerb(verb); // `verb` is `never` here — a fifth verb in the union stops compiling
211
+ }
212
+ exports.applyVerb = applyVerb;
213
+ /** Put a step's value at its path in `out` — or remove the key, for {@link ABSENT}. */
214
+ function placeVerb(out, segs, next) {
215
+ if (next === exports.ABSENT)
216
+ (0, pathOps_js_1.nativeDelete)(out, segs);
217
+ else
218
+ (0, pathOps_js_1.nativeSet)(out, segs, next);
219
+ }
220
+ exports.placeVerb = placeVerb;
221
+ /**
222
+ * Is row `i` a `set` that the NEXT row sets again — same path, same verb — so
223
+ * that applying it is work the next row redoes?
224
+ *
225
+ * The one skip every replay iterator shares (9.22.1). A `set` row writes a
226
+ * clone of the recorded value at its path; the next row, a `set` of the same
227
+ * path, writes a clone of the SAME recorded value over it (the bundle's patch
228
+ * tree is fixed), and nothing runs in between — so the first write is
229
+ * unobservable, container creation and key order included. Only CONSECUTIVE
230
+ * rows qualify: a row in between (an ancestor `merge`, a sibling `set` that
231
+ * creates a container) can see the intermediate value, and a `merge`,
232
+ * `append` or `delete` next row depends on what is there. Rows are never
233
+ * dropped from the log — the 9.22.0 element-write funnel records N
234
+ * whole-array `set` rows on one path, and this is what makes replaying them
235
+ * O(N) instead of O(N × rows).
236
+ *
237
+ * Asked by {@link foldRows} — live state and the redacted mirror through
238
+ * {@link nextGeneration}, `EventLog.materialise` and `stateAt` through
239
+ * {@link applySmartMergeInto}, the public {@link applySmartMerge}, and the
240
+ * admitted record's {@link dryFold}. {@link foldKey} needs no skip: it
241
+ * anchors at the top-level key's LAST `set`/`delete`, or (provenance) a
242
+ * repeated `set` of the one recorded value changes no birth.
243
+ */
244
+ function supersededByNextSet(rows, i) {
245
+ const row = rows[i];
246
+ if (row.verb !== 'set')
247
+ return false;
248
+ const next = rows[i + 1];
249
+ return next !== undefined && next.verb === 'set' && next.path === row.path;
250
+ }
251
+ exports.supersededByNextSet = supersededByNextSet;
252
+ /** Does any row write THROUGH a container (a delimited, nested path)? */
253
+ function hasNestedRow(rows) {
254
+ for (let i = 0; i < rows.length; i++)
255
+ if (rows[i].path.indexOf(paths_js_1.DELIM) !== -1)
256
+ return true;
257
+ return false;
258
+ }
259
+ /**
260
+ * Fold one bundle's rows into `out`, in place: for each row (a `set` the next
261
+ * row sets again is skipped — {@link supersededByNextSet}) take the step
262
+ * ({@link applyVerb}) and put its value where it belongs ({@link placeVerb}).
263
+ * `out` is the caller's — see {@link Discipline} for what it must be.
264
+ *
265
+ * A row whose verb is not one of {@link VERBS} stops the fold with
266
+ * {@link UnknownVerbError} naming the row; the rows before it have been
267
+ * applied, to a state the caller discards (every in-tree caller does).
268
+ */
269
+ function foldRows(out, updates, overwrite, rows, discipline) {
270
+ const detach = discipline !== 'byReference';
271
+ const owned = discipline !== 'private' && hasNestedRow(rows) ? new WeakSet() : undefined;
272
+ (0, pathOps_js_1.own)(out, owned);
273
+ const at = new RecordedPayload(updates, overwrite, detach);
274
+ for (let i = 0; i < rows.length; i++) {
275
+ if (supersededByNextSet(rows, i))
276
+ continue;
277
+ const { path, verb } = rows[i];
278
+ if (!isVerb(verb))
279
+ throw new UnknownVerbError(verb, { path, row: i });
280
+ const segs = path.split(paths_js_1.DELIM);
281
+ if (owned !== undefined && segs.length > 1)
282
+ (0, pathOps_js_1.ownSpine)(out, segs, owned);
283
+ // A total verb reads nothing from what came before it (isTotal) — so the
284
+ // fold does not look.
285
+ const next = applyVerb(verb, isTotal(verb) ? undefined : (0, pathOps_js_1.nativeGet)(out, segs), at, segs);
286
+ // A value this fold CREATED is its own to edit in place; a recorded value
287
+ // placed by reference is the log's.
288
+ if (detach || verb === 'merge')
289
+ (0, pathOps_js_1.own)(next, owned);
290
+ placeVerb(out, segs, next);
291
+ }
292
+ return out;
293
+ }
294
+ exports.foldRows = foldRows;
295
+ /**
296
+ * Applies a commit bundle to a base state by replaying operations in order.
297
+ * Guarantees "last writer wins" semantics.
298
+ *
299
+ * Returns a FULLY DETACHED result: the base is deep-cloned first and every
300
+ * value the replay writes is its own (`set` / `append` clone the recorded
301
+ * value; the `merge` arm reads a detached copy of `updates`), so the result
302
+ * shares no container with `base`, `updates` or `overwrite`. This is the
303
+ * public (`foottrace`) contract, unchanged by copy-on-write
304
+ * (9.29.0) — byte-identical to 9.28.0 for every caller. The engine's own
305
+ * commit does NOT come here: `SharedMemory.applyPatch` builds the next
306
+ * generation with `nextGeneration`, which copies only the written
307
+ * paths. Both run the ONE fold, {@link foldRows}; the verbs are
308
+ * {@link applyVerb}'s.
309
+ *
310
+ * Verb arms — see {@link applyVerb}. A row with any other verb throws
311
+ * {@link UnknownVerbError}.
312
+ *
313
+ * Work, not bytes (9.22.1): a `set` row the NEXT row re-sets is skipped
314
+ * ({@link supersededByNextSet}) — the rows stay in the log, only the clone
315
+ * each would have paid is not. Every consumer inherits the skip from
316
+ * `foldRows`; there is no second replay loop to keep in step.
317
+ */
318
+ function applySmartMerge(base, updates, overwrite, trace) {
319
+ return foldRows(structuredClone(base), updates, overwrite, trace, 'private');
320
+ }
321
+ exports.applySmartMerge = applySmartMerge;
322
+ /**
323
+ * The engine's commit (copy-on-write, 9.29.0 — docs/design/2026-10-copy-on-
324
+ * write-commit.md): the NEXT committed generation. A copy of `base`'s ROOT
325
+ * plus a copy of every container on each written path; every other subtree
326
+ * is SHARED with `base`, which is never edited — committed state is
327
+ * immutable-after-swap, so sharing is safe, and a commit costs O(what it
328
+ * wrote), not O(state). `SharedMemory.applyPatch` (live state and the
329
+ * redacted mirror) is its caller. Internal: the public replay is
330
+ * {@link applySmartMerge}.
331
+ */
332
+ function nextGeneration(base, updates, overwrite, trace) {
333
+ return foldRows((0, pathOps_js_1.ownedRootOf)(base), updates, overwrite, trace, 'pathCopy');
334
+ }
335
+ exports.nextGeneration = nextGeneration;
336
+ /**
337
+ * The same replay, INTO `target` — no clone of its own. For a caller that
338
+ * already holds a private working copy (the read-side folds: `stateAt` in
339
+ * `time-travel/stateAt.ts` and `EventLog.materialise` clone their base ONCE
340
+ * and then apply every bundle here), so a fold over N bundles costs N row
341
+ * applications, not N clones of the whole state. Never hand it committed
342
+ * state: it mutates the ROOT in place. Below the root it follows the live
343
+ * commit's law ({@link nextGeneration}) — a container this call did not
344
+ * create is copied before a write passes through it — so a fold and the live
345
+ * state give the same value at every path, aliased subtrees included.
346
+ * The values it writes are its own, so `target` never aliases the log.
347
+ */
348
+ function applySmartMergeInto(target, updates, overwrite, trace) {
349
+ return foldRows(target, updates, overwrite, trace, 'pathCopy');
350
+ }
351
+ exports.applySmartMergeInto = applySmartMergeInto;
352
+ /**
353
+ * The fold, for COMPARISON only (9.30.0 — the admitted record): `base` with
354
+ * the bundle replayed by the one verb law, copy-on-write below the root,
355
+ * every recorded value placed BY REFERENCE — no clone, so it costs what the
356
+ * bundle wrote and nothing more. It aliases the payload and every subtree of
357
+ * `base` the bundle did not write, and edits neither. Never hand it out,
358
+ * store it or write through it: `TransactionBuffer` builds one to ask whether
359
+ * a bundle folds back to what the stage read, then drops it.
360
+ */
361
+ function dryFold(base, updates, overwrite, trace) {
362
+ return foldRows((0, pathOps_js_1.ownedRootOf)(base), updates, overwrite, trace, 'byReference');
363
+ }
364
+ exports.dryFold = dryFold;
365
+ /**
366
+ * The value of ONE key after `touches` — EVERY row under its top-level key, in
367
+ * commit order, each already checked with {@link isVerb} — folded from an absent
368
+ * key, read at the key (`segs`, its DELIM segments): the value rule of
369
+ * `keyPaths.ts`, the fold `stateAt` does restricted to one top-level key. Top-level
370
+ * keys never interact under the step, so the restriction is exact.
371
+ *
372
+ * The same step ({@link applyVerb}) and the same discipline as {@link foldRows}'s
373
+ * `'pathCopy'`: `set` / `append` values are clones, the merge delta is detached
374
+ * once per BUNDLE (narrowed to the top-level key), so two `merge` rows of one
375
+ * bundle see the same objects, and a row that writes through a container this
376
+ * fold did not create copies it first. Returns `undefined` for an absent key.
377
+ * O(the rows under the key's top-level key), not O(the log).
378
+ */
379
+ function foldKey(touches, segs, options = {}) {
380
+ const root = segs[0];
381
+ let from = 0;
382
+ if (options.anchored === true) {
383
+ for (let i = touches.length - 1; i >= 0; i--) {
384
+ if (isTotal(touches[i].verb) && touches[i].path === root) {
385
+ from = i;
386
+ break;
387
+ }
388
+ }
389
+ }
390
+ const state = {};
391
+ if (options.start !== undefined)
392
+ state[root] = options.start;
393
+ let owned = new WeakSet();
394
+ owned.add(state);
395
+ const observe = options.observe;
396
+ const generations = options.generations;
397
+ let kept = -1;
398
+ let keptBefore;
399
+ /** Drop ownership of every container: the next write through one copies it, so a kept value stays as it is. */
400
+ const release = () => {
401
+ owned = new WeakSet();
402
+ owned.add(state);
403
+ };
404
+ let at;
405
+ let atCommit = -1;
406
+ for (let i = from; i < touches.length; i++) {
407
+ const touch = touches[i];
408
+ if (at === undefined || touch.commitIdx !== atCommit) {
409
+ if (kept !== -1) {
410
+ generations.keep(kept, keptBefore, (0, pathOps_js_1.nativeGet)(state, segs));
411
+ kept = -1;
412
+ release();
413
+ }
414
+ if (generations !== undefined && generations.at(touch.commitIdx, state)) {
415
+ release();
416
+ kept = touch.commitIdx;
417
+ keptBefore = (0, pathOps_js_1.nativeGet)(state, segs);
418
+ }
419
+ at = new RecordedPayload(touch.bundle.updates, touch.bundle.overwrite, true, [root]);
420
+ atCommit = touch.commitIdx;
421
+ }
422
+ const rowSegs = touch.path === root ? [root] : touch.path.split(paths_js_1.DELIM);
423
+ const watched = observe !== undefined && (touch.relation !== undefined || options.everyRow === true);
424
+ let before;
425
+ if (watched) {
426
+ before = (0, pathOps_js_1.nativeGet)(state, segs);
427
+ if (touch.relation === 'inside' && Array.isArray(before))
428
+ before = before.slice();
429
+ }
430
+ if (rowSegs.length > 1)
431
+ (0, pathOps_js_1.ownSpine)(state, rowSegs, owned);
432
+ // A total verb reads nothing from what came before it (isTotal).
433
+ const next = applyVerb(touch.verb, isTotal(touch.verb) ? undefined : (0, pathOps_js_1.nativeGet)(state, rowSegs), at, rowSegs);
434
+ (0, pathOps_js_1.own)(next, owned);
435
+ placeVerb(state, rowSegs, next);
436
+ if (watched)
437
+ observe(touch, before, (0, pathOps_js_1.nativeGet)(state, segs));
438
+ }
439
+ if (kept !== -1)
440
+ generations.keep(kept, keptBefore, (0, pathOps_js_1.nativeGet)(state, segs));
441
+ return (0, pathOps_js_1.nativeGet)(state, segs);
442
+ }
443
+ exports.foldKey = foldKey;