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,286 @@
1
+ /**
2
+ * backtrack.ts — Backward causal chain analysis on the commit log.
3
+ *
4
+ * Implements **backward program slicing** (Weiser 1984, thin-slice variant):
5
+ * given a starting execution step, walk backwards through read→write
6
+ * dependencies to build the causal DAG that produced the data at that step.
7
+ *
8
+ * ## Algorithm
9
+ *
10
+ * BFS on the implicit dependency graph where edges run from reader → writer.
11
+ *
12
+ * 1. Locate startId in commitLog → root node
13
+ * 2. Get keysRead for root via `getKeysRead` callback
14
+ * 3. For each key read, find who last wrote it before this step → parent commit
15
+ * ("wrote" is the writer rule of `keyPaths.ts`, 9.33.0: a row on the key, inside it, or
16
+ * around it that changed it — so a stage that read `cfg` links to the subflow seed or
17
+ * merge-back that wrote `cfg␟b`)
18
+ * 4. Create parent CausalNode, link to root.parents
19
+ * 5. Enqueue parent. Repeat until queue empty or limits hit.
20
+ *
21
+ * Output is a **DAG** (not a linked list): a stage reading `creditScore` AND `dti`
22
+ * from different writers has two parents.
23
+ *
24
+ * ## Staged Optimization
25
+ *
26
+ * Two writer-lookup strategies, chosen automatically by commit log size:
27
+ *
28
+ * | Strategy | When | Complexity per lookup |
29
+ * |----------|------|----------------------|
30
+ * | Linear scan | N ≤ 256 | O(N) — simple backward scan |
31
+ * | Reverse index | N > 256 | O(K log N) — prebuilt key→[indices], binary search |
32
+ *
33
+ * The threshold (256) is chosen so the O(N) build cost of the reverse index
34
+ * is amortized over the BFS traversal. Below 256, linear scan wins because
35
+ * there's no index build overhead. The consumer never sees this — `causalChain()`
36
+ * picks the right strategy internally (like a query optimizer choosing between
37
+ * sequential scan vs index scan based on table size).
38
+ *
39
+ * ## Complexity
40
+ *
41
+ * - **Small logs (N ≤ 256):** O(V × K × N) total. V=visited, K=avg keys/node.
42
+ * - **Large logs (N > 256):** O(N × U) index build + O(V × K × log N) lookups.
43
+ * U = unique keys. Amortized over all BFS hops.
44
+ *
45
+ * ## References
46
+ *
47
+ * - Weiser, M. (1984). "Program Slicing." IEEE TSE.
48
+ * - Sridharan, M. et al. (2007). "Thin Slicing." PLDI.
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * import { causalChain, flattenCausalDAG, formatCausalChain } from 'foottrace';
53
+ *
54
+ * const dag = causalChain(commitLog, 'decide#2', (id) => recorder.getKeysRead(id));
55
+ * const flat = flattenCausalDAG(dag); // BFS-ordered flat list
56
+ * console.log(formatCausalChain(dag)); // human-readable
57
+ * ```
58
+ */
59
+ import type { RegisteredCode } from './honesty.js';
60
+ import type { CommitBundle, UntrackedSource } from './types.js';
61
+ /** A single node in the causal DAG. */
62
+ export interface CausalNode {
63
+ /** Unique execution step identifier. */
64
+ runtimeStageId: string;
65
+ /** Stable stage identifier. */
66
+ stageId: string;
67
+ /** Human-readable stage name. */
68
+ stageName: string;
69
+ /** Keys this stage wrote (from its CommitBundle.trace). */
70
+ keysWritten: string[];
71
+ /** The key whose read→write dependency linked this node to its child. Empty for the root. */
72
+ linkedBy: string;
73
+ /** BFS depth from the starting node (0 = start). */
74
+ depth: number;
75
+ /**
76
+ * Parent nodes — stages this node depends on. DAG: multiple parents
77
+ * possible. KEPT for compatibility; with the `controlDeps` option
78
+ * (RFC-003 D3) governing deciders appear here too. For typed/keyed/
79
+ * weighted detail use {@link parentEdges}.
80
+ */
81
+ parents: CausalNode[];
82
+ /**
83
+ * RFC-003 D3 — one edge per dependency LINK (not per parent): a node
84
+ * reading two keys from the same writer has ONE entry in {@link parents}
85
+ * but TWO `'data'` edges here. Control dependencies (via the
86
+ * `controlDeps` option) add a `'control'` edge to the governing decider.
87
+ */
88
+ parentEdges: CausalEdge[];
89
+ /**
90
+ * RFC-003 D2 honesty marker — stamped from the stage's
91
+ * `CommitBundle.untrackedSources`. Present when this stage ALSO consumed
92
+ * untracked read paths (`args` / `env` / unshadowed `silent` reads): the
93
+ * backward slice through this node may be incomplete, because those reads
94
+ * produce no read→write edge to follow. `formatCausalChain` renders this
95
+ * as a `⚠ … slice may be incomplete here` line. Absent when the stage's
96
+ * reads were fully tracked.
97
+ *
98
+ * Registry code: `'incomplete-sources'` (`memory/honesty.ts ·
99
+ * HONESTY_CODES`, served on `foottrace`) says what this field
100
+ * means. The field carries no `code` — its values are the
101
+ * {@link UntrackedSource}s.
102
+ */
103
+ incompleteSources?: ReadonlyArray<UntrackedSource>;
104
+ /**
105
+ * RFC-003 D4 truncation visibility — set on the ROOT node only, and only
106
+ * when a limit actually cut the slice: `byDepth` when a node at the
107
+ * `maxDepth` horizon still had edges to expand, `byNodes` when the
108
+ * `maxNodes` budget blocked creating a discovered parent. Absent when the
109
+ * slice is complete. `formatCausalChain` appends a `⚠ slice truncated …`
110
+ * line — a consumer must never mistake a truncated slice for a full one.
111
+ * (Until 9.48.0 dev mode also warned on the console; the field is the
112
+ * signal, read it.)
113
+ *
114
+ * Registry code: `'truncated'` (`memory/honesty.ts · HONESTY_CODES`, served
115
+ * on `foottrace`) says what this field means; the field carries no
116
+ * `code`.
117
+ */
118
+ truncated?: {
119
+ byDepth: boolean;
120
+ byNodes: boolean;
121
+ };
122
+ /**
123
+ * F4b (9.33.0) — the keys this node read that NO commit before it wrote (under the writer rule), so the
124
+ * walk has no edge to follow for them: their value came from the initial state, frozen run input or a
125
+ * closure — outside the commit log. The code says so (`'pre-run-origin'`, registered in
126
+ * `memory/honesty.ts · HONESTY_CODES`); `keys` are in the order the walk met them. ABSENT when every key
127
+ * the node expanded had a writer, so such a node keeps its 9.32.0 shape. Before 9.33.0 these reads were
128
+ * dropped without a word.
129
+ */
130
+ preRunReads?: {
131
+ code: RegisteredCode<'pre-run-origin'>;
132
+ keys: string[];
133
+ };
134
+ }
135
+ /**
136
+ * RFC-003 D3 — a typed dependency edge from a child node to one parent.
137
+ *
138
+ * - `kind: 'data'` — read→write dependency; `key` is the state key.
139
+ * - `kind: 'control'` — the parent is the decider/selector whose decision
140
+ * allowed the child to run; `key` is the decide() rule label when present.
141
+ *
142
+ * `weight` defaults to 1.0; the `weigh` hook (RFC-003 D4) can override it.
143
+ * The engine itself NEVER computes weights — semantics belong to the
144
+ * consumer-injected weigher.
145
+ */
146
+ export interface CausalEdge {
147
+ parent: CausalNode;
148
+ kind: 'data' | 'control';
149
+ key?: string;
150
+ weight: number;
151
+ /**
152
+ * F4b (9.33.0) — present only on a `'data'` edge whose parent wrote `key` ONLY through rows inside it
153
+ * (a subflow's input seed, an outputMapper merge-back, a fork child's namespace): that write made part
154
+ * of the value, and earlier writers may account for the rest. Registered as `'nested-rows'` in
155
+ * `memory/honesty.ts · HONESTY_CODES`. Absent for a write on or around the key, so an exact edge keeps
156
+ * its 9.32.0 shape.
157
+ */
158
+ basis?: RegisteredCode<'nested-rows'>;
159
+ }
160
+ /**
161
+ * RFC-003 D3 — a control dependency resolved for one execution step:
162
+ * which decider/selector execution allowed this stage to run.
163
+ */
164
+ export interface ControlDependency {
165
+ /** runtimeStageId of the governing decider/selector execution step. */
166
+ deciderId: string;
167
+ /** The decide() rule label for the chosen branch, when present. */
168
+ label?: string;
169
+ }
170
+ /**
171
+ * RFC-003 D3 — callback resolving the governing decider for an execution
172
+ * step. Return `undefined` when the step is not control-dependent on any
173
+ * recorded decision. Build one with `controlDepRecorder()` from
174
+ * `footprintjs/trace`, or supply your own.
175
+ */
176
+ export type ControlDepLookup = (runtimeStageId: string) => ControlDependency | undefined;
177
+ /**
178
+ * RFC-003 D4 — consumer-injected edge weigher. Called once per created
179
+ * edge; return a weight, or `undefined` to keep the default 1.0. The
180
+ * ENGINE never computes weights (zero new dependencies — semantics like
181
+ * embedding similarity or FDL influence belong to downstream libraries,
182
+ * the same plug-in pattern as `NarrativeFormatter`). Weights render in
183
+ * `formatCausalChain` as `← via systemPrompt (0.18)`.
184
+ */
185
+ export type EdgeWeigher = (child: CausalNode, parent: CausalNode, key: string | undefined, kind: 'data' | 'control') => number | undefined;
186
+ /** Options for causalChain(). */
187
+ export interface CausalChainOptions {
188
+ /** Maximum BFS depth (default: 20). Prevents runaway traversal. */
189
+ maxDepth?: number;
190
+ /** Maximum total nodes to visit (default: 100). Hard cap for safety. */
191
+ maxNodes?: number;
192
+ /**
193
+ * RFC-003 D3 — control-dependence lookup. When provided, expanding a node
194
+ * ALSO links a `'control'` edge to its governing decider (labeled by the
195
+ * decide() rule label when present); the decider node then expands
196
+ * normally through its own data reads, so chains like
197
+ * `status ← [control] ClassifyRisk ← [data: creditScore] PullBureau`
198
+ * resolve end-to-end. Without this option behavior is unchanged.
199
+ */
200
+ controlDeps?: ControlDepLookup;
201
+ /**
202
+ * RFC-003 D4 — edge weigher. Stamps `CausalEdge.weight` at edge creation;
203
+ * `undefined` (or no weigher) → 1.0. See {@link EdgeWeigher}.
204
+ */
205
+ weigh?: EdgeWeigher;
206
+ /**
207
+ * #P1 — how a node's expansion read-set is derived:
208
+ *
209
+ * - `'stage'` (default, historical) — every visited node expands through
210
+ * ALL of its stage's reads (`getKeysRead`). A stage reading `a,b` and
211
+ * writing `x,y` links both reads as causes of both writes — a sound but
212
+ * coarse over-approximation.
213
+ * - `'per-write'` — when the commit log carries per-write read provenance
214
+ * (`TraceEntry.readKeys`, recorded under the executor's
215
+ * `writeProvenance: 'reads-prefix'` dial), a node reached via key `k`
216
+ * expands through ONLY the reads that preceded its write of `k`
217
+ * (temporal prefix). Nodes linked later via additional keys are
218
+ * incrementally re-expanded with just the new reads (worklist — the
219
+ * slice only ever GROWS toward the stage-level ceiling). HONEST
220
+ * FALLBACK: any link whose trace entry lacks `readKeys` expands that
221
+ * node at stage level — mixed or dial-off logs degrade to `'stage'`
222
+ * behavior exactly, never to silence.
223
+ */
224
+ edgeAttribution?: 'stage' | 'per-write';
225
+ /**
226
+ * #P1 — the written keys that anchor the ROOT's expansion under
227
+ * `'per-write'` (e.g. `sliceForKey` passes the sliced key, so the root
228
+ * expands through the reads that fed THAT write, not the whole stage).
229
+ * Ignored under `'stage'`. Unset → root expands at stage level.
230
+ */
231
+ rootLinkKeys?: string[];
232
+ }
233
+ /**
234
+ * Callback that returns the keys a stage read during execution.
235
+ * The backtracker calls this for each visited node to determine
236
+ * which read→write edges to follow.
237
+ *
238
+ * Implementors: QualityRecorder tracks keysRead per step,
239
+ * or build a Map<runtimeStageId, string[]> from ScopeRecorder.onRead events.
240
+ */
241
+ export type KeysReadLookup = (runtimeStageId: string) => string[];
242
+ /**
243
+ * Build the causal DAG rooted at `startId` by walking backwards
244
+ * through read→write dependencies in the commit log.
245
+ *
246
+ * Automatically selects the optimal writer lookup strategy:
247
+ * - Linear scan for small logs (≤ 256 commits)
248
+ * - Reverse index with binary search for large logs (> 256 commits)
249
+ *
250
+ * Produces a DAG (not a tree): if two children both read from the same
251
+ * parent, the parent node is shared (deduped by runtimeStageId).
252
+ *
253
+ * @param commitLog Ordered commit bundles from executor.getSnapshot().commitLog
254
+ * @param startId runtimeStageId to start backtracking from
255
+ * @param getKeysRead Callback returning keys read by a given execution step
256
+ * @param options Depth and node limits
257
+ * @returns Root CausalNode with .parents forming the DAG, or undefined if startId not found
258
+ */
259
+ export declare function causalChain(commitLog: CommitBundle[], startId: string, getKeysRead: KeysReadLookup, options?: CausalChainOptions): CausalNode | undefined;
260
+ /**
261
+ * Flatten the causal DAG into a BFS-ordered list of nodes.
262
+ * Each node appears exactly once (first occurrence by BFS order).
263
+ * Useful for linear display or iteration.
264
+ */
265
+ export declare function flattenCausalDAG(root: CausalNode): CausalNode[];
266
+ /**
267
+ * Format a causal DAG as human-readable indented text.
268
+ * Shows the dependency chain with depth indentation and linked-by keys.
269
+ *
270
+ * RFC-003 D2: nodes that consumed untracked read paths render an extra
271
+ * `⚠ also consumed … — slice may be incomplete here` line, so a consumer
272
+ * (human or LLM) debugging from the slice is TOLD when it is incomplete.
273
+ *
274
+ * RFC-003 D3: control edges render as `← [control: <rule label>]`
275
+ * (label omitted when the decision carried none). Data rendering is
276
+ * byte-identical to the pre-D3 output — `← via <key>` from the node's
277
+ * discovery-time `linkedBy`.
278
+ *
279
+ * RFC-003 D4: edge weights from the `weigh` hook render as a suffix —
280
+ * `← via systemPrompt (0.18)` — only when ≠ 1.0, so unweighted output is
281
+ * unchanged. A truncated slice (root.truncated) appends a final
282
+ * `⚠ slice truncated …` line.
283
+ */
284
+ export declare function formatCausalChain(root: CausalNode): string;
285
+ /** @internal Exposed for testing the strategy selection. */
286
+ export declare const _REVERSE_INDEX_THRESHOLD = 256;
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Typed utilities for querying the commit log.
3
+ *
4
+ * The commitLog is an ordered array of CommitBundle — one per stage commit.
5
+ * These helpers provide type-safe queries without (b: any) casts.
6
+ */
7
+ import type { RegisteredCode } from './honesty.js';
8
+ import type { CommitBundle, CommitPhase } from './types.js';
9
+ import { type Touch } from './verbs.js';
10
+ /**
11
+ * Find the first commit by `stageId`, optionally the first one that WROTE `key` — under the writer
12
+ * rule: a row on the key, inside it, or around it that changed it (see {@link writersOf}).
13
+ */
14
+ export declare function findCommit(commitLog: CommitBundle[], stageId: string, key?: string): CommitBundle | undefined;
15
+ /** Find all commits by stageId. */
16
+ export declare function findCommits(commitLog: CommitBundle[], stageId: string): CommitBundle[];
17
+ /**
18
+ * Find the last commit that WROTE `key` before `beforeIdx` (exclusive; default: the whole log) —
19
+ * the anchor of every backward question ("who made it this?").
20
+ *
21
+ * The writer rule (`keyPaths.ts`): a commit writes `key` when one of its rows is ON the key,
22
+ * INSIDE it (a subflow's input seed writes `cfg␟a`; an outputMapper merge-back writes `cfg␟b`;
23
+ * either is a write of `cfg`), or AROUND it with the value at the key different across the commit
24
+ * (a `set` of `cfg` writes `cfg␟a`; a `merge` of `cfg` that never reaches `a` does not). A write
25
+ * found through a row INSIDE the key wrote only PART of its value — the slice readers say so with
26
+ * the `'nested-rows'` note.
27
+ *
28
+ * @param key A DELIM-joined path (`normalisePath`); a top-level key is a one-segment path.
29
+ */
30
+ export declare function findLastWriter(commitLog: CommitBundle[], key: string, beforeIdx?: number): CommitBundle | undefined;
31
+ /** What {@link writersOf} is asked. */
32
+ export interface WritersOptions {
33
+ /** The last commit ARRAY index considered, inclusive. Default: the whole log. */
34
+ readonly end?: number;
35
+ }
36
+ /**
37
+ * The ARRAY positions (ascending) of every commit in `commitLog[0..end]` that WROTE `key` — the
38
+ * writer rule of `keyPaths.ts`, the one definition every key query shares (`logModel.ts` applies it).
39
+ */
40
+ export declare function writersOf(commitLog: readonly CommitBundle[], key: string, options?: WritersOptions): number[];
41
+ /**
42
+ * Every row under `key`'s TOP-LEVEL key in `commitLog[0..end]`, in commit order — the rows the value
43
+ * rule folds ({@link foldKey}), each with its {@link Touch.relation} to `key` (absent for a sibling
44
+ * under the same top-level key: applied, not observed) — found through the log's index. A row whose
45
+ * verb is not one of the four is refused with {@link UnknownVerbError} naming the row — whether or
46
+ * not the fold would have reached it: the answer must not depend on where an optimisation starts.
47
+ */
48
+ export declare function rowsUnderRoot(commitLog: readonly CommitBundle[], key: string, end: number): Touch[];
49
+ /**
50
+ * Reconstruct the FULL value of `key` as of commit array index `idx`
51
+ * (inclusive) — the migration helper for the "read `bundle.overwrite[key]`
52
+ * as the full value written" pattern (#13c-B).
53
+ *
54
+ * The value rule (`keyPaths.ts`, 9.33.0): every row under the key's TOP-LEVEL
55
+ * key in `commitLog[0..idx]` is folded, anchored at that key's last `set` /
56
+ * `delete`, with the SAME step and clone discipline the replay uses
57
+ * (`verbs.ts` · `foldKey`), and the result is read at `key`. So the answer is
58
+ * the value `stateAt` gives at that path when it folds the log alone — rows
59
+ * INSIDE the key (a subflow seed, an outputMapper merge-back) and AROUND it (a
60
+ * `set` of the container) count; before 9.33.0 only rows on the exact path did,
61
+ * and a seeded `cfg` read as `undefined`. Under `commitValues: 'delta'`, an
62
+ * `append` row's `overwrite[key]` holds only the TAIL; the fold puts the array
63
+ * back together. Two `merge` rows of one bundle see one copy of the bundle's
64
+ * delta, as the replay's do.
65
+ *
66
+ * @param key A DELIM-joined path (`normalisePath`); a top-level key is a
67
+ * one-segment path.
68
+ * @param idx CommitBundle ARRAY index (the `bundle.idx` position),
69
+ * inclusive. NOT the executionIndex from a runtimeStageId.
70
+ * @returns The reconstructed value (a detached clone), or `undefined` when no
71
+ * row in `commitLog[0..idx]` is on, inside or around the key, or the fold
72
+ * leaves it absent (its last write was a delete). Caveat: a key that held a
73
+ * value in the run's INITIAL state folds from absent here — the log alone
74
+ * cannot see the pre-run base, so merges and nested writes onto a seeded key
75
+ * give only what they added. Since 9.17.0 the base TRAVELS with the log:
76
+ * `stateAt(snapshot, idx)` (foottrace) folds from
77
+ * `RuntimeSnapshot.initialState`, which this function never receives.
78
+ * @throws {@link UnknownVerbError} when a row under the key's top-level key
79
+ * carries a verb other than `set | merge | append | delete` — a foreign or
80
+ * corrupted log is refused, not folded as a `merge`. Engine-written logs
81
+ * never carry one.
82
+ * @see commitValueAtWithBasis — the same answer, with the codes that say WHY it is
83
+ * `undefined` or partial (and the `initialState` base this function never receives).
84
+ */
85
+ export declare function commitValueAt(commitLog: CommitBundle[], idx: number, key: string): unknown;
86
+ /**
87
+ * Why {@link commitValueAtWithBasis} answered what it did. Each code is registered, with the one
88
+ * sentence that says what it means, in `memory/honesty.ts · HONESTY_CODES` (served on
89
+ * `foottrace`); the union declares its members through `RegisteredCode`.
90
+ *
91
+ * - `'never-written'` — no commit in range wrote the key (the writer rule — the answer
92
+ * `findLastWriter` and the slice layer give), unless the run removed a value
93
+ * the passed `initialState` held (then `'deleted'`), or a redaction at or
94
+ * around the key hid the write (then `'redacted'` alone).
95
+ * - `'deleted'` — the answer is `undefined` and the key WAS written: its last write left it
96
+ * absent.
97
+ * - `'nested-rows'` — the value rests on rows INSIDE the key (a subflow seed, an outputMapper
98
+ * merge-back, a fork child's namespace) with no `set`/`delete` of the key or
99
+ * of a container around it in range.
100
+ * - `'from-initial-state'` — no `set`/`delete` of the key or around it in range, so the value rests on
101
+ * the pre-run base: folded from `options.initialState` when it holds the
102
+ * key's top-level key, partial when no `initialState` was passed.
103
+ * - `'redacted'` — a path at, inside or around the key is in the `redactedPaths` of a commit
104
+ * the value rests on (from its last `set`/`delete` on, or the whole range).
105
+ */
106
+ export type ValueBasis = RegisteredCode<'never-written' | 'deleted' | 'nested-rows' | 'from-initial-state' | 'redacted'>;
107
+ /** What {@link commitValueAtWithBasis} is asked besides the log, the index and the key. */
108
+ export interface ValueBasisOptions {
109
+ /**
110
+ * The state before the run — `RuntimeSnapshot.initialState` (or a subflow's
111
+ * `treeContext.initialState`). When given, a key with no `set`/`delete` in range folds from it, as
112
+ * `stateAt` does; when absent, such an answer is the log-only fold and carries `'from-initial-state'`.
113
+ */
114
+ readonly initialState?: Readonly<Record<string, unknown>>;
115
+ }
116
+ /** A key's value read off the log, and the codes that say what it rests on — absent codes = an exact answer. */
117
+ export interface ValueWithBasis {
118
+ readonly value: unknown;
119
+ /** In the order of {@link ValueBasis}'s list; EMPTY when a `set`/`delete` of the key or around it anchors the value and no redaction touched it. */
120
+ readonly basis: ValueBasis[];
121
+ }
122
+ /**
123
+ * {@link commitValueAt}, with the codes that say what the answer rests on. Without `options.initialState`
124
+ * the value IS `commitValueAt(commitLog, idx, key)` (pinned by a property); with it, a key that has no
125
+ * `set`/`delete` of itself or of a container around it in range folds from that base — the value
126
+ * `stateAt(snapshot, idx)` gives at the key. Refuses an unknown verb exactly as `commitValueAt` does.
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * const { value, basis } = commitValueAtWithBasis(snap.commitLog, i, 'cfg', { initialState: snap.initialState });
131
+ * for (const code of basis) console.log(code, HONESTY_CODES[code]);
132
+ * ```
133
+ */
134
+ export declare function commitValueAtWithBasis(commitLog: CommitBundle[], idx: number, key: string, options?: ValueBasisOptions): ValueWithBasis;
135
+ /**
136
+ * Why {@link findLastWriterWithBasis} answered what it did — registered in `HONESTY_CODES`:
137
+ * `'never-written'` (no writer before the bound), `'nested-rows'` (the writer reached the key only
138
+ * through rows inside it, so it wrote part of the value), `'redacted'` (no writer the log can see, because a
139
+ * redaction at or around the key hid the write — the rule `commitValueAtWithBasis` follows).
140
+ */
141
+ export type WriterBasis = RegisteredCode<'never-written' | 'nested-rows' | 'redacted'>;
142
+ /** The last writer of a key, and the codes that say what kind of write it was — absent codes = a write on or around the key. */
143
+ export interface WriterWithBasis {
144
+ /** Absent exactly when `basis` holds `'never-written'` or `'redacted'`. */
145
+ readonly writer?: CommitBundle;
146
+ readonly basis: WriterBasis[];
147
+ }
148
+ /**
149
+ * {@link findLastWriter}, with the code that says why: `writer` is `findLastWriter(commitLog, key, beforeIdx)`;
150
+ * `basis` is `['never-written']` when there is none and `['nested-rows']` when that commit wrote the key
151
+ * only through rows inside it.
152
+ */
153
+ export declare function findLastWriterWithBasis(commitLog: CommitBundle[], key: string, beforeIdx?: number): WriterWithBasis;
154
+ /**
155
+ * Position index over a commit log: `runtimeStageId` → the ARRAY INDEX of the
156
+ * FIRST bundle that stage committed. The map's insertion order is the order
157
+ * the stages first committed — execution order.
158
+ *
159
+ * "First" is the contract, and it is load-bearing. A stage normally commits
160
+ * exactly one bundle, but some executions commit more under one
161
+ * `runtimeStageId`: a subflow MOUNT (the merge-back, then the exit), a fork
162
+ * child (its own commit, then the fan-out's). Since 9.39.0 the WRITER names
163
+ * each continuation on the bundle (`CommitBundle.phase`: `'exit'` /
164
+ * `'repeat'`), and every continuation is written after the execution's own
165
+ * bundle — so "where does this stage start?" is the first bundle, and the
166
+ * grouping every reader's axis uses (`commitStops`) is this index. No reader
167
+ * tells a continuation from the log's shape.
168
+ *
169
+ * Build this ONCE when resolving many ids; use {@link commitIndexOf} for a
170
+ * single lookup.
171
+ *
172
+ * @param commitLog Ordered commit bundles — `getSnapshot().commitLog`.
173
+ * @returns A fresh Map, owned by the caller.
174
+ */
175
+ export declare function buildCommitIndex(commitLog: readonly CommitBundle[]): Map<string, number>;
176
+ /**
177
+ * Whether a log RECORDS phases — any bundle carries `phase` — i.e. it was
178
+ * written by 9.39.0 or later and holds at least one continuation. A log that
179
+ * does not either has no continuation at all, or predates the field (see
180
+ * {@link inferLegacyPhases}).
181
+ */
182
+ export declare function recordsPhases(commitLog: readonly CommitBundle[]): boolean;
183
+ /**
184
+ * THE LEGACY READER — the one place a continuation's phase is INFERRED, for a
185
+ * log written before 9.39.0 (which carries no `phase` anywhere). Returns the
186
+ * phase each bundle would have been stamped with, by the rule every reader
187
+ * applied through 9.38.0: a stage's bundle that IMMEDIATELY follows its first
188
+ * bundle is a mount's exit; any later bundle of the same `runtimeStageId` is
189
+ * a fork child's repeat; a first bundle is the stage's own (`undefined`).
190
+ *
191
+ * The rule is a guess, and the guess has a known miss — a single-child fork
192
+ * whose child is a leaf looks exactly like a mount — which is why a reader
193
+ * asks it only when the log records no phase AND it has no execution tree to
194
+ * ask instead (`commitStops`). A log that records phases is never inferred.
195
+ */
196
+ export declare function inferLegacyPhases(commitLog: readonly CommitBundle[]): (CommitPhase | undefined)[];
197
+ /**
198
+ * The ARRAY INDEX of the first commit a given `runtimeStageId` produced, or
199
+ * `-1` when that stage is not in this log (`indexOf` semantics — the name's
200
+ * promise).
201
+ *
202
+ * This is the address translation every reader needs and nobody exported: a
203
+ * `runtimeStageId` is the id an event carries, a commit index is what
204
+ * `commitValueAt` / `stateAt` / `CommitRangeIndex` speak. O(n) — for many
205
+ * lookups build the map once with {@link buildCommitIndex}.
206
+ *
207
+ * A stage that ran inside a SUBFLOW is not in the run-level log (subflows
208
+ * commit to their own isolated log); look it up in that subflow's own
209
+ * `history` instead — see `getSubtreeSnapshot`.
210
+ *
211
+ * @example
212
+ * ```typescript
213
+ * const idx = commitIndexOf(snapshot.commitLog, 'score-risk#7');
214
+ * const before = idx > 0 ? commitValueAt(snapshot.commitLog, idx - 1, 'risk') : undefined;
215
+ * ```
216
+ */
217
+ export declare function commitIndexOf(commitLog: readonly CommitBundle[], runtimeStageId: string): number;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * deltaEncoding.ts — the delta encoding's pure helpers (`commitValues:
3
+ * 'delta'`, #13c-B; docs/design/13c-b-delta-commit-verb.md), out of
4
+ * `TransactionBuffer` since 9.30.0: the overlap families of rule 3 and the
5
+ * order rows are emitted in (which now also places the admitted record's
6
+ * re-encoded families), the append detection, and the row that carries a
7
+ * value. The verb CHOICE stays in `TransactionBuffer · deltaRows`; nothing
8
+ * here replays the verb law.
9
+ */
10
+ import type { Rows } from './admission.js';
11
+ /** Op-level verbs staged into `opTrace`. `'delete'` is staged distinctly so
12
+ * delta-mode commits (#13c-B) can emit a real `delete` trace entry; under
13
+ * the default `'full'` mode it commits as `'set'` (of `undefined`) —
14
+ * byte-identical to the historical flattening. */
15
+ export type OpVerb = 'set' | 'merge' | 'delete';
16
+ /** A path that passed the net-change filter, with everything the delta
17
+ * encoder needs to decide its verb — resolved once, then reused by the
18
+ * overlap-family grouping (`toDeltaPayload` rule 3). */
19
+ export type Survivor = {
20
+ path: string;
21
+ segments: string[];
22
+ verbs: OpVerb[];
23
+ readKeys?: string[];
24
+ /** Index in `opTrace` of this path's last op (its last touch). */
25
+ last: number;
26
+ /** Value at this path when the stage began (the append/diff base). */
27
+ before: unknown;
28
+ /** Value at this path after every staged op (read-your-writes view). */
29
+ after: unknown;
30
+ };
31
+ /**
32
+ * A delta row carrying a VALUE: an `append` of the tail when `before` is a
33
+ * strict prefix of `value` ({@link isStrictArrayPrefix}), else a `set` of it.
34
+ * The value is cloned here — the one copy the record takes.
35
+ *
36
+ * `marked` — a redaction mark sits BELOW this path (`list.1.token`): its
37
+ * indices count from the start of the WHOLE array, and an `append` row holds
38
+ * only the tail, where `scrubPatch` would find no element 1 and leave the
39
+ * secret in the log and the redacted mirror. Such a path always takes the
40
+ * `set` of the whole value, where the mark lands (9.30.0; the same gap was
41
+ * open in 9.29.0 for a hard write of base + tail).
42
+ */
43
+ export declare function pushValueRow(rows: Rows, path: string, segments: string[], before: unknown, value: unknown, prov: {
44
+ readKeys: string[];
45
+ } | undefined, marked?: boolean): void;
46
+ /**
47
+ * Group survivors into OVERLAP FAMILIES (`toDeltaPayload` rule 3): each path's
48
+ * family is keyed by its SHALLOWEST surviving ancestor — walking prefixes
49
+ * shallow first means the first hit is that root, and a depth-1 path (the
50
+ * common case) never enters the loop at all. Any two paths sharing a root
51
+ * are exactly the paths whose patch storage overlaps.
52
+ */
53
+ export declare function groupIntoFamilies(survivors: Survivor[], survivingPaths: Set<string>): {
54
+ rootOf: Map<string, string>;
55
+ byRoot: Map<string, Survivor[]>;
56
+ };
57
+ /**
58
+ * Emit survivors in last-touch order, with one exception: in an overlapping
59
+ * family the ROOT is emitted LAST — its whole-subtree set is what makes the
60
+ * replayed subtree exactly the family value (a descendant applied afterwards
61
+ * could only re-state a value already inside it, but would also leave behind
62
+ * the `key: undefined` shells 'full' mode never has). `before(touch)` runs
63
+ * ahead of each survivor with that survivor's last touch — where the
64
+ * re-encoded families of the admitted record take their places.
65
+ */
66
+ export declare function emitInFamilyOrder(survivors: Survivor[], rootOf: Map<string, string>, byRoot: Map<string, Survivor[]>, emit: (s: Survivor) => void, before: (touch: number) => void): void;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * equality.ts — structural equality of committed-state values: THE owner of
3
+ * "what counts as a change". What `TransactionBuffer`'s net-change filter, the
4
+ * admitted record's compare, the read model's writer rule and the
5
+ * borrowed-mutation guard all ask. Its one import is the classifier of value
6
+ * kinds (`capture/valueKinds.ts`, a leaf); re-exported from `utils.ts`, so no
7
+ * importer moved.
8
+ */
9
+ /**
10
+ * Structural deep equality for committed-state values.
11
+ *
12
+ * Used by {@link TransactionBuffer} to decide whether a stage actually CHANGED
13
+ * a path or merely re-wrote / reverted it to the value it already held (a
14
+ * "no-op write"), and by `borrowedMutation.firstDifferingPath` to find where a
15
+ * borrowed read moved. Committed state must survive `structuredClone`, so the
16
+ * values it can hold are the kinds the clone keeps, and two values are equal
17
+ * when a record holds the same thing for both. Every kind has ONE arm below
18
+ * (`equalPairs`' switch over `ValueKind` — a kind added to the union does not
19
+ * compile until it is compared). Before 9.22.0 a Date, Map or Set was compared
20
+ * as an object with no own keys, and until 9.44.2 so were a RegExp, an Error,
21
+ * a boxed primitive, an ArrayBuffer, a DataView and a Blob: any two of a kind
22
+ * were "equal", so `$setValue('re', /y/g)` over `/x/g` committed no row and
23
+ * live state kept the OLD value.
24
+ *
25
+ * Semantics:
26
+ * - reference / identical-primitive short-circuits first (cheap fast path)
27
+ * - `NaN` equals `NaN` (primitive compare falls back to `Object.is`)
28
+ * - two values of different KINDS are never equal (a `Date` is never `{}`,
29
+ * an array never `{ 0: … }`)
30
+ * - `Date`: same `getTime()` (two invalid dates are equal)
31
+ * - `RegExp`: same `source` and `flags` (the clone resets `lastIndex`)
32
+ * - `Error`: the same restored kind (one of the six built-in error names,
33
+ * else `Error`), own `message`, `stack` and own `cause` (deep) — what the
34
+ * clone keeps; any other own field is dropped by it, so it never counts
35
+ * (the admitted record's law)
36
+ * - a boxed primitive: the same wrapper type and the same primitive inside
37
+ * - an `ArrayBuffer`: length, resizability and bytes; a typed array or a
38
+ * `DataView`: its type and the bytes IT views — never the rest of its
39
+ * buffer (a Node `Buffer` views a slice of a shared pool, and the record
40
+ * keeps the view's bytes only: `capture/freeze.ts · freezeRecord`)
41
+ * - `Map`: same size AND, per key of `a`, `b` holds a deep-equal value —
42
+ * keys by `Map` identity (`SameValueZero`), values deep
43
+ * - `Set`: same size AND every member of `a` is deep-equal to SOME member
44
+ * of `b` (order-insensitive — a Set has no order to compare by)
45
+ * - an OPAQUE value (a Blob, a `DOMException` — content this library cannot
46
+ * read): by the caller's {@link OpaqueRule} — equal only to itself by
47
+ * default (replacing one is always a change), or, when both sides are
48
+ * copies of one record, equal to any other of its kind
49
+ * - arrays: equal length AND deep-equal element-wise (order-sensitive; an
50
+ * array's expando keys are out of contract and not compared)
51
+ * - objects: identical set of keys that HOLD a value AND deep-equal per key.
52
+ * An own key whose value is `undefined` counts as ABSENT: it is this
53
+ * library's other spelling of a deleted key (`'full'` mode flattens
54
+ * `delete` into `key: undefined`; `'delta'` mode's `delete` verb removes
55
+ * the key), and every consumer — JSON, the fold, `commitValueAt`, the
56
+ * mirror — reads the two spellings as one state. So must the net-change
57
+ * filter (9.19.1): `{}` and `{ k: undefined }` are not a change. Arrays
58
+ * are untouched — a slot holding `undefined` is still a slot.
59
+ *
60
+ * Cost & safety:
61
+ * - Allocates only transient `Object.keys` arrays, an error's property
62
+ * descriptors and a byte view per compared buffer — no clones (but one
63
+ * probe clone per class prototype, remembered: `capture/valueKinds.ts ·
64
+ * kindOf`). It is strictly cheaper than the `structuredClone` the commit
65
+ * already performs.
66
+ * - Primitive comparisons (the bulk of state) are O(1) via the `===` /
67
+ * `Object.is` fast paths; only nested objects/arrays incur a walk, bounded
68
+ * by the value's own size.
69
+ * - Terminates on CYCLIC values (9.18.1): a state value must survive
70
+ * `structuredClone`, and `structuredClone` preserves cycles — so a
71
+ * self-referencing value is a legal value, not an out-of-contract one. A
72
+ * pair of objects already under comparison is treated as equal (the
73
+ * structural answer, lodash `isEqual` semantics); acyclic inputs see no
74
+ * change. Dev mode still warns about cycles at write time
75
+ * (`ScopeFacade.setValue`) because they surprise narrative and JSON.
76
+ */
77
+ export declare function deepEqual(a: any, b: any, opaque?: OpaqueRule): boolean;
78
+ /**
79
+ * How {@link deepEqual} compares two OPAQUE values (a Blob, a `DOMException` —
80
+ * content this library cannot read):
81
+ *
82
+ * - `'identity'` (the default) — equal only to itself. The answer when one
83
+ * side is a value a stage handed in: the net-change filter
84
+ * (`TransactionBuffer`), the append check (`deltaEncoding`). A new object
85
+ * is a change, because nothing can say it is not.
86
+ * - `'copies'` — equal to any other of its kind. The answer when BOTH sides
87
+ * are copies of one recorded value — a fold's generations (`logModel`,
88
+ * `elementProvenance`), a retained read against live state
89
+ * (`borrowedMutation`), the record replayed against the read-back
90
+ * (`admission`): copies never share identity, so identity would call every
91
+ * opaque value changed.
92
+ */
93
+ export type OpaqueRule = 'identity' | 'copies';