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.
- package/LICENSE +21 -0
- package/README.md +73 -2
- package/dist/esm/index.d.ts +27 -0
- package/dist/esm/index.js +12 -0
- package/dist/esm/lib/capture/freeze.d.ts +91 -0
- package/dist/esm/lib/capture/freeze.js +313 -0
- package/dist/esm/lib/capture/ownData.d.ts +18 -0
- package/dist/esm/lib/capture/ownData.js +35 -0
- package/dist/esm/lib/capture/valueKinds.d.ts +57 -0
- package/dist/esm/lib/capture/valueKinds.js +141 -0
- package/dist/esm/lib/ids/runtimeStageId.d.ts +173 -0
- package/dist/esm/lib/ids/runtimeStageId.js +193 -0
- package/dist/esm/lib/memory/EventLog.d.ts +105 -0
- package/dist/esm/lib/memory/EventLog.js +147 -0
- package/dist/esm/lib/memory/RecordFrame.d.ts +232 -0
- package/dist/esm/lib/memory/RecordFrame.js +295 -0
- package/dist/esm/lib/memory/SharedMemory.d.ts +46 -0
- package/dist/esm/lib/memory/SharedMemory.js +87 -0
- package/dist/esm/lib/memory/TransactionBuffer.d.ts +521 -0
- package/dist/esm/lib/memory/TransactionBuffer.js +946 -0
- package/dist/esm/lib/memory/admission.d.ts +81 -0
- package/dist/esm/lib/memory/admission.js +152 -0
- package/dist/esm/lib/memory/backtrack.d.ts +286 -0
- package/dist/esm/lib/memory/backtrack.js +456 -0
- package/dist/esm/lib/memory/commitLogUtils.d.ts +217 -0
- package/dist/esm/lib/memory/commitLogUtils.js +383 -0
- package/dist/esm/lib/memory/deltaEncoding.d.ts +66 -0
- package/dist/esm/lib/memory/deltaEncoding.js +121 -0
- package/dist/esm/lib/memory/equality.d.ts +93 -0
- package/dist/esm/lib/memory/equality.js +261 -0
- package/dist/esm/lib/memory/eventPosition.d.ts +18 -0
- package/dist/esm/lib/memory/eventPosition.js +1 -0
- package/dist/esm/lib/memory/honesty.d.ts +93 -0
- package/dist/esm/lib/memory/honesty.js +91 -0
- package/dist/esm/lib/memory/keyPaths.d.ts +125 -0
- package/dist/esm/lib/memory/keyPaths.js +226 -0
- package/dist/esm/lib/memory/logModel.d.ts +76 -0
- package/dist/esm/lib/memory/logModel.js +363 -0
- package/dist/esm/lib/memory/merge.d.ts +26 -0
- package/dist/esm/lib/memory/merge.js +51 -0
- package/dist/esm/lib/memory/pathOps.d.ts +118 -0
- package/dist/esm/lib/memory/pathOps.js +292 -0
- package/dist/esm/lib/memory/paths.d.ts +56 -0
- package/dist/esm/lib/memory/paths.js +60 -0
- package/dist/esm/lib/memory/placeholders.d.ts +39 -0
- package/dist/esm/lib/memory/placeholders.js +39 -0
- package/dist/esm/lib/memory/recordCommit.d.ts +45 -0
- package/dist/esm/lib/memory/recordCommit.js +60 -0
- package/dist/esm/lib/memory/scrub.d.ts +52 -0
- package/dist/esm/lib/memory/scrub.js +72 -0
- package/dist/esm/lib/memory/types.d.ts +209 -0
- package/dist/esm/lib/memory/types.js +9 -0
- package/dist/esm/lib/memory/utils.d.ts +73 -0
- package/dist/esm/lib/memory/utils.js +150 -0
- package/dist/esm/lib/memory/verbs.d.ts +333 -0
- package/dist/esm/lib/memory/verbs.js +426 -0
- package/dist/esm/lib/recorder/CommitRangeIndex.d.ts +147 -0
- package/dist/esm/lib/recorder/CommitRangeIndex.js +206 -0
- package/dist/esm/lib/slice/elementProvenance.d.ts +81 -0
- package/dist/esm/lib/slice/elementProvenance.js +189 -0
- package/dist/esm/lib/slice/forwardSliceForKey.d.ts +88 -0
- package/dist/esm/lib/slice/forwardSliceForKey.js +242 -0
- package/dist/esm/lib/slice/index.d.ts +26 -0
- package/dist/esm/lib/slice/index.js +25 -0
- package/dist/esm/lib/slice/keyIndex.d.ts +137 -0
- package/dist/esm/lib/slice/keyIndex.js +292 -0
- package/dist/esm/lib/slice/keyTimeline.d.ts +49 -0
- package/dist/esm/lib/slice/keyTimeline.js +112 -0
- package/dist/esm/lib/slice/keysReadSources.d.ts +51 -0
- package/dist/esm/lib/slice/keysReadSources.js +94 -0
- package/dist/esm/lib/slice/serialize.d.ts +78 -0
- package/dist/esm/lib/slice/serialize.js +277 -0
- package/dist/esm/lib/slice/sliceForKey.d.ts +40 -0
- package/dist/esm/lib/slice/sliceForKey.js +78 -0
- package/dist/esm/lib/slice/types.d.ts +567 -0
- package/dist/esm/lib/slice/types.js +24 -0
- package/dist/esm/lib/time-travel/axis.d.ts +119 -0
- package/dist/esm/lib/time-travel/axis.js +178 -0
- package/dist/esm/lib/time-travel/bundles.d.ts +77 -0
- package/dist/esm/lib/time-travel/bundles.js +135 -0
- package/dist/esm/lib/time-travel/chain.d.ts +93 -0
- package/dist/esm/lib/time-travel/chain.js +187 -0
- package/dist/esm/lib/time-travel/commitStops.d.ts +55 -0
- package/dist/esm/lib/time-travel/commitStops.js +201 -0
- package/dist/esm/lib/time-travel/index.d.ts +22 -0
- package/dist/esm/lib/time-travel/index.js +20 -0
- package/dist/esm/lib/time-travel/stateAt.d.ts +102 -0
- package/dist/esm/lib/time-travel/stateAt.js +152 -0
- package/dist/esm/lib/time-travel/tagStops.d.ts +72 -0
- package/dist/esm/lib/time-travel/tagStops.js +101 -0
- package/dist/esm/lib/time-travel/timeTravel.d.ts +70 -0
- package/dist/esm/lib/time-travel/timeTravel.js +359 -0
- package/dist/esm/lib/time-travel/types.d.ts +470 -0
- package/dist/esm/lib/time-travel/types.js +22 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/paths.d.ts +4 -0
- package/dist/esm/paths.js +4 -0
- package/dist/esm/write.d.ts +49 -0
- package/dist/esm/write.js +47 -0
- package/dist/index.js +67 -0
- package/dist/lib/capture/freeze.js +319 -0
- package/dist/lib/capture/ownData.js +40 -0
- package/dist/lib/capture/valueKinds.js +145 -0
- package/dist/lib/ids/runtimeStageId.js +209 -0
- package/dist/lib/memory/EventLog.js +151 -0
- package/dist/lib/memory/RecordFrame.js +299 -0
- package/dist/lib/memory/SharedMemory.js +91 -0
- package/dist/lib/memory/TransactionBuffer.js +950 -0
- package/dist/lib/memory/admission.js +158 -0
- package/dist/lib/memory/backtrack.js +462 -0
- package/dist/lib/memory/commitLogUtils.js +398 -0
- package/dist/lib/memory/deltaEncoding.js +127 -0
- package/dist/lib/memory/equality.js +265 -0
- package/dist/lib/memory/eventPosition.js +2 -0
- package/dist/lib/memory/honesty.js +94 -0
- package/dist/lib/memory/keyPaths.js +242 -0
- package/dist/lib/memory/logModel.js +371 -0
- package/dist/lib/memory/merge.js +55 -0
- package/dist/lib/memory/pathOps.js +308 -0
- package/dist/lib/memory/paths.js +66 -0
- package/dist/lib/memory/placeholders.js +42 -0
- package/dist/lib/memory/recordCommit.js +64 -0
- package/dist/lib/memory/scrub.js +77 -0
- package/dist/lib/memory/types.js +10 -0
- package/dist/lib/memory/utils.js +169 -0
- package/dist/lib/memory/verbs.js +443 -0
- package/dist/lib/recorder/CommitRangeIndex.js +210 -0
- package/dist/lib/slice/elementProvenance.js +194 -0
- package/dist/lib/slice/forwardSliceForKey.js +246 -0
- package/dist/lib/slice/index.js +42 -0
- package/dist/lib/slice/keyIndex.js +312 -0
- package/dist/lib/slice/keyTimeline.js +116 -0
- package/dist/lib/slice/keysReadSources.js +100 -0
- package/dist/lib/slice/serialize.js +285 -0
- package/dist/lib/slice/sliceForKey.js +82 -0
- package/dist/lib/slice/types.js +25 -0
- package/dist/lib/time-travel/axis.js +183 -0
- package/dist/lib/time-travel/bundles.js +143 -0
- package/dist/lib/time-travel/chain.js +192 -0
- package/dist/lib/time-travel/commitStops.js +205 -0
- package/dist/lib/time-travel/index.js +31 -0
- package/dist/lib/time-travel/stateAt.js +159 -0
- package/dist/lib/time-travel/tagStops.js +105 -0
- package/dist/lib/time-travel/timeTravel.js +363 -0
- package/dist/lib/time-travel/types.js +23 -0
- package/dist/paths.js +14 -0
- package/dist/types/index.d.ts +27 -0
- package/dist/types/lib/capture/freeze.d.ts +91 -0
- package/dist/types/lib/capture/ownData.d.ts +18 -0
- package/dist/types/lib/capture/valueKinds.d.ts +57 -0
- package/dist/types/lib/ids/runtimeStageId.d.ts +173 -0
- package/dist/types/lib/memory/EventLog.d.ts +105 -0
- package/dist/types/lib/memory/RecordFrame.d.ts +232 -0
- package/dist/types/lib/memory/SharedMemory.d.ts +46 -0
- package/dist/types/lib/memory/TransactionBuffer.d.ts +521 -0
- package/dist/types/lib/memory/admission.d.ts +81 -0
- package/dist/types/lib/memory/backtrack.d.ts +286 -0
- package/dist/types/lib/memory/commitLogUtils.d.ts +217 -0
- package/dist/types/lib/memory/deltaEncoding.d.ts +66 -0
- package/dist/types/lib/memory/equality.d.ts +93 -0
- package/dist/types/lib/memory/eventPosition.d.ts +18 -0
- package/dist/types/lib/memory/honesty.d.ts +93 -0
- package/dist/types/lib/memory/keyPaths.d.ts +125 -0
- package/dist/types/lib/memory/logModel.d.ts +76 -0
- package/dist/types/lib/memory/merge.d.ts +26 -0
- package/dist/types/lib/memory/pathOps.d.ts +118 -0
- package/dist/types/lib/memory/paths.d.ts +56 -0
- package/dist/types/lib/memory/placeholders.d.ts +39 -0
- package/dist/types/lib/memory/recordCommit.d.ts +45 -0
- package/dist/types/lib/memory/scrub.d.ts +52 -0
- package/dist/types/lib/memory/types.d.ts +209 -0
- package/dist/types/lib/memory/utils.d.ts +73 -0
- package/dist/types/lib/memory/verbs.d.ts +333 -0
- package/dist/types/lib/recorder/CommitRangeIndex.d.ts +147 -0
- package/dist/types/lib/slice/elementProvenance.d.ts +81 -0
- package/dist/types/lib/slice/forwardSliceForKey.d.ts +88 -0
- package/dist/types/lib/slice/index.d.ts +26 -0
- package/dist/types/lib/slice/keyIndex.d.ts +137 -0
- package/dist/types/lib/slice/keyTimeline.d.ts +49 -0
- package/dist/types/lib/slice/keysReadSources.d.ts +51 -0
- package/dist/types/lib/slice/serialize.d.ts +78 -0
- package/dist/types/lib/slice/sliceForKey.d.ts +40 -0
- package/dist/types/lib/slice/types.d.ts +567 -0
- package/dist/types/lib/time-travel/axis.d.ts +119 -0
- package/dist/types/lib/time-travel/bundles.d.ts +77 -0
- package/dist/types/lib/time-travel/chain.d.ts +93 -0
- package/dist/types/lib/time-travel/commitStops.d.ts +55 -0
- package/dist/types/lib/time-travel/index.d.ts +22 -0
- package/dist/types/lib/time-travel/stateAt.d.ts +102 -0
- package/dist/types/lib/time-travel/tagStops.d.ts +72 -0
- package/dist/types/lib/time-travel/timeTravel.d.ts +70 -0
- package/dist/types/lib/time-travel/types.d.ts +470 -0
- package/dist/types/paths.d.ts +4 -0
- package/dist/types/write.d.ts +49 -0
- package/dist/write.js +56 -0
- package/package.json +48 -4
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verbs.ts — THE VERB LAW (L1): the one place a commit row becomes a value.
|
|
3
|
+
*
|
|
4
|
+
* A row of a commit bundle's `trace` carries one of four verbs —
|
|
5
|
+
* `set | merge | append | delete` — and ONE step turns it into a value:
|
|
6
|
+
* {@link applyVerb}. State is a left fold of that step (the event-sourcing
|
|
7
|
+
* reducer): {@link foldRows} folds one bundle's rows into a state, and
|
|
8
|
+
* {@link foldKey} folds ONE KEY across a log — every row under the key's
|
|
9
|
+
* top-level key, read at the key (the value rule, `keyPaths.ts`). Every reader
|
|
10
|
+
* of the log runs one of the two —
|
|
11
|
+
*
|
|
12
|
+
* - the live commit and the redacted mirror (`nextGeneration`),
|
|
13
|
+
* - the public replay (`applySmartMerge`), the read-side folds
|
|
14
|
+
* (`applySmartMergeInto` — `EventLog.materialise`, `stateAt`) and the
|
|
15
|
+
* admitted record's comparison (`dryFold`): all {@link foldRows},
|
|
16
|
+
* - `commitValueAt`, and `arrayProvenance` as an OBSERVER of the same fold:
|
|
17
|
+
* {@link foldKey}.
|
|
18
|
+
*
|
|
19
|
+
* Three questions that used to be answered once per reader are answered here
|
|
20
|
+
* once: what a verb does to the value before it ({@link applyVerb}), how the
|
|
21
|
+
* result is placed ({@link placeVerb}), and how the log's own values are
|
|
22
|
+
* treated on the way in (the clone discipline — a PARAMETER of the fold, see
|
|
23
|
+
* {@link Discipline} and {@link RecordedPayload}). A new verb is one new arm of
|
|
24
|
+
* `applyVerb` and one row of the traits table; the compiler refuses either
|
|
25
|
+
* missing ({@link isTotal}, {@link recordsTail}, {@link VERBS}).
|
|
26
|
+
*
|
|
27
|
+
* An UNKNOWN verb is refused with {@link UnknownVerbError}, never folded as a
|
|
28
|
+
* `merge` (the old silent default). Engine-written logs carry the four, so
|
|
29
|
+
* every engine run folds byte-for-byte as before.
|
|
30
|
+
*/
|
|
31
|
+
import type { CommitIdx } from '../ids/runtimeStageId.js';
|
|
32
|
+
import type { PathRelation } from './keyPaths.js';
|
|
33
|
+
import type { CommitBundle, MemoryPatch, TraceEntry } from './types.js';
|
|
34
|
+
/** A commit row's verb — `TraceEntry['verb']`, the union the types file declares. */
|
|
35
|
+
export type Verb = TraceEntry['verb'];
|
|
36
|
+
/** The four verbs, in the order the contract names them. */
|
|
37
|
+
export declare const VERBS: readonly Verb[];
|
|
38
|
+
/** Is `value` one of the four? The question every reader of a foreign log asks first. */
|
|
39
|
+
export declare function isVerb(value: unknown): value is Verb;
|
|
40
|
+
/**
|
|
41
|
+
* Does the verb decide the value ALONE — read nothing from what came before
|
|
42
|
+
* it? `set` and `delete`. A reader that only needs the final value may start
|
|
43
|
+
* at the last such row ({@link foldKey} `anchored`); one that tracks how the
|
|
44
|
+
* value grew (provenance) starts at the first. Pinned against
|
|
45
|
+
* {@link applyVerb} itself, so a new verb cannot misreport it.
|
|
46
|
+
*/
|
|
47
|
+
export declare function isTotal(verb: Verb): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Does the row record ONLY what it adds — a tail — rather than the whole
|
|
50
|
+
* value? `append`. What the row added is then known exactly: a reader that
|
|
51
|
+
* tracks where array elements came from attributes them to this row without
|
|
52
|
+
* comparing arrays, where every other verb has to be inferred from.
|
|
53
|
+
*/
|
|
54
|
+
export declare function recordsTail(verb: Verb): boolean;
|
|
55
|
+
/** Where in a log a refused row sits. */
|
|
56
|
+
export interface RowSite {
|
|
57
|
+
/** The row's `path`, DELIM-joined as the trace records it. */
|
|
58
|
+
readonly path: string;
|
|
59
|
+
/** The row's position in its bundle's `trace`. */
|
|
60
|
+
readonly row: number;
|
|
61
|
+
/** The bundle's position in the commit log, when the reader knows it. */
|
|
62
|
+
readonly commit?: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Thrown by the readers of a commit log — {@link foldRows} (behind
|
|
66
|
+
* `applySmartMerge`), `commitValueAt`, `arrayProvenance` — when a row carries a
|
|
67
|
+
* verb outside {@link VERBS}: a foreign or corrupted log fails loudly instead
|
|
68
|
+
* of being replayed as a `merge`. `verb`, `path`, `row` and `commit` name the
|
|
69
|
+
* row (`path` / `row` / `commit` are absent when the caller handed over a
|
|
70
|
+
* bare verb); `path` is the trace's own DELIM-joined form — take it apart with
|
|
71
|
+
* `pathSegments`. The message quotes `verb` and `path`, so a hostile log cannot
|
|
72
|
+
* reshape it.
|
|
73
|
+
*/
|
|
74
|
+
export declare class UnknownVerbError extends Error {
|
|
75
|
+
readonly verb: unknown;
|
|
76
|
+
readonly path?: string;
|
|
77
|
+
readonly row?: number;
|
|
78
|
+
readonly commit?: number;
|
|
79
|
+
constructor(verb: unknown, site?: RowSite);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* What a `delete` row leaves behind: the KEY IS GONE — which is not the value
|
|
83
|
+
* `undefined` (a root replay removes the key; a per-path fold reads it as
|
|
84
|
+
* `undefined`). {@link applyVerb} returns it, {@link placeVerb} acts on it.
|
|
85
|
+
*/
|
|
86
|
+
export declare const ABSENT: unique symbol;
|
|
87
|
+
/**
|
|
88
|
+
* One bundle's recorded payload, read by path under a clone discipline — the
|
|
89
|
+
* values {@link applyVerb} reads out of the log.
|
|
90
|
+
*
|
|
91
|
+
* `detach` — `true` for every fold that keeps its result: a `set` value or an
|
|
92
|
+
* `append` tail is a clone of what the log holds and the merge delta comes from
|
|
93
|
+
* ONE detached copy of `updates`, so the result never aliases the log. `false`
|
|
94
|
+
* only for the comparison fold ({@link dryFold}), which places recorded values
|
|
95
|
+
* as recorded and drops its result.
|
|
96
|
+
*
|
|
97
|
+
* The delta is detached ONCE per bundle, lazily — a bundle with no `merge` row
|
|
98
|
+
* pays nothing. `deepSmartMerge` dedups an array union BY REFERENCE and two
|
|
99
|
+
* `merge` rows of one bundle replay the same accumulated delta, so the rows
|
|
100
|
+
* must keep seeing the same objects; a clone per row duplicated their elements
|
|
101
|
+
* (the differential caught it in the replay; `commitValueAt` carried the bug
|
|
102
|
+
* until it folded through here). `only` narrows the copy to ONE path — a
|
|
103
|
+
* per-path fold reads nothing else, and must not pay for the rest of the
|
|
104
|
+
* bundle.
|
|
105
|
+
*/
|
|
106
|
+
export declare class RecordedPayload {
|
|
107
|
+
private readonly updates;
|
|
108
|
+
private readonly overwrite;
|
|
109
|
+
private readonly detach;
|
|
110
|
+
private readonly only?;
|
|
111
|
+
private deltas;
|
|
112
|
+
constructor(updates: MemoryPatch, overwrite: MemoryPatch, detach: boolean, only?: string[] | undefined);
|
|
113
|
+
/** What a `set` places and an `append` adds: `overwrite[path]`, cloned when detaching. */
|
|
114
|
+
value(segs: string[]): unknown;
|
|
115
|
+
/** The accumulated merge delta at `path`: `updates[path]`, from the bundle's one copy. */
|
|
116
|
+
delta(segs: string[]): unknown;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* ONE row, ONE step: the value at the row's path AFTER the row, given the
|
|
120
|
+
* value BEFORE it and the bundle's recorded payload. THE verb law — no other
|
|
121
|
+
* function in the library has an arm per verb.
|
|
122
|
+
*
|
|
123
|
+
* - `'set'` — the recorded value (`overwrite[path]`, the full final value).
|
|
124
|
+
* - `'merge'` — `deepSmartMerge` of the accumulated `updates[path]` delta onto
|
|
125
|
+
* the value before.
|
|
126
|
+
* - `'append'` — (delta mode) `overwrite[path]` holds ONLY the tail: concatenate
|
|
127
|
+
* it onto the value before. When either is not an array (an out-of-order
|
|
128
|
+
* replay base, or a REDACTED tail — `scrubPatch` replaces matched payloads
|
|
129
|
+
* with the `'REDACTED'` string) the tail BECOMES the value — the same
|
|
130
|
+
* terminal value a redacted or corrupt `set` produces.
|
|
131
|
+
* - `'delete'` — {@link ABSENT}: the key is removed. The path stays enumerated
|
|
132
|
+
* in `overwrite` (value `undefined`) for key-set consumers; the step ignores
|
|
133
|
+
* that value.
|
|
134
|
+
*
|
|
135
|
+
* Pure: it reads the payload and never writes. `verb` must be a {@link Verb}
|
|
136
|
+
* (the folds check with {@link isVerb} first and name the row); anything else
|
|
137
|
+
* that reaches it is refused with {@link UnknownVerbError}.
|
|
138
|
+
*/
|
|
139
|
+
export declare function applyVerb(verb: Verb, before: unknown, at: RecordedPayload, segs: string[]): unknown;
|
|
140
|
+
/** Put a step's value at its path in `out` — or remove the key, for {@link ABSENT}. */
|
|
141
|
+
export declare function placeVerb(out: any, segs: string[], next: unknown): void;
|
|
142
|
+
/**
|
|
143
|
+
* Is row `i` a `set` that the NEXT row sets again — same path, same verb — so
|
|
144
|
+
* that applying it is work the next row redoes?
|
|
145
|
+
*
|
|
146
|
+
* The one skip every replay iterator shares (9.22.1). A `set` row writes a
|
|
147
|
+
* clone of the recorded value at its path; the next row, a `set` of the same
|
|
148
|
+
* path, writes a clone of the SAME recorded value over it (the bundle's patch
|
|
149
|
+
* tree is fixed), and nothing runs in between — so the first write is
|
|
150
|
+
* unobservable, container creation and key order included. Only CONSECUTIVE
|
|
151
|
+
* rows qualify: a row in between (an ancestor `merge`, a sibling `set` that
|
|
152
|
+
* creates a container) can see the intermediate value, and a `merge`,
|
|
153
|
+
* `append` or `delete` next row depends on what is there. Rows are never
|
|
154
|
+
* dropped from the log — the 9.22.0 element-write funnel records N
|
|
155
|
+
* whole-array `set` rows on one path, and this is what makes replaying them
|
|
156
|
+
* O(N) instead of O(N × rows).
|
|
157
|
+
*
|
|
158
|
+
* Asked by {@link foldRows} — live state and the redacted mirror through
|
|
159
|
+
* {@link nextGeneration}, `EventLog.materialise` and `stateAt` through
|
|
160
|
+
* {@link applySmartMergeInto}, the public {@link applySmartMerge}, and the
|
|
161
|
+
* admitted record's {@link dryFold}. {@link foldKey} needs no skip: it
|
|
162
|
+
* anchors at the top-level key's LAST `set`/`delete`, or (provenance) a
|
|
163
|
+
* repeated `set` of the one recorded value changes no birth.
|
|
164
|
+
*/
|
|
165
|
+
export declare function supersededByNextSet(rows: readonly {
|
|
166
|
+
path: string;
|
|
167
|
+
verb: string;
|
|
168
|
+
}[], i: number): boolean;
|
|
169
|
+
/**
|
|
170
|
+
* How {@link foldRows} treats the state it edits and the values it reads out of
|
|
171
|
+
* the log — the clone discipline, a parameter of the ONE loop:
|
|
172
|
+
*
|
|
173
|
+
* - `'private'` — `out` is a private deep clone, edited in place; every
|
|
174
|
+
* recorded value is cloned in. The public {@link applySmartMerge}.
|
|
175
|
+
* - `'pathCopy'` — copy-on-write (9.29.0): before a row that writes THROUGH a
|
|
176
|
+
* container (a nested path), the containers on its path that this fold did
|
|
177
|
+
* not create are copied, so no row edits a container another generation
|
|
178
|
+
* (or the log) holds; each value a row creates is marked owned, so later
|
|
179
|
+
* rows of the bundle write into it in place. Every recorded value is cloned
|
|
180
|
+
* in. The live commit ({@link nextGeneration}) and the read-side folds
|
|
181
|
+
* ({@link applySmartMergeInto}).
|
|
182
|
+
* - `'byReference'` — `'pathCopy'`, but recorded values are placed AS
|
|
183
|
+
* RECORDED (no clone) and never marked owned — a later row that writes
|
|
184
|
+
* through one copies it first, so the payload is never edited. For
|
|
185
|
+
* COMPARISON only ({@link dryFold}): the result aliases the log.
|
|
186
|
+
*/
|
|
187
|
+
export type Discipline = 'private' | 'pathCopy' | 'byReference';
|
|
188
|
+
/**
|
|
189
|
+
* Fold one bundle's rows into `out`, in place: for each row (a `set` the next
|
|
190
|
+
* row sets again is skipped — {@link supersededByNextSet}) take the step
|
|
191
|
+
* ({@link applyVerb}) and put its value where it belongs ({@link placeVerb}).
|
|
192
|
+
* `out` is the caller's — see {@link Discipline} for what it must be.
|
|
193
|
+
*
|
|
194
|
+
* A row whose verb is not one of {@link VERBS} stops the fold with
|
|
195
|
+
* {@link UnknownVerbError} naming the row; the rows before it have been
|
|
196
|
+
* applied, to a state the caller discards (every in-tree caller does).
|
|
197
|
+
*/
|
|
198
|
+
export declare function foldRows(out: any, updates: MemoryPatch, overwrite: MemoryPatch, rows: readonly TraceEntry[], discipline: Discipline): any;
|
|
199
|
+
/**
|
|
200
|
+
* Applies a commit bundle to a base state by replaying operations in order.
|
|
201
|
+
* Guarantees "last writer wins" semantics.
|
|
202
|
+
*
|
|
203
|
+
* Returns a FULLY DETACHED result: the base is deep-cloned first and every
|
|
204
|
+
* value the replay writes is its own (`set` / `append` clone the recorded
|
|
205
|
+
* value; the `merge` arm reads a detached copy of `updates`), so the result
|
|
206
|
+
* shares no container with `base`, `updates` or `overwrite`. This is the
|
|
207
|
+
* public (`foottrace`) contract, unchanged by copy-on-write
|
|
208
|
+
* (9.29.0) — byte-identical to 9.28.0 for every caller. The engine's own
|
|
209
|
+
* commit does NOT come here: `SharedMemory.applyPatch` builds the next
|
|
210
|
+
* generation with `nextGeneration`, which copies only the written
|
|
211
|
+
* paths. Both run the ONE fold, {@link foldRows}; the verbs are
|
|
212
|
+
* {@link applyVerb}'s.
|
|
213
|
+
*
|
|
214
|
+
* Verb arms — see {@link applyVerb}. A row with any other verb throws
|
|
215
|
+
* {@link UnknownVerbError}.
|
|
216
|
+
*
|
|
217
|
+
* Work, not bytes (9.22.1): a `set` row the NEXT row re-sets is skipped
|
|
218
|
+
* ({@link supersededByNextSet}) — the rows stay in the log, only the clone
|
|
219
|
+
* each would have paid is not. Every consumer inherits the skip from
|
|
220
|
+
* `foldRows`; there is no second replay loop to keep in step.
|
|
221
|
+
*/
|
|
222
|
+
export declare function applySmartMerge(base: any, updates: MemoryPatch, overwrite: MemoryPatch, trace: TraceEntry[]): any;
|
|
223
|
+
/**
|
|
224
|
+
* The engine's commit (copy-on-write, 9.29.0 — docs/design/2026-10-copy-on-
|
|
225
|
+
* write-commit.md): the NEXT committed generation. A copy of `base`'s ROOT
|
|
226
|
+
* plus a copy of every container on each written path; every other subtree
|
|
227
|
+
* is SHARED with `base`, which is never edited — committed state is
|
|
228
|
+
* immutable-after-swap, so sharing is safe, and a commit costs O(what it
|
|
229
|
+
* wrote), not O(state). `SharedMemory.applyPatch` (live state and the
|
|
230
|
+
* redacted mirror) is its caller. Internal: the public replay is
|
|
231
|
+
* {@link applySmartMerge}.
|
|
232
|
+
*/
|
|
233
|
+
export declare function nextGeneration(base: any, updates: MemoryPatch, overwrite: MemoryPatch, trace: TraceEntry[]): any;
|
|
234
|
+
/**
|
|
235
|
+
* The same replay, INTO `target` — no clone of its own. For a caller that
|
|
236
|
+
* already holds a private working copy (the read-side folds: `stateAt` in
|
|
237
|
+
* `time-travel/stateAt.ts` and `EventLog.materialise` clone their base ONCE
|
|
238
|
+
* and then apply every bundle here), so a fold over N bundles costs N row
|
|
239
|
+
* applications, not N clones of the whole state. Never hand it committed
|
|
240
|
+
* state: it mutates the ROOT in place. Below the root it follows the live
|
|
241
|
+
* commit's law ({@link nextGeneration}) — a container this call did not
|
|
242
|
+
* create is copied before a write passes through it — so a fold and the live
|
|
243
|
+
* state give the same value at every path, aliased subtrees included.
|
|
244
|
+
* The values it writes are its own, so `target` never aliases the log.
|
|
245
|
+
*/
|
|
246
|
+
export declare function applySmartMergeInto(target: any, updates: MemoryPatch, overwrite: MemoryPatch, trace: TraceEntry[]): any;
|
|
247
|
+
/**
|
|
248
|
+
* The fold, for COMPARISON only (9.30.0 — the admitted record): `base` with
|
|
249
|
+
* the bundle replayed by the one verb law, copy-on-write below the root,
|
|
250
|
+
* every recorded value placed BY REFERENCE — no clone, so it costs what the
|
|
251
|
+
* bundle wrote and nothing more. It aliases the payload and every subtree of
|
|
252
|
+
* `base` the bundle did not write, and edits neither. Never hand it out,
|
|
253
|
+
* store it or write through it: `TransactionBuffer` builds one to ask whether
|
|
254
|
+
* a bundle folds back to what the stage read, then drops it.
|
|
255
|
+
*/
|
|
256
|
+
export declare function dryFold(base: any, updates: MemoryPatch, overwrite: MemoryPatch, trace: TraceEntry[]): any;
|
|
257
|
+
/**
|
|
258
|
+
* One row of the log under the folded key's TOP-LEVEL key — what {@link foldKey}
|
|
259
|
+
* applies, in commit order.
|
|
260
|
+
*/
|
|
261
|
+
export interface Touch {
|
|
262
|
+
readonly verb: Verb;
|
|
263
|
+
readonly bundle: CommitBundle;
|
|
264
|
+
/** The bundle's position in the commit log. */
|
|
265
|
+
readonly commitIdx: CommitIdx;
|
|
266
|
+
/** The row's own path (DELIM-joined `TraceEntry.path`). */
|
|
267
|
+
readonly path: string;
|
|
268
|
+
/**
|
|
269
|
+
* How the row sits against the folded key (`keyPaths · relation`). ABSENT for a
|
|
270
|
+
* row on another path under the same top-level key: such a row is APPLIED (an
|
|
271
|
+
* array union on a container above the key dedups the whole array, so a sibling
|
|
272
|
+
* changes where it leaves the key) but never observed.
|
|
273
|
+
*/
|
|
274
|
+
readonly relation?: PathRelation;
|
|
275
|
+
}
|
|
276
|
+
/** What a reader asks of {@link foldKey}. */
|
|
277
|
+
export interface KeyFold {
|
|
278
|
+
/**
|
|
279
|
+
* Start at the LAST row that sets or deletes the whole TOP-LEVEL key ({@link
|
|
280
|
+
* isTotal}, on the root path itself) — rows before it cannot change anything
|
|
281
|
+
* under that key, so a reader that wants only the value skips them. A `set` of
|
|
282
|
+
* a path below the root is NOT an anchor: an earlier sibling can still move
|
|
283
|
+
* the key through a later union. Off by default: a reader that tracks how the
|
|
284
|
+
* value GREW must see every row.
|
|
285
|
+
*/
|
|
286
|
+
readonly anchored?: boolean;
|
|
287
|
+
/**
|
|
288
|
+
* Called after each applied row that has a {@link Touch.relation} — on, inside
|
|
289
|
+
* or around the key (every row, with `everyRow`) — with the value AT THE KEY before it and after it
|
|
290
|
+
* (`undefined` for an absent key). The seam a provenance track and the writer
|
|
291
|
+
* rule hang on: they watch the fold, they never have a verb switch of their
|
|
292
|
+
* own. For a row INSIDE the key, an array `before` is a shallow snapshot: that
|
|
293
|
+
* row edits the fold's own copy in place.
|
|
294
|
+
*/
|
|
295
|
+
readonly observe?: (touch: Touch, before: unknown, after: unknown) => void;
|
|
296
|
+
/**
|
|
297
|
+
* Observe EVERY applied row, not only the key's own. The writer rule needs it: it compares the value at the
|
|
298
|
+
* key across a WHOLE commit, and a sibling under the same top-level key can move that value — a write
|
|
299
|
+
* through a container that holds a primitive replaces the container (`nativeSet`), and with it the key.
|
|
300
|
+
*/
|
|
301
|
+
readonly everyRow?: boolean;
|
|
302
|
+
/**
|
|
303
|
+
* Keep GENERATIONS at chosen commits: `at` is asked before a commit's first row (with the fold's state as
|
|
304
|
+
* it stands); for a commit it accepts, `keep` is handed the value at the key before the commit and after
|
|
305
|
+
* it — copy-on-write around that commit (the fold drops ownership of every container before and after it),
|
|
306
|
+
* so no later row edits either value. The writer rule's memoised root fold (`commitLogUtils · LogModel`)
|
|
307
|
+
* keeps the commits a key's verdict needs, once per top-level key.
|
|
308
|
+
*/
|
|
309
|
+
/**
|
|
310
|
+
* The value the top-level key holds BEFORE the first row — default: absent. Never edited: the fold copies
|
|
311
|
+
* it before writing through it. `commitValueAt` on a memoised log starts from a kept generation.
|
|
312
|
+
*/
|
|
313
|
+
readonly start?: unknown;
|
|
314
|
+
readonly generations?: {
|
|
315
|
+
at(commitIdx: number, state: Readonly<Record<string, unknown>>): boolean;
|
|
316
|
+
keep(commitIdx: number, before: unknown, after: unknown): void;
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* The value of ONE key after `touches` — EVERY row under its top-level key, in
|
|
321
|
+
* commit order, each already checked with {@link isVerb} — folded from an absent
|
|
322
|
+
* key, read at the key (`segs`, its DELIM segments): the value rule of
|
|
323
|
+
* `keyPaths.ts`, the fold `stateAt` does restricted to one top-level key. Top-level
|
|
324
|
+
* keys never interact under the step, so the restriction is exact.
|
|
325
|
+
*
|
|
326
|
+
* The same step ({@link applyVerb}) and the same discipline as {@link foldRows}'s
|
|
327
|
+
* `'pathCopy'`: `set` / `append` values are clones, the merge delta is detached
|
|
328
|
+
* once per BUNDLE (narrowed to the top-level key), so two `merge` rows of one
|
|
329
|
+
* bundle see the same objects, and a row that writes through a container this
|
|
330
|
+
* fold did not create copies it first. Returns `undefined` for an absent key.
|
|
331
|
+
* O(the rows under the key's top-level key), not O(the log).
|
|
332
|
+
*/
|
|
333
|
+
export declare function foldKey(touches: readonly Touch[], segs: string[], options?: KeyFold): unknown;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CommitRangeIndex<TLabel> — interval index over commit indices.
|
|
3
|
+
*
|
|
4
|
+
* Built incrementally during traversal: `open(label, startIdx)` when a
|
|
5
|
+
* boundary begins, `close(token, endIdx)` when it ends. Query at any
|
|
6
|
+
* commit position with `enclosing(idx)` (returns ranges containing
|
|
7
|
+
* that index, ordered outer→inner) or `overlapping(start, end)`
|
|
8
|
+
* (returns ranges intersecting a slice).
|
|
9
|
+
*
|
|
10
|
+
* See `docs/design/commit-range-index.md` for the full contract. In
|
|
11
|
+
* one paragraph: this is a generic interval data structure for
|
|
12
|
+
* commit-range queries. footprintjs owns ZERO knowledge of what
|
|
13
|
+
* labels mean — consumers (agentfootprint, lens, OTel exporters)
|
|
14
|
+
* pick their own `TLabel` type. Open ranges (mid-run, no end yet)
|
|
15
|
+
* are first-class — query results carry `endIdx: undefined` for them.
|
|
16
|
+
*
|
|
17
|
+
* Pattern: incremental builder + interval query. Same "collect during
|
|
18
|
+
* traversal, never post-process" rule footprintjs's CLAUDE.md
|
|
19
|
+
* requires of every observer.
|
|
20
|
+
* Role: structural primitive for time-travel UIs and per-boundary
|
|
21
|
+
* aggregation.
|
|
22
|
+
* Channel: consumer-driven (no engine subscription).
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* import { CommitRangeIndex } from 'foottrace';
|
|
27
|
+
*
|
|
28
|
+
* const idx = new CommitRangeIndex<string>();
|
|
29
|
+
* const t = idx.open('LLMCall', executor.getCommitCount());
|
|
30
|
+
* // ... LLM call runs, scope writes happen ...
|
|
31
|
+
* idx.close(t, executor.getCommitCount());
|
|
32
|
+
*
|
|
33
|
+
* idx.enclosing(50); // → ranges containing commit 50, outer→inner
|
|
34
|
+
* idx.overlapping(40, 60); // → ranges sharing the slice [40,60]
|
|
35
|
+
* idx.clear(); // wipe (e.g., on new run)
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* REDACTION NOTE: labels are stored verbatim and returned verbatim in
|
|
39
|
+
* query results — the index does NOT redact `TLabel` content. If a
|
|
40
|
+
* consumer attaches a label containing PII (user email, scope reads
|
|
41
|
+
* with sensitive keys, etc.) and then serializes the index for
|
|
42
|
+
* logging or telemetry, that data leaves the trust boundary. Use
|
|
43
|
+
* `RedactionPolicy` (or your own scrubbing) on the consumer side
|
|
44
|
+
* BEFORE attaching labels. The index follows the same contract as
|
|
45
|
+
* other footprintjs storage primitives (SequenceStore, KeyedStore):
|
|
46
|
+
* storage is verbatim; redaction is the caller's responsibility.
|
|
47
|
+
*/
|
|
48
|
+
/** Opaque token identifying an open range. Hold onto it; pass to `close()`.
|
|
49
|
+
* Index-scoped — using a token from one CommitRangeIndex on another is
|
|
50
|
+
* a silent no-op (verified by the per-index `_owner` symbol).
|
|
51
|
+
*
|
|
52
|
+
* SECURITY NOTE: the `_owner` symbol is enumerable on the token object
|
|
53
|
+
* via `Object.getOwnPropertySymbols(token)`. This means tokens are NOT
|
|
54
|
+
* adversary-safe — a malicious caller with access to ANY token from
|
|
55
|
+
* this index can recover the owner symbol and forge new tokens. The
|
|
56
|
+
* index is designed for in-process trust boundaries (cooperative
|
|
57
|
+
* recorders sharing one runner), not for hostile-input scenarios.
|
|
58
|
+
* If adversary-safety becomes a requirement, switch to a WeakMap-
|
|
59
|
+
* scoped token model (see security panel review Y2). */
|
|
60
|
+
export interface RangeToken {
|
|
61
|
+
/** Per-index sequential id. Opaque to consumers — they shouldn't read it. */
|
|
62
|
+
readonly _id: number;
|
|
63
|
+
/** Per-index identity — prevents accidental cross-index token misuse. */
|
|
64
|
+
readonly _owner: symbol;
|
|
65
|
+
}
|
|
66
|
+
/** A single range as returned by query methods. Frozen-shape — readonly fields. */
|
|
67
|
+
export interface RangeEntry<TLabel> {
|
|
68
|
+
readonly label: TLabel;
|
|
69
|
+
readonly startIdx: number;
|
|
70
|
+
/** Undefined while the range is still open (mid-run boundary). */
|
|
71
|
+
readonly endIdx?: number;
|
|
72
|
+
}
|
|
73
|
+
export declare class CommitRangeIndex<TLabel> {
|
|
74
|
+
private entries;
|
|
75
|
+
private byId;
|
|
76
|
+
private nextId;
|
|
77
|
+
/** Identity for token scoping — each index gets a fresh symbol so
|
|
78
|
+
* tokens from one index can't accidentally close ranges in another.
|
|
79
|
+
* ROTATED on `clear()` to invalidate stale tokens that survived a
|
|
80
|
+
* run reset (would otherwise hit a recycled id and silently mutate
|
|
81
|
+
* a different range — see DS+logic panel review RED #1). */
|
|
82
|
+
private owner;
|
|
83
|
+
/**
|
|
84
|
+
* Open a new range. Returns a token the caller MUST hold and pass
|
|
85
|
+
* to `close()` later. Each `open()` gets a fresh token; tokens
|
|
86
|
+
* cannot be reused or shared across indices (silent no-op if
|
|
87
|
+
* misused — see Law 2 in the design doc). Tokens from BEFORE
|
|
88
|
+
* the most recent `clear()` are also invalid (owner symbol
|
|
89
|
+
* rotates on clear).
|
|
90
|
+
*/
|
|
91
|
+
open(label: TLabel, startIdx: number): RangeToken;
|
|
92
|
+
/**
|
|
93
|
+
* Close an open range at `endIdx` (inclusive). After close, the
|
|
94
|
+
* range is queryable with both bounds. Closing an already-closed
|
|
95
|
+
* token is a no-op. Closing an unknown token (from another index,
|
|
96
|
+
* or fabricated) is a no-op.
|
|
97
|
+
*/
|
|
98
|
+
close(token: RangeToken, endIdx: number): void;
|
|
99
|
+
/**
|
|
100
|
+
* Returns ALL ranges enclosing `commitIdx`, ordered outer→inner.
|
|
101
|
+
* Includes both closed and open ranges. For a closed range to
|
|
102
|
+
* enclose: `startIdx <= commitIdx <= endIdx`. For an open range:
|
|
103
|
+
* `startIdx <= commitIdx` (no upper bound check).
|
|
104
|
+
*
|
|
105
|
+
* Ordering rule: ascending by `startIdx`, with TIES BROKEN BY
|
|
106
|
+
* descending `endIdx` (wider range = outer). Open ranges (endIdx
|
|
107
|
+
* undefined) are treated as `+Infinity` for tie-break — they
|
|
108
|
+
* always sort outer of any closed range starting at the same idx.
|
|
109
|
+
* This is the only deterministic outer→inner ordering when two
|
|
110
|
+
* boundaries open at the same commit (e.g., Parallel root +
|
|
111
|
+
* its first branch).
|
|
112
|
+
*
|
|
113
|
+
* Returns a SHALLOW IMMUTABLE COPY — caller mutations don't affect
|
|
114
|
+
* internal state.
|
|
115
|
+
*/
|
|
116
|
+
enclosing(commitIdx: number): readonly RangeEntry<TLabel>[];
|
|
117
|
+
/**
|
|
118
|
+
* Returns all ranges OVERLAPPING the slice `[startIdx, endIdx]`. A
|
|
119
|
+
* range overlaps if it shares at least one commit position with the
|
|
120
|
+
* slice. Use for parallel-branch detection, time-window queries,
|
|
121
|
+
* or "what boundaries fired during this slice."
|
|
122
|
+
*
|
|
123
|
+
* Sorted by the SAME outer→inner comparator as `enclosing()`:
|
|
124
|
+
* ascending by `startIdx`, ties broken by descending `endIdx`
|
|
125
|
+
* (wider = outer; open ranges treated as +Infinity).
|
|
126
|
+
*
|
|
127
|
+
* Returns a SHALLOW IMMUTABLE COPY.
|
|
128
|
+
*/
|
|
129
|
+
overlapping(startIdx: number, endIdx: number): readonly RangeEntry<TLabel>[];
|
|
130
|
+
/** Total range count (open + closed). */
|
|
131
|
+
get size(): number;
|
|
132
|
+
/**
|
|
133
|
+
* Wipe all ranges + reset the token counter AND rotate the owner
|
|
134
|
+
* symbol so any token from before this clear becomes invalid.
|
|
135
|
+
* Critical: without rotating the owner, a stale token whose `_id`
|
|
136
|
+
* happens to match a recycled id after clear would silently mutate
|
|
137
|
+
* the wrong entry. Owner rotation makes stale-token close a no-op
|
|
138
|
+
* via the `_owner !== this.owner` guard.
|
|
139
|
+
*
|
|
140
|
+
* Call from a consumer's runId guard when a new run starts (e.g.,
|
|
141
|
+
* agentfootprint's `observeRunId(onNewRun)` from Phase 2).
|
|
142
|
+
*/
|
|
143
|
+
clear(): void;
|
|
144
|
+
/** O(1) lookup by token id. Always returns the entry that the token
|
|
145
|
+
* references (or undefined if the id was never opened in this index). */
|
|
146
|
+
private findById;
|
|
147
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* slice/elementProvenance.ts — APPEND-FOLD PROVENANCE.
|
|
3
|
+
*
|
|
4
|
+
* THE PROBLEM (why this file exists): in agent charts, almost all dataflow
|
|
5
|
+
* funnels through ONE array key (`history`). A key-level slice on it
|
|
6
|
+
* degenerates to "everything depends on history" — true and useless. The
|
|
7
|
+
* question that actually triages an agent run is element-level:
|
|
8
|
+
* "which stage produced history[7]?".
|
|
9
|
+
*
|
|
10
|
+
* THE INSIGHT: the commit log already contains the answer. Under
|
|
11
|
+
* `commitValues: 'delta'` every array growth is recorded as an `append` verb
|
|
12
|
+
* whose `overwrite[key]` holds exactly the new tail — each element has an
|
|
13
|
+
* explicit birth record. Under `'full'` mode, push-style growth appears as
|
|
14
|
+
* consecutive `set`s where the previous array is a strict prefix of the next
|
|
15
|
+
* — the tail is attributable by inference. No new capture is needed; this is
|
|
16
|
+
* a pure post-hoc query.
|
|
17
|
+
*
|
|
18
|
+
* THE ALGORITHM (append-fold): WATCH the per-key verb fold — the SAME fold
|
|
19
|
+
* `commitValueAt` runs (`memory/verbs.ts` · `foldKey`: one step, `applyVerb`,
|
|
20
|
+
* for every verb; this file has no verb switch of its own) — while carrying a
|
|
21
|
+
* births array kept index-aligned with the value. The observer is told, after
|
|
22
|
+
* each row on, inside or around the key, the value at the key before it and
|
|
23
|
+
* after it, and keeps the births — for the commits that WROTE the key under
|
|
24
|
+
* the writer rule (`commitLogUtils · writersOf`; since 9.33.0 a write inside
|
|
25
|
+
* the key or around it counts, as it does for every key query): a row that
|
|
26
|
+
* records only its tail (`append`) earns an exact attribution; every other row
|
|
27
|
+
* is a whole-value transition attributed by inference. One difference from
|
|
28
|
+
* `commitValueAt`: that helper ANCHORS at the latest `set`/`delete` as a skip
|
|
29
|
+
* optimization (earlier commits cannot change the final VALUE). Provenance
|
|
30
|
+
* must fold from the FIRST touch, because full-mode growth is a chain of
|
|
31
|
+
* `set`s and the anchor would erase every birth but the last. The final value
|
|
32
|
+
* is identical either way (the fold is deterministic left-to-right) — a
|
|
33
|
+
* property test pins this equivalence.
|
|
34
|
+
*
|
|
35
|
+
* INVARIANT (maintained on every branch): when the folded value is an array,
|
|
36
|
+
* `births.length === value.length` and `births[i]` describes `value[i]`.
|
|
37
|
+
*
|
|
38
|
+
* HONESTY: every birth is labeled with how it was determined
|
|
39
|
+
* ({@link AttributionBasis}) — `'append-verb'` is engine-recorded truth,
|
|
40
|
+
* `'prefix-inference'` is a heuristic (a wholesale replacement that happens
|
|
41
|
+
* to share the old prefix is indistinguishable from an append), and
|
|
42
|
+
* `'whole-value'` is an explicit reset. Absence is honest too: a missing
|
|
43
|
+
* provenance carries a {@link MissingProvenanceReason}, mirroring
|
|
44
|
+
* `VariableSlice.missing` — one absence pattern module-wide.
|
|
45
|
+
*
|
|
46
|
+
* COMPLEXITY: delta-mode logs need no equality checks — O(total elements).
|
|
47
|
+
* Full-mode logs pay a strict-prefix check (deepEqual per element) per
|
|
48
|
+
* full-value touch: O(touches × length) element comparisons worst case.
|
|
49
|
+
* Post-hoc query, off the hot path — acceptable; measured in the perf tests.
|
|
50
|
+
*/
|
|
51
|
+
import type { CommitBundle } from '../memory/types.js';
|
|
52
|
+
import type { ArrayProvenance, ElementBirth, StateKey } from './types.js';
|
|
53
|
+
/**
|
|
54
|
+
* Element-level provenance for one array-valued key: fold the key's commits
|
|
55
|
+
* and return index-aligned birth records for every element.
|
|
56
|
+
*
|
|
57
|
+
* @param key A {@link StateKey}: the top-level key string, or a path array
|
|
58
|
+
* for nested keys (normalised internally — no engine delimiters needed).
|
|
59
|
+
* @param options.atIdx Inclusive commit array index to fold to (default: the
|
|
60
|
+
* whole log). NOT the executionIndex from a runtimeStageId.
|
|
61
|
+
* @returns Always an {@link ArrayProvenance}; on failure `missing` says why
|
|
62
|
+
* (`'not-an-array'` for scalar/deleted/degraded keys — those are
|
|
63
|
+
* `sliceForKey` territory). Blind spot shared with the whole commit-log
|
|
64
|
+
* family: elements present in the run's INITIAL state (seeded, never
|
|
65
|
+
* re-set in range) are invisible here.
|
|
66
|
+
*/
|
|
67
|
+
export declare function arrayProvenance(commitLog: CommitBundle[], key: StateKey, options?: {
|
|
68
|
+
atIdx?: number;
|
|
69
|
+
}): ArrayProvenance;
|
|
70
|
+
/**
|
|
71
|
+
* Convenience over {@link arrayProvenance}: the birth of ONE element.
|
|
72
|
+
* Returns `undefined` when the key has no array provenance (any
|
|
73
|
+
* {@link MissingProvenanceReason}) or `index` is out of range at `atIdx` —
|
|
74
|
+
* Map.get-like semantics; use {@link arrayProvenance} directly when you need
|
|
75
|
+
* the missing reason.
|
|
76
|
+
*
|
|
77
|
+
* @see ElementBirth
|
|
78
|
+
*/
|
|
79
|
+
export declare function elementProvenance(commitLog: CommitBundle[], key: StateKey, index: number, options?: {
|
|
80
|
+
atIdx?: number;
|
|
81
|
+
}): ElementBirth | undefined;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* slice/forwardSliceForKey.ts — the FORWARD half of variable-first slicing.
|
|
3
|
+
*
|
|
4
|
+
* `sliceForKey` answers "why is `key` what it is?" by walking backward from
|
|
5
|
+
* the value to its causes. This file answers the opposite question, which no
|
|
6
|
+
* query answered before: **"who READ this value, and what did it FEED?"**
|
|
7
|
+
* (the production shape: *who read `recipeId`, and what did it feed?* — one
|
|
8
|
+
* query, not a manual scan of a trace viewer).
|
|
9
|
+
*
|
|
10
|
+
* ## The algorithm: LIVE RANGES, not steps
|
|
11
|
+
*
|
|
12
|
+
* A backward slice hops writer→writer. Forward, the unit is a value's LIFE:
|
|
13
|
+
*
|
|
14
|
+
* 1. Anchor at a write of `key` — the SAME anchor idiom as `sliceForKey`
|
|
15
|
+
* (the last writer, or the last writer before `before`).
|
|
16
|
+
* 2. That value lives until the key's NEXT write (`nextWriteIdx`). Every
|
|
17
|
+
* stage that READ `key` inside that window saw THIS value — a `read`.
|
|
18
|
+
* (9.33.0: a write that reached the key only through paths INSIDE it — a
|
|
19
|
+
* subflow seed, an outputMapper merge-back — changed part of the value
|
|
20
|
+
* and does NOT end its life: a reader after it still read the rest. Every
|
|
21
|
+
* life such a write starts or crosses carries a `'nested-rows'` note.)
|
|
22
|
+
* 3. A reading stage that also WROTE something carried the value onward —
|
|
23
|
+
* a `fed` edge to that write, which is itself the start of a new life.
|
|
24
|
+
* 4. Descend into fed lives breadth-first (visited-set guarded, budgeted).
|
|
25
|
+
*
|
|
26
|
+
* ### LAW — the live range is `(writeIdx, nextWriteIdx]`
|
|
27
|
+
*
|
|
28
|
+
* Open at the bottom, CLOSED at the top, and both ends follow from the
|
|
29
|
+
* engine's event order (`onRead` fires PRE-commit — see CLAUDE.md):
|
|
30
|
+
* - a read by the writing stage itself (`readIdx === writeIdx`) saw the
|
|
31
|
+
* PREVIOUS value: read-modify-write reads belong to the previous life;
|
|
32
|
+
* - a read by the OVERWRITING stage (`readIdx === nextWriteIdx`) also fired
|
|
33
|
+
* before its commit, so it still saw THIS value.
|
|
34
|
+
* A read after that commit attributes to the NEW write, never the old one.
|
|
35
|
+
* (Granularity limit, stated because it cannot be seen: a stage that writes
|
|
36
|
+
* `key` and then re-reads it within the SAME stage read its own value; at
|
|
37
|
+
* commit-index resolution that read is indistinguishable from the one that
|
|
38
|
+
* opened the stage.)
|
|
39
|
+
*
|
|
40
|
+
* ### LAW — a `fed` edge is EXACT only under recorded read provenance
|
|
41
|
+
*
|
|
42
|
+
* With `writeProvenance: 'reads-prefix'` on, the child write's
|
|
43
|
+
* `TraceEntry.readKeys` names the keys read before it: this key IN that list
|
|
44
|
+
* is an exact edge (`basis: 'per-write'`), and this key ABSENT from it is an
|
|
45
|
+
* exact *exclusion* — no edge at all. With the dial off there is no such
|
|
46
|
+
* evidence, only "the stage read this and wrote that": the edge is stamped
|
|
47
|
+
* `basis: 'stage'` and the slice carries a `'conservative-fed-edges'` note.
|
|
48
|
+
* A conservative edge is never presented as an exact one.
|
|
49
|
+
*
|
|
50
|
+
* Budgets mirror `causalChain`'s (maxDepth 20, maxNodes 100) and a cut is
|
|
51
|
+
* STATED — `root.truncated` plus a `'truncated'` note.
|
|
52
|
+
*
|
|
53
|
+
* DAG position: memory ← slice. Imports memory/ only (see README.md).
|
|
54
|
+
*/
|
|
55
|
+
import type { KeysReadLookup } from '../memory/backtrack.js';
|
|
56
|
+
import type { CommitBundle } from '../memory/types.js';
|
|
57
|
+
import type { ForwardSlice, KeysReadSource, StateKey } from './types.js';
|
|
58
|
+
/** Options for {@link forwardSliceForKey} — anchoring plus walk budgets. */
|
|
59
|
+
export interface ForwardSliceForKeyOptions {
|
|
60
|
+
/**
|
|
61
|
+
* Exclusive commit-array-index upper bound for the ANCHOR search: follow
|
|
62
|
+
* the value as it stood BEFORE this idx. Same contract as
|
|
63
|
+
* `sliceForKey`'s `before` / `findLastWriter`'s `beforeIdx`.
|
|
64
|
+
*
|
|
65
|
+
* It bounds the anchor ONLY — the walk then runs forward past it, because
|
|
66
|
+
* "what did the value at step 12 go on to feed?" is the whole question.
|
|
67
|
+
*/
|
|
68
|
+
before?: number;
|
|
69
|
+
/** Maximum BFS depth in value-lives (default: 20 — causalChain's). */
|
|
70
|
+
maxDepth?: number;
|
|
71
|
+
/** Maximum total nodes (default: 100 — causalChain's). */
|
|
72
|
+
maxNodes?: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Forward slice for one state key: who read this value, and what did it feed?
|
|
76
|
+
*
|
|
77
|
+
* @param commitLog Ordered commit bundles — `getSnapshot().commitLog`.
|
|
78
|
+
* @param key A {@link StateKey}: the plain key string, or a path array
|
|
79
|
+
* (normalised through the SAME `normaliseStateKey` the backward door uses,
|
|
80
|
+
* so both doors accept identical inputs).
|
|
81
|
+
* @param keysRead A {@link KeysReadSource} strategy or a bare lookup — the
|
|
82
|
+
* same reads providers `sliceForKey` takes. Reads are not in the commit
|
|
83
|
+
* log; without a provider a forward walk can see nothing, which the
|
|
84
|
+
* `'reads-not-recorded'` note says out loud.
|
|
85
|
+
* @returns Always a {@link ForwardSlice}. Absence is a RESULT with a reason
|
|
86
|
+
* and, for an unknown key, a bounded list of the keys the log does know.
|
|
87
|
+
*/
|
|
88
|
+
export declare function forwardSliceForKey(commitLog: CommitBundle[], key: StateKey, keysRead: KeysReadSource | KeysReadLookup, options?: ForwardSliceForKeyOptions): ForwardSlice;
|