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,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* chain.ts — reading a pause/resume as ONE axis.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS FILE EXISTS. A pause and its resume produce TWO snapshots. On a
|
|
5
|
+
* cross-executor resume (the checkpoint was persisted and a fresh executor
|
|
6
|
+
* picked it up) the second run gets a fresh runtime: its `commitLog` starts at
|
|
7
|
+
* index 0 and holds only the post-resume commits, and its `initialState` is
|
|
8
|
+
* the state at the pause. Two logs, two disjoint axes — so a reader scrubbing
|
|
9
|
+
* a resumed run saw only the second half unless the application had kept the
|
|
10
|
+
* paused snapshot and stitched the halves itself. Every application that
|
|
11
|
+
* pauses would write that stitching, and none of them own the arithmetic.
|
|
12
|
+
*
|
|
13
|
+
* THE HONEST BIT, up front: COMMIT INDICES ARE RUN-LOCAL. There is no global
|
|
14
|
+
* index across a chain and this file does not invent one — `commitIdx` and
|
|
15
|
+
* `lastCommitIdx` keep indexing their own source, and every stop carries the
|
|
16
|
+
* {@link Stop.sourceIdx} they belong to. The cursor's STEPS are what run
|
|
17
|
+
* across the whole chain.
|
|
18
|
+
*
|
|
19
|
+
* AND THE REFUSAL: a chain that is not ordered, or whose legs are not from one
|
|
20
|
+
* lineage, is REFUSED rather than guessed at. The record carries TWO signals
|
|
21
|
+
* and this file reads both:
|
|
22
|
+
*
|
|
23
|
+
* - The execution index the engine stamps into every `runtimeStageId` —
|
|
24
|
+
* globally monotonic per run and, deliberately, NOT reset on resume. So a
|
|
25
|
+
* later leg's first execution index must be past the earlier leg's last, and
|
|
26
|
+
* no stage may appear in two legs. This catches a backwards chain, a fresh
|
|
27
|
+
* run (which restarts at 0) and a same-executor resume (whose one snapshot
|
|
28
|
+
* already holds both halves).
|
|
29
|
+
* - The later leg's `initialState`. On a resume the fresh runtime is seeded
|
|
30
|
+
* from the checkpoint, so that base IS the state at the pause — and the
|
|
31
|
+
* state the earlier legs FOLD TO must deep-equal it. This is what catches a
|
|
32
|
+
* leg from some OTHER lineage whose indices merely happen to be higher: the
|
|
33
|
+
* counters line up by accident, the state never does.
|
|
34
|
+
*
|
|
35
|
+
* A chart identity or a lineage id is NOT on the record, so nothing here
|
|
36
|
+
* pretends to check one. And where the state signal is not on the record
|
|
37
|
+
* either — a leg recorded before 9.17 carries no `initialState`; an earlier
|
|
38
|
+
* leg with no base folds `'log-only'`; a leg with unreadable rows folds an
|
|
39
|
+
* incomplete state — that check is SKIPPED, not faked: the chain then rests on
|
|
40
|
+
* the index checks alone, and every fold's `basis` still says what it stood
|
|
41
|
+
* on. That degradation is documented on `refuseChain`, on `timeTravel`'s
|
|
42
|
+
* `@throws`, and in the README.
|
|
43
|
+
*/
|
|
44
|
+
import { kindOf } from '../capture/valueKinds.js';
|
|
45
|
+
import { parseRuntimeStageId } from '../ids/runtimeStageId.js';
|
|
46
|
+
import { deepEqual, DELIM } from '../memory/utils.js';
|
|
47
|
+
import { foldLegs } from './stateAt.js';
|
|
48
|
+
/** The execution-index span of one leg, read off its `runtimeStageId`s. */
|
|
49
|
+
export function spanOf(log) {
|
|
50
|
+
let first;
|
|
51
|
+
let last;
|
|
52
|
+
const ids = new Set();
|
|
53
|
+
for (const bundle of log) {
|
|
54
|
+
const id = bundle.runtimeStageId;
|
|
55
|
+
if (!id)
|
|
56
|
+
continue;
|
|
57
|
+
ids.add(id);
|
|
58
|
+
const { executionIndex } = parseRuntimeStageId(id);
|
|
59
|
+
if (Number.isNaN(executionIndex))
|
|
60
|
+
continue;
|
|
61
|
+
if (first === undefined || executionIndex < first)
|
|
62
|
+
first = executionIndex;
|
|
63
|
+
if (last === undefined || executionIndex > last)
|
|
64
|
+
last = executionIndex;
|
|
65
|
+
}
|
|
66
|
+
return { first, last, ids };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Structural equality of two folded states, ignoring the paths the log was
|
|
70
|
+
* redacted at.
|
|
71
|
+
*
|
|
72
|
+
* The fold reproduces `'REDACTED'` wherever the engine scrubbed a write, while
|
|
73
|
+
* the checkpoint that seeded the resumed leg holds the real value — so those
|
|
74
|
+
* paths CANNOT agree, and are not asked to. Everything else is decided by the
|
|
75
|
+
* ONE equality law (`memory/equality.ts · deepEqual`, every value kind compared
|
|
76
|
+
* as the record holds it; 9.44.2 — this walk had a law of its own, which held
|
|
77
|
+
* any two RegExps, Errors or buffers equal). Both sides are copies of one
|
|
78
|
+
* record, so an opaque value compares as `'copies'`. Only the containers above
|
|
79
|
+
* a redacted path are walked here.
|
|
80
|
+
*/
|
|
81
|
+
function sameState(a, b, redacted, path) {
|
|
82
|
+
if (!redactedBelow(path, redacted))
|
|
83
|
+
return deepEqual(a, b, 'copies');
|
|
84
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
85
|
+
if (a.length !== b.length)
|
|
86
|
+
return false;
|
|
87
|
+
return a.every((item, i) => sameState(item, b[i], redacted, path === '' ? String(i) : `${path}${DELIM}${i}`));
|
|
88
|
+
}
|
|
89
|
+
const plain = (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && kindOf(v) === 'plain';
|
|
90
|
+
if (!plain(a) || !plain(b))
|
|
91
|
+
return deepEqual(a, b, 'copies');
|
|
92
|
+
const present = (o) => Object.keys(o).filter((k) => o[k] !== undefined);
|
|
93
|
+
const leftKeys = present(a);
|
|
94
|
+
const rightKeys = new Set(present(b));
|
|
95
|
+
if (leftKeys.length !== rightKeys.size || !leftKeys.every((k) => rightKeys.has(k)))
|
|
96
|
+
return false;
|
|
97
|
+
return leftKeys.every((k) => {
|
|
98
|
+
const childPath = path === '' ? k : `${path}${DELIM}${k}`;
|
|
99
|
+
return redacted.has(childPath) || sameState(a[k], b[k], redacted, childPath);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/** Is `path`, or a path below it, one the log was redacted at? */
|
|
103
|
+
function redactedBelow(path, redacted) {
|
|
104
|
+
if (redacted.size === 0)
|
|
105
|
+
return false;
|
|
106
|
+
if (path === '')
|
|
107
|
+
return true;
|
|
108
|
+
for (const r of redacted)
|
|
109
|
+
if (r === path || r.startsWith(`${path}${DELIM}`))
|
|
110
|
+
return true;
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Check that `legs` really are one run read in order, and say why not.
|
|
115
|
+
*
|
|
116
|
+
* Returns the refusal message, or `undefined` when the chain is sound. Three
|
|
117
|
+
* distinct failures, because they are three different mistakes, checked in
|
|
118
|
+
* this order:
|
|
119
|
+
*
|
|
120
|
+
* - REPEATED STAGE — one `runtimeStageId` appears in two legs. Ids are unique
|
|
121
|
+
* within a run, so a repeat means the same execution was recorded twice: a
|
|
122
|
+
* same-executor resume, whose snapshot ALREADY holds the whole log and needs
|
|
123
|
+
* no chaining, is the usual cause.
|
|
124
|
+
* - OUT OF ORDER — a later leg's first execution index is not past the earlier
|
|
125
|
+
* leg's last. Either the sources were handed over backwards, or the second
|
|
126
|
+
* is not a continuation at all (a fresh run restarts the counter at 0, so an
|
|
127
|
+
* unrelated snapshot fails here rather than silently interleaving).
|
|
128
|
+
* - WRONG LINEAGE — leg k's `initialState` is not the state legs 0..k-1 fold
|
|
129
|
+
* to. On a resume the later leg is seeded from the checkpoint, so its base
|
|
130
|
+
* IS the state at the pause; a leg that begins anywhere else is from some
|
|
131
|
+
* other run, however its indices happen to line up.
|
|
132
|
+
*
|
|
133
|
+
* THE LINEAGE CHECK RUNS ONLY WHERE THE RECORD ALLOWS IT, and is skipped —
|
|
134
|
+
* never faked — otherwise: leg k must carry an `initialState` (a recording
|
|
135
|
+
* made before 9.17 has none), and the fold of the legs before it must stand on
|
|
136
|
+
* a real base (`basis: 'initial+log'`) with no unreadable rows (`skipped`
|
|
137
|
+
* absent) — a log-only or gapped fold is not a state a real base may be held
|
|
138
|
+
* against. On such a chain the two index checks are the whole defence, and
|
|
139
|
+
* `foldLegs`' leg rule then CONTINUES the fold across the seam, which is the
|
|
140
|
+
* right state for a real resume and is what `basis` reports. Paths the log
|
|
141
|
+
* was redacted at are excluded from the comparison: the fold holds
|
|
142
|
+
* `'REDACTED'` there and the checkpoint holds the value.
|
|
143
|
+
*/
|
|
144
|
+
export function refuseChain(legs) {
|
|
145
|
+
if (legs.length === 0)
|
|
146
|
+
return 'timeTravel: a chain needs at least one source, got an empty array.';
|
|
147
|
+
const spans = legs.map((leg) => spanOf(leg.log));
|
|
148
|
+
const seen = new Map();
|
|
149
|
+
for (const [i, span] of spans.entries()) {
|
|
150
|
+
for (const id of span.ids) {
|
|
151
|
+
const earlier = seen.get(id);
|
|
152
|
+
if (earlier !== undefined) {
|
|
153
|
+
return (`timeTravel: sources ${earlier} and ${i} both record '${id}'. A runtimeStageId is unique ` +
|
|
154
|
+
'within a run, so these are not two legs of one run — a same-executor resume already ' +
|
|
155
|
+
'carries the whole log in ONE snapshot and must not be chained with the paused one.');
|
|
156
|
+
}
|
|
157
|
+
seen.set(id, i);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
let previous;
|
|
161
|
+
for (const [i, span] of spans.entries()) {
|
|
162
|
+
if (span.first === undefined || span.last === undefined)
|
|
163
|
+
continue;
|
|
164
|
+
if (previous && span.first <= previous.last) {
|
|
165
|
+
return (`timeTravel: source ${i} starts at execution index ${span.first}, which is not past ` +
|
|
166
|
+
`source ${previous.idx}'s last (${previous.last}). Chain the sources in run order, and ` +
|
|
167
|
+
'only sources from ONE lineage — the engine never resets the execution counter across a ' +
|
|
168
|
+
'resume, so a fresh run restarting at 0 is a different run, not a continuation.');
|
|
169
|
+
}
|
|
170
|
+
previous = { idx: i, last: span.last };
|
|
171
|
+
}
|
|
172
|
+
for (let k = 1; k < legs.length; k++) {
|
|
173
|
+
const base = legs[k].base;
|
|
174
|
+
if (!base || typeof base !== 'object')
|
|
175
|
+
continue; // no signal on the record — documented degradation
|
|
176
|
+
const before = foldLegs(legs, k - 1, Number.POSITIVE_INFINITY);
|
|
177
|
+
if (before.basis !== 'initial+log' || before.skipped)
|
|
178
|
+
continue; // not a state a real base can be held to
|
|
179
|
+
if (!sameState(before.state, base, new Set(before.redactedPaths), '')) {
|
|
180
|
+
return (`timeTravel: source ${k}'s initialState is not the state source ${k - 1} folds to. On a resume ` +
|
|
181
|
+
"the later leg's initialState IS the state at the pause, so a leg that begins anywhere else is " +
|
|
182
|
+
'not from one lineage — a different chart, or another run of this one — even when its execution ' +
|
|
183
|
+
'indices happen to be higher.');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commitStops — the one strategy foottrace ships.
|
|
3
|
+
*
|
|
4
|
+
* One stop per executed stage, in execution order, plus `'start'` / `'end'`
|
|
5
|
+
* bookends. It is the only stop grammar the substrate itself knows: the engine
|
|
6
|
+
* stamps one `runtimeStageId` per executed stage, and an empty commit is a
|
|
7
|
+
* deliberate cursor stop, not noise. Richer vocabularies (milestones, turns,
|
|
8
|
+
* tool calls) belong to the consumer that owns them — they plug in through
|
|
9
|
+
* `TimeTravelStrategy`, they do not go here.
|
|
10
|
+
*/
|
|
11
|
+
import type { CommitBundle, ExecutionTree } from '../memory/types.js';
|
|
12
|
+
import type { Stop, TimeTravelStrategy } from './types.js';
|
|
13
|
+
/**
|
|
14
|
+
* Derive one stop per executed stage from a recorded commit log.
|
|
15
|
+
*
|
|
16
|
+
* **One stop per `runtimeStageId`.** A stage can commit MORE than one bundle:
|
|
17
|
+
* a subflow mount commits its output-mapping bundle and then its EXIT; a
|
|
18
|
+
* parallel fork child is committed by the stage funnel and again by the
|
|
19
|
+
* fan-out (its REPEAT), and siblings interleave. The writer names each
|
|
20
|
+
* continuation on its bundle (`phase: 'exit' | 'repeat'`, 9.39.0), so the
|
|
21
|
+
* grouping is read, never inferred: the axis shows the stage ONCE, at its
|
|
22
|
+
* first commit ({@link buildCommitIndex}), which keeps `runtimeStageId → stop`
|
|
23
|
+
* one-to-one — the property `jumpTo` and marks depend on. The FIRST bundle per
|
|
24
|
+
* `runtimeStageId` is the stage's own and never carries a `phase`; a stage
|
|
25
|
+
* whose bundles include an `'exit'` is a `'mount'`. A continuation is normally empty
|
|
26
|
+
* (the first commit released the staging buffer); one that carries a write is
|
|
27
|
+
* folded where it sits in the log, which is when it happened.
|
|
28
|
+
*
|
|
29
|
+
* **Stops PARTITION the log.** A stop's range runs from its own first commit
|
|
30
|
+
* up to (and including) the last commit before the NEXT stop begins —
|
|
31
|
+
* `lastCommitIdx`. Every bundle therefore belongs to exactly one stop, nothing
|
|
32
|
+
* is orphaned, and `stateAt(stop)` is precisely the state that existed just
|
|
33
|
+
* before the next stop's stage started. Folding through `commitIdx` instead
|
|
34
|
+
* would silently drop a mount's output mapping.
|
|
35
|
+
*
|
|
36
|
+
* **Bookends.** `'start'` is the position before the first commit — the fold
|
|
37
|
+
* base, which no commit index can otherwise address. `'end'` is "the run is
|
|
38
|
+
* over"; it folds the whole log, the same as the last stage's stop, because
|
|
39
|
+
* "after the last stage" and "the run finished" are the same state asked as
|
|
40
|
+
* two different questions.
|
|
41
|
+
*
|
|
42
|
+
* An EMPTY log yields NO stops at all — not two bookends around nothing. A
|
|
43
|
+
* cursor with nowhere to stand says so (`Move.reason === 'empty'`).
|
|
44
|
+
*
|
|
45
|
+
* **THE SHAPE IS A CONTRACT (9.18.0).** For a non-empty log the result is
|
|
46
|
+
* `[start, …stages, end]` — `'start'` first, `'end'` last, one stop per
|
|
47
|
+
* executed stage between them, in execution order — and for an empty log it
|
|
48
|
+
* is `[]`. A strategy that COMPOSES this one (keep some stages, drop the rest)
|
|
49
|
+
* rests on that shape; it is pinned by `test/lib/time-travel/`, and
|
|
50
|
+
* {@link splitAxis} is the one guard that reads it — a composer calls that, or
|
|
51
|
+
* {@link filterStops} which calls it, instead of writing its own.
|
|
52
|
+
*/
|
|
53
|
+
export declare function commitStops(commitLog: readonly CommitBundle[], executionTree?: ExecutionTree, subflowResults?: unknown): Stop[];
|
|
54
|
+
/** `commitStops` as a {@link TimeTravelStrategy} — the default for `timeTravel`. */
|
|
55
|
+
export declare const commitStopsStrategy: TimeTravelStrategy;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* commitStops — the one strategy foottrace ships.
|
|
3
|
+
*
|
|
4
|
+
* One stop per executed stage, in execution order, plus `'start'` / `'end'`
|
|
5
|
+
* bookends. It is the only stop grammar the substrate itself knows: the engine
|
|
6
|
+
* stamps one `runtimeStageId` per executed stage, and an empty commit is a
|
|
7
|
+
* deliberate cursor stop, not noise. Richer vocabularies (milestones, turns,
|
|
8
|
+
* tool calls) belong to the consumer that owns them — they plug in through
|
|
9
|
+
* `TimeTravelStrategy`, they do not go here.
|
|
10
|
+
*/
|
|
11
|
+
import { isExecutionKey, isWithinSubflow, parseRuntimeStageId, stageIdOf } from '../ids/runtimeStageId.js';
|
|
12
|
+
import { buildCommitIndex, inferLegacyPhases, recordsPhases } from '../memory/commitLogUtils.js';
|
|
13
|
+
/**
|
|
14
|
+
* Every `runtimeStageId` in the execution tree that the engine marked as a
|
|
15
|
+
* subflow mount. Authoritative, as is a bundle's recorded `phase: 'exit'`
|
|
16
|
+
* (9.39.0) — a mount with a merge-back records its exit as a continuation. A
|
|
17
|
+
* mount without one has a single bundle, its own (no `phase`), so only the
|
|
18
|
+
* tree names it. Only a log written before 9.39.0 AND handed over without its
|
|
19
|
+
* tree falls back to the legacy inference (`inferLegacyPhases`).
|
|
20
|
+
*/
|
|
21
|
+
function mountIdsFrom(tree) {
|
|
22
|
+
const ids = new Set();
|
|
23
|
+
if (!tree)
|
|
24
|
+
return ids;
|
|
25
|
+
const work = [tree];
|
|
26
|
+
while (work.length > 0) {
|
|
27
|
+
const node = work.pop();
|
|
28
|
+
if (node.subflowId && node.runtimeStageId)
|
|
29
|
+
ids.add(node.runtimeStageId);
|
|
30
|
+
if (node.next)
|
|
31
|
+
work.push(node.next);
|
|
32
|
+
if (node.children)
|
|
33
|
+
work.push(...node.children);
|
|
34
|
+
}
|
|
35
|
+
return ids;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Derive one stop per executed stage from a recorded commit log.
|
|
39
|
+
*
|
|
40
|
+
* **One stop per `runtimeStageId`.** A stage can commit MORE than one bundle:
|
|
41
|
+
* a subflow mount commits its output-mapping bundle and then its EXIT; a
|
|
42
|
+
* parallel fork child is committed by the stage funnel and again by the
|
|
43
|
+
* fan-out (its REPEAT), and siblings interleave. The writer names each
|
|
44
|
+
* continuation on its bundle (`phase: 'exit' | 'repeat'`, 9.39.0), so the
|
|
45
|
+
* grouping is read, never inferred: the axis shows the stage ONCE, at its
|
|
46
|
+
* first commit ({@link buildCommitIndex}), which keeps `runtimeStageId → stop`
|
|
47
|
+
* one-to-one — the property `jumpTo` and marks depend on. The FIRST bundle per
|
|
48
|
+
* `runtimeStageId` is the stage's own and never carries a `phase`; a stage
|
|
49
|
+
* whose bundles include an `'exit'` is a `'mount'`. A continuation is normally empty
|
|
50
|
+
* (the first commit released the staging buffer); one that carries a write is
|
|
51
|
+
* folded where it sits in the log, which is when it happened.
|
|
52
|
+
*
|
|
53
|
+
* **Stops PARTITION the log.** A stop's range runs from its own first commit
|
|
54
|
+
* up to (and including) the last commit before the NEXT stop begins —
|
|
55
|
+
* `lastCommitIdx`. Every bundle therefore belongs to exactly one stop, nothing
|
|
56
|
+
* is orphaned, and `stateAt(stop)` is precisely the state that existed just
|
|
57
|
+
* before the next stop's stage started. Folding through `commitIdx` instead
|
|
58
|
+
* would silently drop a mount's output mapping.
|
|
59
|
+
*
|
|
60
|
+
* **Bookends.** `'start'` is the position before the first commit — the fold
|
|
61
|
+
* base, which no commit index can otherwise address. `'end'` is "the run is
|
|
62
|
+
* over"; it folds the whole log, the same as the last stage's stop, because
|
|
63
|
+
* "after the last stage" and "the run finished" are the same state asked as
|
|
64
|
+
* two different questions.
|
|
65
|
+
*
|
|
66
|
+
* An EMPTY log yields NO stops at all — not two bookends around nothing. A
|
|
67
|
+
* cursor with nowhere to stand says so (`Move.reason === 'empty'`).
|
|
68
|
+
*
|
|
69
|
+
* **THE SHAPE IS A CONTRACT (9.18.0).** For a non-empty log the result is
|
|
70
|
+
* `[start, …stages, end]` — `'start'` first, `'end'` last, one stop per
|
|
71
|
+
* executed stage between them, in execution order — and for an empty log it
|
|
72
|
+
* is `[]`. A strategy that COMPOSES this one (keep some stages, drop the rest)
|
|
73
|
+
* rests on that shape; it is pinned by `test/lib/time-travel/`, and
|
|
74
|
+
* {@link splitAxis} is the one guard that reads it — a composer calls that, or
|
|
75
|
+
* {@link filterStops} which calls it, instead of writing its own.
|
|
76
|
+
*/
|
|
77
|
+
export function commitStops(commitLog, executionTree, subflowResults) {
|
|
78
|
+
if (commitLog.length === 0)
|
|
79
|
+
return [];
|
|
80
|
+
// No tree? The snapshot's `subflowResults` still keys every mount EXECUTION
|
|
81
|
+
// by its runtimeStageId (9.39.0) — the mapper-less mounts whose one bundle
|
|
82
|
+
// carries no `phase` (fork / selector children, lazy mounts, `parallelForEach`
|
|
83
|
+
// branches) included.
|
|
84
|
+
const mountIds = executionTree ? mountIdsFrom(executionTree) : mountKeysOf(subflowResults);
|
|
85
|
+
const phases = phasesOf(commitLog, executionTree !== undefined);
|
|
86
|
+
// First commit of each distinct runtimeStageId, in execution order — the
|
|
87
|
+
// one index every reader shares (`buildCommitIndex`).
|
|
88
|
+
// A subflow's `inputMapper` seed is not one of the subflow's stages: it is
|
|
89
|
+
// the MOUNT's commit, made before any of them ran. It gets no stop of its
|
|
90
|
+
// own; it belongs to the `'start'` bookend, which is where a reader looks
|
|
91
|
+
// for "the input this chart began with" anyway. See {@link seedCommits}.
|
|
92
|
+
const seeds = seedCommits(commitLog, executionTree);
|
|
93
|
+
const firsts = [];
|
|
94
|
+
for (const [id, i] of buildCommitIndex(commitLog))
|
|
95
|
+
if (id && i >= seeds)
|
|
96
|
+
firsts.push(i);
|
|
97
|
+
const exits = new Set();
|
|
98
|
+
for (let i = seeds; i < commitLog.length; i++)
|
|
99
|
+
if (phases[i] === 'exit')
|
|
100
|
+
exits.add(commitLog[i].runtimeStageId);
|
|
101
|
+
const stops = [
|
|
102
|
+
{
|
|
103
|
+
step: 0,
|
|
104
|
+
runtimeStageId: '',
|
|
105
|
+
commitIdx: -1,
|
|
106
|
+
// Covers the subflow input seed, when the log opens with one.
|
|
107
|
+
lastCommitIdx: (firsts.length > 0 ? firsts[0] : commitLog.length) - 1,
|
|
108
|
+
stageId: '',
|
|
109
|
+
label: 'Run start',
|
|
110
|
+
kind: 'start',
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
for (let g = 0; g < firsts.length; g++) {
|
|
114
|
+
const first = firsts[g];
|
|
115
|
+
const bundle = commitLog[first];
|
|
116
|
+
// Partition boundary: everything up to the next stage's first commit.
|
|
117
|
+
const last = g + 1 < firsts.length ? firsts[g + 1] - 1 : commitLog.length - 1;
|
|
118
|
+
stops.push({
|
|
119
|
+
step: stops.length,
|
|
120
|
+
runtimeStageId: bundle.runtimeStageId,
|
|
121
|
+
commitIdx: first,
|
|
122
|
+
lastCommitIdx: last,
|
|
123
|
+
stageId: bundle.stageId,
|
|
124
|
+
subflowPath: parseRuntimeStageId(bundle.runtimeStageId).subflowPath,
|
|
125
|
+
label: bundle.stage || bundle.stageId || bundle.runtimeStageId,
|
|
126
|
+
kind: mountIds.has(bundle.runtimeStageId) || exits.has(bundle.runtimeStageId) ? 'mount' : 'commit',
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
stops.push({
|
|
130
|
+
step: stops.length,
|
|
131
|
+
runtimeStageId: '',
|
|
132
|
+
commitIdx: commitLog.length - 1,
|
|
133
|
+
lastCommitIdx: commitLog.length - 1,
|
|
134
|
+
stageId: '',
|
|
135
|
+
label: 'Run end',
|
|
136
|
+
kind: 'end',
|
|
137
|
+
});
|
|
138
|
+
return stops;
|
|
139
|
+
}
|
|
140
|
+
/** The per-execution mount keys of a snapshot's `subflowResults` (`'#'` keys; path keys skipped). */
|
|
141
|
+
function mountKeysOf(subflowResults) {
|
|
142
|
+
const ids = new Set();
|
|
143
|
+
if (subflowResults === null || typeof subflowResults !== 'object')
|
|
144
|
+
return ids;
|
|
145
|
+
for (const key of Object.keys(subflowResults))
|
|
146
|
+
if (isExecutionKey(key))
|
|
147
|
+
ids.add(key);
|
|
148
|
+
return ids;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The phase of every bundle: as RECORDED when the log records any (9.39.0+).
|
|
152
|
+
* A log that records none is either free of continuations (nothing to read)
|
|
153
|
+
* or older than the field: with its execution tree the tree names the mounts,
|
|
154
|
+
* so nothing is inferred; without it, the one legacy reader infers what
|
|
155
|
+
* 9.38.0 inferred (`inferLegacyPhases`) — a stored recording keeps its axis.
|
|
156
|
+
*/
|
|
157
|
+
function phasesOf(commitLog, haveTree) {
|
|
158
|
+
if (recordsPhases(commitLog))
|
|
159
|
+
return commitLog.map((bundle) => bundle.phase);
|
|
160
|
+
return haveTree ? [] : inferLegacyPhases(commitLog);
|
|
161
|
+
}
|
|
162
|
+
/** `commitStops` as a {@link TimeTravelStrategy} — the default for `timeTravel`. */
|
|
163
|
+
export const commitStopsStrategy = { stopsFor: commitStops };
|
|
164
|
+
/**
|
|
165
|
+
* How many commits at the head of a log are a subflow's input seed — 0 on a
|
|
166
|
+
* run's own log. The seed is `history[0]` of the subflow's log, committed by
|
|
167
|
+
* the MOUNT before any of the subflow's stages ran:
|
|
168
|
+
*
|
|
169
|
+
* - since R13 it carries the mount's runtimeStageId — and a mount's stage
|
|
170
|
+
* address (the id before `#`) IS the subflow path of every stage in the
|
|
171
|
+
* log it opens (`sub#2` opens `sub/in#3`; `outer/sub#5` opens
|
|
172
|
+
* `outer/sub/in#6`), which no stage of that log can be;
|
|
173
|
+
* - through 9.33.0 it carried `''`, which is still read the same way, so an
|
|
174
|
+
* old recording keeps its axis.
|
|
175
|
+
*
|
|
176
|
+
* A log that holds ONLY the seed (the subflow committed no stage — it paused
|
|
177
|
+
* or failed first) has no stage to compare with; the execution tree's root
|
|
178
|
+
* stands in for it: a subflow's tree is rooted at one of its own stages,
|
|
179
|
+
* whose id carries the same subflow path. Without a tree such a log reads as
|
|
180
|
+
* one stop.
|
|
181
|
+
*/
|
|
182
|
+
function seedCommits(commitLog, executionTree) {
|
|
183
|
+
const head = commitLog[0]?.runtimeStageId;
|
|
184
|
+
if (head === undefined)
|
|
185
|
+
return 0;
|
|
186
|
+
if (head === '') {
|
|
187
|
+
let n = 0;
|
|
188
|
+
while (n < commitLog.length && commitLog[n].runtimeStageId === '')
|
|
189
|
+
n++;
|
|
190
|
+
return n;
|
|
191
|
+
}
|
|
192
|
+
let next = 1;
|
|
193
|
+
while (next < commitLog.length && commitLog[next].runtimeStageId === head)
|
|
194
|
+
next++;
|
|
195
|
+
const address = stageIdOf(head);
|
|
196
|
+
if (next >= commitLog.length) {
|
|
197
|
+
const rootId = executionTree?.id;
|
|
198
|
+
return typeof rootId === 'string' && isWithinSubflow(rootId, address) ? next : 0;
|
|
199
|
+
}
|
|
200
|
+
return parseRuntimeStageId(commitLog[next].runtimeStageId).subflowPath === address ? next : 0;
|
|
201
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* time-travel/ — the reader's cursor over a finished trace.
|
|
3
|
+
*
|
|
4
|
+
* Time travel here is READ-TIME: a cursor over a Trace that has already
|
|
5
|
+
* happened, with a Fold at each stop. It is not the Walker and never becomes a
|
|
6
|
+
* second live cursor — nothing in this folder can move, re-run or mutate an
|
|
7
|
+
* execution.
|
|
8
|
+
*
|
|
9
|
+
* See README.md for the five laws, the strategy seam, and a worked example of
|
|
10
|
+
* every export.
|
|
11
|
+
*
|
|
12
|
+
* DAG position: memory ← time-travel (plus `ids/runtimeStageId`, the
|
|
13
|
+
* zero-dependency id grammar).
|
|
14
|
+
*/
|
|
15
|
+
export type { StopFilter } from './axis.js';
|
|
16
|
+
export { filterStops, splitAxis } from './axis.js';
|
|
17
|
+
export { isCommitBundle } from './bundles.js';
|
|
18
|
+
export { commitStops, commitStopsStrategy } from './commitStops.js';
|
|
19
|
+
export { stateAt } from './stateAt.js';
|
|
20
|
+
export { tagStops } from './tagStops.js';
|
|
21
|
+
export { timeTravel } from './timeTravel.js';
|
|
22
|
+
export type { AxisRefusal, AxisSplit, BookendedAxis, FoldBasis, FoldedState, FoldSource, LogGap, Mark, Move, MoveRefusal, Stop, StopKind, TimeTravel, TimeTravelOptions, TimeTravelSource, TimeTravelStrategy, } from './types.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* time-travel/ — the reader's cursor over a finished trace.
|
|
3
|
+
*
|
|
4
|
+
* Time travel here is READ-TIME: a cursor over a Trace that has already
|
|
5
|
+
* happened, with a Fold at each stop. It is not the Walker and never becomes a
|
|
6
|
+
* second live cursor — nothing in this folder can move, re-run or mutate an
|
|
7
|
+
* execution.
|
|
8
|
+
*
|
|
9
|
+
* See README.md for the five laws, the strategy seam, and a worked example of
|
|
10
|
+
* every export.
|
|
11
|
+
*
|
|
12
|
+
* DAG position: memory ← time-travel (plus `ids/runtimeStageId`, the
|
|
13
|
+
* zero-dependency id grammar).
|
|
14
|
+
*/
|
|
15
|
+
export { filterStops, splitAxis } from './axis.js';
|
|
16
|
+
export { isCommitBundle } from './bundles.js';
|
|
17
|
+
export { commitStops, commitStopsStrategy } from './commitStops.js';
|
|
18
|
+
export { stateAt } from './stateAt.js';
|
|
19
|
+
export { tagStops } from './tagStops.js';
|
|
20
|
+
export { timeTravel } from './timeTravel.js';
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stateAt — the fold at a stop.
|
|
3
|
+
*
|
|
4
|
+
* The commit log stores DIFFS. Replaying them onto the run's fold base
|
|
5
|
+
* reproduces the state as it stood after any commit — byte-for-byte the state
|
|
6
|
+
* the next stage saw, in both `commitValues` encodings, because the replay
|
|
7
|
+
* runs through `applySmartMergeInto`, the one verb law (`memory/verbs.ts`) the
|
|
8
|
+
* live commit itself uses. There is deliberately no second implementation of
|
|
9
|
+
* the verbs here.
|
|
10
|
+
*/
|
|
11
|
+
import type { CommitBundle } from '../memory/types.js';
|
|
12
|
+
import type { FoldedState, FoldSource, LogGap } from './types.js';
|
|
13
|
+
/** One source, read: its log (gaps held in place) and its fold base. */
|
|
14
|
+
export interface ReadSource {
|
|
15
|
+
readonly log: readonly CommitBundle[];
|
|
16
|
+
readonly gaps: readonly LogGap[];
|
|
17
|
+
readonly base: Record<string, unknown> | undefined;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Read whichever shape the caller handed us — `commitLog` (a run snapshot) or
|
|
21
|
+
* `history` (a subflow's own log) — narrowing the rows to commit bundles as it
|
|
22
|
+
* goes (see `bundles.ts`: a row that is not one becomes an index-holding gap).
|
|
23
|
+
*/
|
|
24
|
+
export declare function readSource(source: FoldSource | undefined): ReadSource;
|
|
25
|
+
/**
|
|
26
|
+
* Fold one chain of legs: every leg BEFORE `legIdx` in full, then `legIdx`
|
|
27
|
+
* through `through`.
|
|
28
|
+
*
|
|
29
|
+
* THE LEG RULE. A leg that carries its own `initialState` RESTARTS the
|
|
30
|
+
* accumulation from it — on a pause/resume that base IS the state at the
|
|
31
|
+
* pause, recorded by the engine, and is authoritative over anything the
|
|
32
|
+
* previous leg's log rebuilt. A leg with no base (a pre-9.17 snapshot, a
|
|
33
|
+
* hand-built log) CONTINUES from what the earlier legs folded, which is the
|
|
34
|
+
* best honest answer available and is exactly what `basis` then reports.
|
|
35
|
+
*
|
|
36
|
+
* With a single leg this is the 9.17.0 fold, unchanged.
|
|
37
|
+
*/
|
|
38
|
+
export declare function foldLegs(legs: readonly ReadSource[], legIdx: number, through: number): FoldedState;
|
|
39
|
+
/**
|
|
40
|
+
* What a fold leaves behind for the NEXT one (9.25.0): the private working
|
|
41
|
+
* copy and the facts accumulated so far. A cursor stepping forward on the
|
|
42
|
+
* same leg hands it back and only the bundles after `through` are applied —
|
|
43
|
+
* one bundle per step instead of a replay from the base. Any other ask (an
|
|
44
|
+
* earlier stop, another leg) folds from scratch; the memo is never a claim.
|
|
45
|
+
*/
|
|
46
|
+
export interface FoldMemo {
|
|
47
|
+
readonly legIdx: number;
|
|
48
|
+
readonly through: number;
|
|
49
|
+
readonly out: Record<string, unknown>;
|
|
50
|
+
readonly based: boolean;
|
|
51
|
+
readonly redactedPaths: ReadonlySet<string>;
|
|
52
|
+
readonly skipped: readonly LogGap[] | undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The fold, resumable. ONE clone per fold: the base is cloned once (or `{}`
|
|
56
|
+
* stands in), every bundle is applied INTO that copy, and the state handed
|
|
57
|
+
* out is one more clone, frozen — so the working copy stays private and a
|
|
58
|
+
* later step can continue from it. Same answer as the 9.17.0 fold, pinned by
|
|
59
|
+
* test/lib/time-travel/fold-memo.test.ts against a fresh fold at every stop.
|
|
60
|
+
*/
|
|
61
|
+
export declare function foldLegsFrom(legs: readonly ReadSource[], legIdx: number, through: number, memo?: FoldMemo): {
|
|
62
|
+
readonly folded: FoldedState;
|
|
63
|
+
readonly memo: FoldMemo;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Fold `source`'s log from its base up to and including `commitIdx`.
|
|
67
|
+
*
|
|
68
|
+
* @param source A run snapshot (`commitLog` + `initialState`) or a subflow
|
|
69
|
+
* subtree (`history` + `initialState`). Both spellings work,
|
|
70
|
+
* and the rows may be unvalidated (`unknown[]`) — a row that
|
|
71
|
+
* is not a commit bundle is skipped and REPORTED, by index,
|
|
72
|
+
* in {@link FoldedState.skipped}, never crashed on.
|
|
73
|
+
* @param commitIdx ARRAY INDEX of the last bundle to fold, inclusive. `-1`
|
|
74
|
+
* (or any negative) folds nothing and returns the base —
|
|
75
|
+
* the state before the run's first commit. Past the end
|
|
76
|
+
* clamps to the end; the result's `throughCommitIdx` reports
|
|
77
|
+
* where the fold actually stopped.
|
|
78
|
+
*
|
|
79
|
+
* @returns A detached, deeply frozen state plus how it was derived: `basis`
|
|
80
|
+
* says whether the real fold base was available, `redacted` /
|
|
81
|
+
* `redactedPaths` say where the log was scrubbed at write time, and
|
|
82
|
+
* `skipped` says which rows could not be read. The engine redacts values as
|
|
83
|
+
* it records them, so a replay of a redacted run yields `'REDACTED'` in
|
|
84
|
+
* those places — correct, and said out loud rather than presented as the
|
|
85
|
+
* real value.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```typescript
|
|
89
|
+
* const snapshot = executor.getSnapshot();
|
|
90
|
+
* const { state, basis } = stateAt(snapshot, 3); // after the 4th commit
|
|
91
|
+
* console.log(basis, state); // 'initial+log' { ... }
|
|
92
|
+
*
|
|
93
|
+
* // A subflow's own log folds identically:
|
|
94
|
+
* const inner = getSubtreeSnapshot(snapshot, 'sf-payment')!;
|
|
95
|
+
* stateAt(inner, 0).state; // after the subflow's first commit
|
|
96
|
+
*
|
|
97
|
+
* // A STORED recording needs no cast — its rows are `unknown[]`:
|
|
98
|
+
* const stored = JSON.parse(await fs.readFile(path, 'utf8'));
|
|
99
|
+
* stateAt(stored, 3).skipped; // undefined, or the bad rows
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export declare function stateAt(source: FoldSource | undefined, commitIdx: number): FoldedState;
|