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,159 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* stateAt — the fold at a stop.
|
|
4
|
+
*
|
|
5
|
+
* The commit log stores DIFFS. Replaying them onto the run's fold base
|
|
6
|
+
* reproduces the state as it stood after any commit — byte-for-byte the state
|
|
7
|
+
* the next stage saw, in both `commitValues` encodings, because the replay
|
|
8
|
+
* runs through `applySmartMergeInto`, the one verb law (`memory/verbs.ts`) the
|
|
9
|
+
* live commit itself uses. There is deliberately no second implementation of
|
|
10
|
+
* the verbs here.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.stateAt = exports.foldLegsFrom = exports.foldLegs = exports.readSource = void 0;
|
|
14
|
+
const freeze_js_1 = require("../capture/freeze.js");
|
|
15
|
+
const utils_js_1 = require("../memory/utils.js");
|
|
16
|
+
const bundles_js_1 = require("./bundles.js");
|
|
17
|
+
/**
|
|
18
|
+
* Read whichever shape the caller handed us — `commitLog` (a run snapshot) or
|
|
19
|
+
* `history` (a subflow's own log) — narrowing the rows to commit bundles as it
|
|
20
|
+
* goes (see `bundles.ts`: a row that is not one becomes an index-holding gap).
|
|
21
|
+
*/
|
|
22
|
+
function readSource(source) {
|
|
23
|
+
const base = source?.initialState;
|
|
24
|
+
if (!source)
|
|
25
|
+
return { log: [], gaps: [], base };
|
|
26
|
+
const withLog = source;
|
|
27
|
+
const raw = Array.isArray(withLog.commitLog)
|
|
28
|
+
? withLog.commitLog
|
|
29
|
+
: Array.isArray(source.history)
|
|
30
|
+
? source.history
|
|
31
|
+
: undefined;
|
|
32
|
+
const { log, gaps } = (0, bundles_js_1.readLog)(raw);
|
|
33
|
+
return { log, gaps, base };
|
|
34
|
+
}
|
|
35
|
+
exports.readSource = readSource;
|
|
36
|
+
/**
|
|
37
|
+
* Fold one chain of legs: every leg BEFORE `legIdx` in full, then `legIdx`
|
|
38
|
+
* through `through`.
|
|
39
|
+
*
|
|
40
|
+
* THE LEG RULE. A leg that carries its own `initialState` RESTARTS the
|
|
41
|
+
* accumulation from it — on a pause/resume that base IS the state at the
|
|
42
|
+
* pause, recorded by the engine, and is authoritative over anything the
|
|
43
|
+
* previous leg's log rebuilt. A leg with no base (a pre-9.17 snapshot, a
|
|
44
|
+
* hand-built log) CONTINUES from what the earlier legs folded, which is the
|
|
45
|
+
* best honest answer available and is exactly what `basis` then reports.
|
|
46
|
+
*
|
|
47
|
+
* With a single leg this is the 9.17.0 fold, unchanged.
|
|
48
|
+
*/
|
|
49
|
+
function foldLegs(legs, legIdx, through) {
|
|
50
|
+
return foldLegsFrom(legs, legIdx, through).folded;
|
|
51
|
+
}
|
|
52
|
+
exports.foldLegs = foldLegs;
|
|
53
|
+
/** Apply `log[from + 1 .. end]` of one leg into `out`, collecting the leg's facts. */
|
|
54
|
+
function applyLeg(out, leg, from, end, redactedPaths, skipped) {
|
|
55
|
+
for (const gap of leg.gaps) {
|
|
56
|
+
if (gap.index > from && gap.index <= end)
|
|
57
|
+
(skipped ??= []).push(gap);
|
|
58
|
+
}
|
|
59
|
+
for (let i = from + 1; i <= end; i++) {
|
|
60
|
+
const bundle = leg.log[i];
|
|
61
|
+
if (!bundle)
|
|
62
|
+
continue;
|
|
63
|
+
for (const path of bundle.redactedPaths ?? [])
|
|
64
|
+
redactedPaths.add(path);
|
|
65
|
+
(0, utils_js_1.applySmartMergeInto)(out, bundle.updates, bundle.overwrite, bundle.trace);
|
|
66
|
+
}
|
|
67
|
+
return skipped;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The fold, resumable. ONE clone per fold: the base is cloned once (or `{}`
|
|
71
|
+
* stands in), every bundle is applied INTO that copy, and the state handed
|
|
72
|
+
* out is one more clone, frozen — so the working copy stays private and a
|
|
73
|
+
* later step can continue from it. Same answer as the 9.17.0 fold, pinned by
|
|
74
|
+
* test/lib/time-travel/fold-memo.test.ts against a fresh fold at every stop.
|
|
75
|
+
*/
|
|
76
|
+
function foldLegsFrom(legs, legIdx, through, memo) {
|
|
77
|
+
const last = legs[legIdx];
|
|
78
|
+
const endOnLeg = last ? Math.min(through, last.log.length - 1) : -1;
|
|
79
|
+
const lastThrough = endOnLeg < -1 ? -1 : endOnLeg;
|
|
80
|
+
const resumable = memo !== undefined && memo.legIdx === legIdx && lastThrough >= memo.through;
|
|
81
|
+
let based = resumable ? memo.based : false;
|
|
82
|
+
let out = resumable ? memo.out : {};
|
|
83
|
+
const redactedPaths = new Set(resumable ? memo.redactedPaths : []);
|
|
84
|
+
let skipped = resumable && memo.skipped ? [...memo.skipped] : undefined;
|
|
85
|
+
if (resumable) {
|
|
86
|
+
if (last)
|
|
87
|
+
skipped = applyLeg(out, last, memo.through, lastThrough, redactedPaths, skipped);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
for (let leg = 0; leg <= legIdx && leg < legs.length; leg++) {
|
|
91
|
+
const source = legs[leg];
|
|
92
|
+
if (source.base && typeof source.base === 'object') {
|
|
93
|
+
out = structuredClone(source.base);
|
|
94
|
+
based = true;
|
|
95
|
+
}
|
|
96
|
+
const end = leg === legIdx ? lastThrough : source.log.length - 1;
|
|
97
|
+
skipped = applyLeg(out, source, -1, end, redactedPaths, skipped);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const basis = based ? 'initial+log' : 'log-only';
|
|
101
|
+
const folded = {
|
|
102
|
+
// The one freeze walk (`capture/freeze.ts`): a typed array in state is skipped, not thrown on.
|
|
103
|
+
state: (0, freeze_js_1.deepFreeze)(structuredClone(out)),
|
|
104
|
+
basis,
|
|
105
|
+
redacted: redactedPaths.size > 0,
|
|
106
|
+
redactedPaths: Object.freeze([...redactedPaths].sort()),
|
|
107
|
+
throughCommitIdx: lastThrough,
|
|
108
|
+
...(skipped ? { skipped: Object.freeze(skipped) } : {}),
|
|
109
|
+
};
|
|
110
|
+
return { folded, memo: { legIdx, through: lastThrough, out, based, redactedPaths, skipped } };
|
|
111
|
+
}
|
|
112
|
+
exports.foldLegsFrom = foldLegsFrom;
|
|
113
|
+
/**
|
|
114
|
+
* Fold `source`'s log from its base up to and including `commitIdx`.
|
|
115
|
+
*
|
|
116
|
+
* @param source A run snapshot (`commitLog` + `initialState`) or a subflow
|
|
117
|
+
* subtree (`history` + `initialState`). Both spellings work,
|
|
118
|
+
* and the rows may be unvalidated (`unknown[]`) — a row that
|
|
119
|
+
* is not a commit bundle is skipped and REPORTED, by index,
|
|
120
|
+
* in {@link FoldedState.skipped}, never crashed on.
|
|
121
|
+
* @param commitIdx ARRAY INDEX of the last bundle to fold, inclusive. `-1`
|
|
122
|
+
* (or any negative) folds nothing and returns the base —
|
|
123
|
+
* the state before the run's first commit. Past the end
|
|
124
|
+
* clamps to the end; the result's `throughCommitIdx` reports
|
|
125
|
+
* where the fold actually stopped.
|
|
126
|
+
*
|
|
127
|
+
* @returns A detached, deeply frozen state plus how it was derived: `basis`
|
|
128
|
+
* says whether the real fold base was available, `redacted` /
|
|
129
|
+
* `redactedPaths` say where the log was scrubbed at write time, and
|
|
130
|
+
* `skipped` says which rows could not be read. The engine redacts values as
|
|
131
|
+
* it records them, so a replay of a redacted run yields `'REDACTED'` in
|
|
132
|
+
* those places — correct, and said out loud rather than presented as the
|
|
133
|
+
* real value.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```typescript
|
|
137
|
+
* const snapshot = executor.getSnapshot();
|
|
138
|
+
* const { state, basis } = stateAt(snapshot, 3); // after the 4th commit
|
|
139
|
+
* console.log(basis, state); // 'initial+log' { ... }
|
|
140
|
+
*
|
|
141
|
+
* // A subflow's own log folds identically:
|
|
142
|
+
* const inner = getSubtreeSnapshot(snapshot, 'sf-payment')!;
|
|
143
|
+
* stateAt(inner, 0).state; // after the subflow's first commit
|
|
144
|
+
*
|
|
145
|
+
* // A STORED recording needs no cast — its rows are `unknown[]`:
|
|
146
|
+
* const stored = JSON.parse(await fs.readFile(path, 'utf8'));
|
|
147
|
+
* stateAt(stored, 3).skipped; // undefined, or the bad rows
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
function stateAt(source, commitIdx) {
|
|
151
|
+
// `NaN` is out of contract; treat it as "fold nothing" rather than letting
|
|
152
|
+
// it flow through the arithmetic into `throughCommitIdx`, where it would
|
|
153
|
+
// serialize as `null` and tell a consumer nothing about where the fold
|
|
154
|
+
// stopped. (±Infinity needs no special case: it clamps like any index past
|
|
155
|
+
// either end.)
|
|
156
|
+
const asked = Number.isNaN(commitIdx) ? -1 : Math.floor(commitIdx);
|
|
157
|
+
return foldLegs([readSource(source)], 0, asked);
|
|
158
|
+
}
|
|
159
|
+
exports.stateAt = stateAt;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* tagStops — the stops a chart DECLARED, read back off its commit log.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS FILE EXISTS. `commitStops` is the only stop grammar the substrate
|
|
6
|
+
* knows: one stop per executed stage. Every richer axis — milestones, turns,
|
|
7
|
+
* tool calls — was a consumer's classifier over `runtimeStageId`: a switch
|
|
8
|
+
* that parses `#` and `/`, lives outside the recording, and goes stale when
|
|
9
|
+
* a stage is renamed. Since 9.21.0 the author can put NAMES on a stage at
|
|
10
|
+
* build time (`FlowChartBuilder.tag` / `options.tags`) and the engine stamps
|
|
11
|
+
* them on the stage's commit bundle (`CommitBundle.tags`). This strategy
|
|
12
|
+
* reads them back: a stored recording from ANY chart scrubs by its own
|
|
13
|
+
* milestones, with no id conventions in the reader.
|
|
14
|
+
*
|
|
15
|
+
* WHAT IT IS. One expression over `filterStops(commitStops(...))`: keep the
|
|
16
|
+
* stops whose FIRST bundle carries a tag (any of the asked-for ones, or any
|
|
17
|
+
* tag at all when none are asked for), give the survivors the log back, and
|
|
18
|
+
* carry the bundle's whole tag array as `Stop.meta`. Everything a filtering
|
|
19
|
+
* axis owes — the bookend guard, the re-partition so a dropped stage's
|
|
20
|
+
* commits fold into the tagged stop BEFORE them, `prologue` on a start that
|
|
21
|
+
* absorbed stages — comes from `filterStops`, stated once, in `axis.ts`.
|
|
22
|
+
*
|
|
23
|
+
* WHAT IT IS NOT. Not a second vocabulary: the strings are the consumer's,
|
|
24
|
+
* carried verbatim. Not a derivation: a stage with no declared tag gets no
|
|
25
|
+
* stop here, however recognisable its id — a reader that wants ids classified
|
|
26
|
+
* writes that keep rule itself (`filterStops`), or reads the fold
|
|
27
|
+
* (`stateAt`) and keeps by what changed. And not a re-walk: only a tag the
|
|
28
|
+
* engine actually stamped can put a stop on this axis.
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.tagStops = void 0;
|
|
32
|
+
const axis_js_1 = require("./axis.js");
|
|
33
|
+
const commitStops_js_1 = require("./commitStops.js");
|
|
34
|
+
/**
|
|
35
|
+
* The tags a stop's first bundle carries, or `undefined` when it carries
|
|
36
|
+
* none. Reads the bundle at `stop.commitIdx` — the FIRST commit of the
|
|
37
|
+
* stage, which is where `StageContext.commit` records the stamp (a fork
|
|
38
|
+
* child's repeat and a mount's exit — the bundles with a `phase` — carry
|
|
39
|
+
* none).
|
|
40
|
+
*/
|
|
41
|
+
function tagsAt(log, stop) {
|
|
42
|
+
const raw = log[stop.commitIdx]?.tags;
|
|
43
|
+
// A stored row is `unknown[]`-shaped until narrowed: keep only the strings,
|
|
44
|
+
// so a damaged row can never put a non-name into `Stop.meta`.
|
|
45
|
+
const tags = Array.isArray(raw) ? raw.filter((t) => typeof t === 'string') : [];
|
|
46
|
+
return tags.length > 0 ? tags : undefined;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A {@link TimeTravelStrategy} whose stops are the stages that carry a
|
|
50
|
+
* declared tag.
|
|
51
|
+
*
|
|
52
|
+
* **Keep rule: any-of.** A stop survives when its bundle's `tags` shares at
|
|
53
|
+
* least one name with `tags`; with `tags` omitted (or empty), every tagged
|
|
54
|
+
* stop survives. All-of, or a rule over more than the tags, is a
|
|
55
|
+
* `filterStops` call away — the library owns no vocabulary, so it owns no
|
|
56
|
+
* richer rule either.
|
|
57
|
+
*
|
|
58
|
+
* **Attribution by precedence.** An untagged stage's commits fold into the
|
|
59
|
+
* tagged stop BEFORE it (what `filterStops` does); the commits before the
|
|
60
|
+
* first tagged stage are absorbed by `'start'`, which then says
|
|
61
|
+
* `prologue: true`. A run with no tagged stage yields the two bookends only.
|
|
62
|
+
*
|
|
63
|
+
* **`meta` is the array.** Each kept stop carries its bundle's full `tags`
|
|
64
|
+
* (not just the ones asked for), so a reader that filtered on
|
|
65
|
+
* `'milestone:llm-turn'` can still see the stage was also `'audit'`.
|
|
66
|
+
*
|
|
67
|
+
* **The equivalence law.** `stateAt(stop)` at every kept stop equals
|
|
68
|
+
* `commitStops`'s fold at the same commit index — a filtering axis moves the
|
|
69
|
+
* stops, never the log.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* import { flowChart } from 'footprintjs';
|
|
74
|
+
* import { tagStops, timeTravel } from 'foottrace';
|
|
75
|
+
*
|
|
76
|
+
* const chart = flowChart<State>('Seed', seedFn, 'seed')
|
|
77
|
+
* .addFunction('Call model', callFn, 'call-llm')
|
|
78
|
+
* .tag('milestone:llm-turn')
|
|
79
|
+
* .addFunction('Route', routeFn, 'route')
|
|
80
|
+
* .tag('milestone:decision')
|
|
81
|
+
* .build();
|
|
82
|
+
*
|
|
83
|
+
* // …run it, keep the snapshot…
|
|
84
|
+
* const cursor = timeTravel(snapshot, { strategy: tagStops(['milestone:llm-turn']) });
|
|
85
|
+
* cursor.stops.map((s) => s.label); // ['Run start', 'Call model', 'Run end']
|
|
86
|
+
* cursor.stops[1].meta; // ['milestone:llm-turn']
|
|
87
|
+
* cursor.stops[0].prologue; // true — 'seed' ran before the first turn
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
function tagStops(tags) {
|
|
91
|
+
const wanted = tags && tags.length > 0 ? new Set(tags) : undefined;
|
|
92
|
+
return {
|
|
93
|
+
stopsFor(commitLog, executionTree, subflowResults) {
|
|
94
|
+
return (0, axis_js_1.filterStops)((0, commitStops_js_1.commitStops)(commitLog, executionTree, subflowResults), (stop) => {
|
|
95
|
+
const stamped = tagsAt(commitLog, stop);
|
|
96
|
+
if (!stamped)
|
|
97
|
+
return null;
|
|
98
|
+
if (wanted && !stamped.some((tag) => wanted.has(tag)))
|
|
99
|
+
return null;
|
|
100
|
+
return { meta: stamped };
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
exports.tagStops = tagStops;
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* timeTravel — a reader's cursor over a finished trace.
|
|
4
|
+
*
|
|
5
|
+
* The five laws this file implements (and the folder README states with an
|
|
6
|
+
* example each):
|
|
7
|
+
*
|
|
8
|
+
* 1. ONE cursor. This object holds the only position. `drill()` returns a
|
|
9
|
+
* separate cursor over a separate log, never a second position here.
|
|
10
|
+
* 2. A miss never moves. Every refusal returns the reason and leaves the
|
|
11
|
+
* cursor exactly where it was.
|
|
12
|
+
* 3. A fold result is detached. `stateAt` returns frozen, cloned state that
|
|
13
|
+
* shares nothing with the engine.
|
|
14
|
+
* 4. Marks live beside the log. They are the reader's notes, never written
|
|
15
|
+
* into the run's record.
|
|
16
|
+
* 5. Stops are DERIVED from the recorded log — read-time queries over
|
|
17
|
+
* collected data, never a re-walk that invents what was not recorded.
|
|
18
|
+
*
|
|
19
|
+
* A CHAIN (9.18.0) does not add a sixth law and does not bend these five: it
|
|
20
|
+
* is still one cursor, over sources that are still only read. What it adds is
|
|
21
|
+
* that the STEPS run across a pause/resume — see `chain.ts` for the ordering
|
|
22
|
+
* and lineage checks, and for why commit indices stay run-local.
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.timeTravel = void 0;
|
|
26
|
+
const runtimeStageId_js_1 = require("../ids/runtimeStageId.js");
|
|
27
|
+
const bundles_js_1 = require("./bundles.js");
|
|
28
|
+
const chain_js_1 = require("./chain.js");
|
|
29
|
+
const commitStops_js_1 = require("./commitStops.js");
|
|
30
|
+
const stateAt_js_1 = require("./stateAt.js");
|
|
31
|
+
class Cursor {
|
|
32
|
+
stops;
|
|
33
|
+
path;
|
|
34
|
+
mountRuntimeStageId;
|
|
35
|
+
parent;
|
|
36
|
+
sourceCount;
|
|
37
|
+
legs;
|
|
38
|
+
sources;
|
|
39
|
+
options;
|
|
40
|
+
_marks;
|
|
41
|
+
step;
|
|
42
|
+
/** The last fold's working copy, so a step forward applies one bundle (9.25.0). */
|
|
43
|
+
foldMemo;
|
|
44
|
+
constructor(sources, options, path, parent, mountRuntimeStageId) {
|
|
45
|
+
this.sources = sources;
|
|
46
|
+
this.options = options;
|
|
47
|
+
this.path = path;
|
|
48
|
+
this.parent = parent;
|
|
49
|
+
this.mountRuntimeStageId = mountRuntimeStageId;
|
|
50
|
+
this.legs = sources.map((source) => (0, stateAt_js_1.readSource)(source));
|
|
51
|
+
this.sourceCount = sources.length;
|
|
52
|
+
// The shipped strategy has no vocabulary of its own (`meta` is never set),
|
|
53
|
+
// so it serves any `TMeta` a caller asked for.
|
|
54
|
+
const strategy = options.strategy ?? commitStops_js_1.commitStopsStrategy;
|
|
55
|
+
// ONE CALL PER SOURCE, with that source's own log and tree: a strategy
|
|
56
|
+
// never has to know it is part of a chain, and its indices stay local to
|
|
57
|
+
// the leg it was handed.
|
|
58
|
+
const perSource = this.legs.map((leg, i) => strategy.stopsFor(leg.log, (0, bundles_js_1.readTree)(sources[i].executionTree), sources[i].subflowResults));
|
|
59
|
+
this.stops = Object.freeze(sources.length === 1 ? perSource[0] : chainStops(perSource));
|
|
60
|
+
this._marks = [...(options.marks ?? [])];
|
|
61
|
+
this.step = 0;
|
|
62
|
+
}
|
|
63
|
+
// ── Position ─────────────────────────────────────────────────────────────
|
|
64
|
+
at() {
|
|
65
|
+
return this.stops[this.step];
|
|
66
|
+
}
|
|
67
|
+
moveTo(step) {
|
|
68
|
+
const from = this.at();
|
|
69
|
+
this.step = step;
|
|
70
|
+
return { moved: true, from, to: this.stops[step] };
|
|
71
|
+
}
|
|
72
|
+
/** Law 2: a refusal never touches `this.step`. */
|
|
73
|
+
refuse(reason, nearest) {
|
|
74
|
+
const at = this.at();
|
|
75
|
+
return nearest ? { moved: false, reason, at, nearest } : { moved: false, reason, at };
|
|
76
|
+
}
|
|
77
|
+
first() {
|
|
78
|
+
if (this.stops.length === 0)
|
|
79
|
+
return this.refuse('empty');
|
|
80
|
+
if (this.step === 0)
|
|
81
|
+
return this.refuse('clamped');
|
|
82
|
+
return this.moveTo(0);
|
|
83
|
+
}
|
|
84
|
+
last() {
|
|
85
|
+
if (this.stops.length === 0)
|
|
86
|
+
return this.refuse('empty');
|
|
87
|
+
const end = this.stops.length - 1;
|
|
88
|
+
if (this.step === end)
|
|
89
|
+
return this.refuse('clamped');
|
|
90
|
+
return this.moveTo(end);
|
|
91
|
+
}
|
|
92
|
+
prev() {
|
|
93
|
+
if (this.stops.length === 0)
|
|
94
|
+
return this.refuse('empty');
|
|
95
|
+
if (this.step === 0)
|
|
96
|
+
return this.refuse('clamped');
|
|
97
|
+
return this.moveTo(this.step - 1);
|
|
98
|
+
}
|
|
99
|
+
next() {
|
|
100
|
+
if (this.stops.length === 0)
|
|
101
|
+
return this.refuse('empty');
|
|
102
|
+
if (this.step >= this.stops.length - 1)
|
|
103
|
+
return this.refuse('clamped');
|
|
104
|
+
return this.moveTo(this.step + 1);
|
|
105
|
+
}
|
|
106
|
+
jumpTo(target) {
|
|
107
|
+
if (this.stops.length === 0)
|
|
108
|
+
return this.refuse('empty');
|
|
109
|
+
if (typeof target === 'number') {
|
|
110
|
+
const step = Math.trunc(target);
|
|
111
|
+
if (Number.isNaN(step))
|
|
112
|
+
return this.refuse('miss');
|
|
113
|
+
if (step < 0)
|
|
114
|
+
return this.refuse('clamped', this.stops[0]);
|
|
115
|
+
if (step > this.stops.length - 1)
|
|
116
|
+
return this.refuse('clamped', this.stops[this.stops.length - 1]);
|
|
117
|
+
if (step === this.step)
|
|
118
|
+
return this.refuse('clamped');
|
|
119
|
+
return this.moveTo(step);
|
|
120
|
+
}
|
|
121
|
+
// `''` is not an address: it is what the bookends and any pre-stage
|
|
122
|
+
// root commit carry. Those are reached by step, never by id.
|
|
123
|
+
if (target === '')
|
|
124
|
+
return this.refuse('miss');
|
|
125
|
+
const hit = this.stops.find((stop) => stop.runtimeStageId === target);
|
|
126
|
+
if (!hit)
|
|
127
|
+
return this.refuse('miss', this.nearestTo(target));
|
|
128
|
+
if (hit.step === this.step)
|
|
129
|
+
return this.refuse('clamped');
|
|
130
|
+
return this.moveTo(hit.step);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The closest namable stop for a `runtimeStageId` that is not on this axis:
|
|
134
|
+
* the last stop of the same STAGE (a different iteration of the same node),
|
|
135
|
+
* else the last stop whose commit precedes the missed id's execution index.
|
|
136
|
+
* Naming a neighbour is a courtesy for the UI — it never moves the cursor.
|
|
137
|
+
*/
|
|
138
|
+
nearestTo(runtimeStageId) {
|
|
139
|
+
if (!(0, runtimeStageId_js_1.isExecutionKey)(runtimeStageId))
|
|
140
|
+
return undefined;
|
|
141
|
+
const stagePart = (0, runtimeStageId_js_1.stageIdOf)(runtimeStageId);
|
|
142
|
+
const wanted = (0, runtimeStageId_js_1.executionIndexOf)(runtimeStageId);
|
|
143
|
+
let sameStage;
|
|
144
|
+
let byIndex;
|
|
145
|
+
for (const stop of this.stops) {
|
|
146
|
+
if (!(0, runtimeStageId_js_1.isExecutionKey)(stop.runtimeStageId))
|
|
147
|
+
continue;
|
|
148
|
+
if ((0, runtimeStageId_js_1.stageIdOf)(stop.runtimeStageId) === stagePart)
|
|
149
|
+
sameStage = stop;
|
|
150
|
+
if (!Number.isNaN(wanted)) {
|
|
151
|
+
const idx = (0, runtimeStageId_js_1.executionIndexOf)(stop.runtimeStageId);
|
|
152
|
+
if (!Number.isNaN(idx) && idx <= wanted)
|
|
153
|
+
byIndex = stop;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return sameStage ?? byIndex;
|
|
157
|
+
}
|
|
158
|
+
// ── Reading ──────────────────────────────────────────────────────────────
|
|
159
|
+
stateAt(stop) {
|
|
160
|
+
const target = stop ?? this.at();
|
|
161
|
+
const leg = target?.sourceIdx ?? 0;
|
|
162
|
+
const fold = (0, stateAt_js_1.foldLegsFrom)(this.legs, leg, target ? target.lastCommitIdx : -1, this.foldMemo);
|
|
163
|
+
this.foldMemo = fold.memo;
|
|
164
|
+
const folded = fold.folded;
|
|
165
|
+
// Which leg the index belongs to, but only when there is more than one —
|
|
166
|
+
// a single-source fold keeps the 9.17.0 shape exactly.
|
|
167
|
+
return this.legs.length > 1 ? { ...folded, sourceIdx: leg } : folded;
|
|
168
|
+
}
|
|
169
|
+
changedSince(from) {
|
|
170
|
+
const to = this.at();
|
|
171
|
+
if (!to)
|
|
172
|
+
return Object.freeze([]);
|
|
173
|
+
const start = from ?? this.stops[this.step - 1];
|
|
174
|
+
// No earlier stop (we are at `'start'`) ⇒ everything up to here: nothing
|
|
175
|
+
// on a run cursor, and the subflow's `inputMapper` seed on a drilled one
|
|
176
|
+
// (the start bookend covers the seed that precedes stage one — `commitStops`).
|
|
177
|
+
const toLeg = to.sourceIdx ?? 0;
|
|
178
|
+
const fromLeg = start ? start.sourceIdx ?? 0 : toLeg;
|
|
179
|
+
const afterIdx = start ? start.lastCommitIdx : -1;
|
|
180
|
+
const keys = new Set();
|
|
181
|
+
// A range that crosses a chain seam is walked leg by leg: the tail of the
|
|
182
|
+
// leg it started in, every leg in between whole, then the head of the leg
|
|
183
|
+
// it ends in. Backwards ranges walk nothing and give `[]`, as before.
|
|
184
|
+
for (let leg = fromLeg; leg <= toLeg && leg < this.legs.length; leg++) {
|
|
185
|
+
const log = this.legs[leg].log;
|
|
186
|
+
const lo = leg === fromLeg ? afterIdx + 1 : 0;
|
|
187
|
+
const hi = leg === toLeg ? to.lastCommitIdx : log.length - 1;
|
|
188
|
+
for (let i = lo; i <= hi; i++) {
|
|
189
|
+
const bundle = log[i];
|
|
190
|
+
if (!bundle)
|
|
191
|
+
continue;
|
|
192
|
+
for (const entry of bundle.trace)
|
|
193
|
+
keys.add(entry.path);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return Object.freeze([...keys].sort());
|
|
197
|
+
}
|
|
198
|
+
// ── Marks (law 4: beside the log, never in it) ───────────────────────────
|
|
199
|
+
mark(name, stop) {
|
|
200
|
+
const target = stop ?? this.at();
|
|
201
|
+
if (!target)
|
|
202
|
+
return undefined;
|
|
203
|
+
const entry = Object.freeze({ name, runtimeStageId: target.runtimeStageId, step: target.step });
|
|
204
|
+
const existing = this._marks.findIndex((m) => m.name === name);
|
|
205
|
+
if (existing >= 0)
|
|
206
|
+
this._marks[existing] = entry;
|
|
207
|
+
else
|
|
208
|
+
this._marks.push(entry);
|
|
209
|
+
return entry;
|
|
210
|
+
}
|
|
211
|
+
marks() {
|
|
212
|
+
return Object.freeze([...this._marks]);
|
|
213
|
+
}
|
|
214
|
+
jumpToMark(name) {
|
|
215
|
+
if (this.stops.length === 0)
|
|
216
|
+
return this.refuse('empty');
|
|
217
|
+
const entry = this._marks.find((m) => m.name === name);
|
|
218
|
+
if (!entry)
|
|
219
|
+
return this.refuse('miss');
|
|
220
|
+
// Resolve by runtimeStageId first — the address that survives a change of
|
|
221
|
+
// strategy — and fall back to the remembered step only for the bookends,
|
|
222
|
+
// which have no stage id of their own.
|
|
223
|
+
const byId = entry.runtimeStageId
|
|
224
|
+
? this.stops.find((stop) => stop.runtimeStageId === entry.runtimeStageId)
|
|
225
|
+
: this.stops[entry.step];
|
|
226
|
+
if (!byId)
|
|
227
|
+
return this.refuse('miss', this.nearestTo(entry.runtimeStageId));
|
|
228
|
+
if (byId.step === this.step)
|
|
229
|
+
return this.refuse('clamped');
|
|
230
|
+
return this.moveTo(byId.step);
|
|
231
|
+
}
|
|
232
|
+
// ── Drilling (law 1: a separate cursor, not a second position) ───────────
|
|
233
|
+
drill(mountRuntimeStageId) {
|
|
234
|
+
// On a chain, the mount is looked for in every leg — a subflow that ran
|
|
235
|
+
// before the pause is in the first snapshot's results, one that ran after
|
|
236
|
+
// is in the second's. Ids are unique across a sound chain (chain.ts
|
|
237
|
+
// refuses one where they are not), so the first hit is the only hit.
|
|
238
|
+
let results;
|
|
239
|
+
let result;
|
|
240
|
+
for (const source of this.sources) {
|
|
241
|
+
// A stored recording's results are parsed JSON: read them as a map only
|
|
242
|
+
// when they are one, and as "no subflows" otherwise.
|
|
243
|
+
if (!(0, bundles_js_1.isPlainish)(source.subflowResults))
|
|
244
|
+
continue;
|
|
245
|
+
const hit = source.subflowResults[mountRuntimeStageId];
|
|
246
|
+
if (hit) {
|
|
247
|
+
results = source.subflowResults;
|
|
248
|
+
result = hit;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const tree = result?.treeContext;
|
|
253
|
+
if (!results || !result || !tree || !Array.isArray(tree.history))
|
|
254
|
+
return undefined;
|
|
255
|
+
// `path` is the subflow PATH — the same string for every iteration of a
|
|
256
|
+
// mount inside a loop. `mountRuntimeStageId` is the iteration actually
|
|
257
|
+
// opened, so a UI can label and key three drill panels over `sfx#2`,
|
|
258
|
+
// `sfx#8`, `sfx#14` apart instead of calling all three 'sfx'.
|
|
259
|
+
const childPath = result.subflowId ?? mountRuntimeStageId;
|
|
260
|
+
const child = {
|
|
261
|
+
// Rows are narrowed per bundle when the child reads them — a stored
|
|
262
|
+
// recording's inner history needs no cast either.
|
|
263
|
+
commitLog: tree.history,
|
|
264
|
+
initialState: tree.initialState,
|
|
265
|
+
executionTree: tree.stageContexts,
|
|
266
|
+
// Nested subflow results are merged UP into one flat, dual-keyed map, so
|
|
267
|
+
// the child can drill further through the same map.
|
|
268
|
+
subflowResults: results,
|
|
269
|
+
};
|
|
270
|
+
return new Cursor([child], { strategy: this.options.strategy }, childPath, this, mountRuntimeStageId);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Re-step per-source stops into ONE axis.
|
|
275
|
+
*
|
|
276
|
+
* The INTERIOR bookends go: a `'start'` on any leg but the first, and an
|
|
277
|
+
* `'end'` on any leg but the last, are bookends of a leg and not of the run —
|
|
278
|
+
* keeping them would put "the run is over" in the middle of the axis. The
|
|
279
|
+
* commits they covered are not orphaned: a fold always replays its leg from
|
|
280
|
+
* that leg's own base, and `changedSince` walks a seam-crossing range leg by
|
|
281
|
+
* leg (including the head of the leg it ends in). The stops' `commitIdx` /
|
|
282
|
+
* `lastCommitIdx` ranges PARTITION EACH LEG, never span the seam: a stop's
|
|
283
|
+
* range ends at its own leg's last index, and the next leg's first stop opens
|
|
284
|
+
* at that leg's 0.
|
|
285
|
+
*/
|
|
286
|
+
function chainStops(perSource) {
|
|
287
|
+
// "Interior" is measured against the legs that actually CONTRIBUTE stops: a
|
|
288
|
+
// leg whose log was empty has no bookends to drop and must not make its
|
|
289
|
+
// neighbour's `'start'` or `'end'` interior — the chained axis still opens
|
|
290
|
+
// with the first stop that exists and closes with the last.
|
|
291
|
+
const contributing = perSource.map((stops) => stops.length > 0);
|
|
292
|
+
const firstLeg = contributing.indexOf(true);
|
|
293
|
+
const lastLeg = contributing.lastIndexOf(true);
|
|
294
|
+
const out = [];
|
|
295
|
+
for (const [sourceIdx, stops] of perSource.entries()) {
|
|
296
|
+
for (const stop of stops) {
|
|
297
|
+
if (sourceIdx > firstLeg && stop.kind === 'start')
|
|
298
|
+
continue;
|
|
299
|
+
if (sourceIdx < lastLeg && stop.kind === 'end')
|
|
300
|
+
continue;
|
|
301
|
+
out.push({ ...stop, step: out.length, sourceIdx });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return out;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Open a reader's cursor over a finished run.
|
|
308
|
+
*
|
|
309
|
+
* Nothing here executes anything: `snapshot` is a record of a run that already
|
|
310
|
+
* happened, and every question is answered by reading it. A stored JSON
|
|
311
|
+
* snapshot works exactly like a live one — since 9.18.0 its `commitLog` may be
|
|
312
|
+
* an unvalidated `unknown[]`, narrowed per bundle where the fold reads it, so
|
|
313
|
+
* a replayed recording needs no cast.
|
|
314
|
+
*
|
|
315
|
+
* @param snapshot `executor.getSnapshot()`, or any object with the same
|
|
316
|
+
* shape — including one round-tripped through JSON, or a subflow subtree
|
|
317
|
+
* (`getSubtreeSnapshot()`, whose log is spelled `history`). An ARRAY of them
|
|
318
|
+
* is a CHAIN: a pause and its resume read as one axis (see below).
|
|
319
|
+
* @param options `strategy` chooses how stops are derived (default:
|
|
320
|
+
* `commitStops`, one stop per executed stage); `marks` seeds the cursor with
|
|
321
|
+
* bookmarks, e.g. restored from a saved reading session.
|
|
322
|
+
*
|
|
323
|
+
* @throws when a chain is not in run order, or its legs are not from one
|
|
324
|
+
* lineage — three checks, each with its own reason (see `chain.ts`):
|
|
325
|
+
* no `runtimeStageId` may appear in two legs; each leg's first execution
|
|
326
|
+
* index must be past the previous leg's last (the engine never resets the
|
|
327
|
+
* counter across a resume); and each leg's `initialState` must deep-equal
|
|
328
|
+
* the state the legs before it fold to — on a resume that base IS the state
|
|
329
|
+
* at the pause, so a foreign leg whose indices merely happen to be higher
|
|
330
|
+
* is refused too. The third check runs only where the record allows it: the
|
|
331
|
+
* later leg must carry an `initialState` and the earlier legs must fold from
|
|
332
|
+
* a real base with no unreadable rows; otherwise (an older recording with
|
|
333
|
+
* no base) the chain rests on the two index checks alone, and the fold's
|
|
334
|
+
* `basis` says what it stood on.
|
|
335
|
+
*
|
|
336
|
+
* @example
|
|
337
|
+
* ```typescript
|
|
338
|
+
* const tt = timeTravel(executor.getSnapshot());
|
|
339
|
+
* tt.jumpTo('score-risk#4');
|
|
340
|
+
* console.log(tt.changedSince()); // ['riskScore', 'tier']
|
|
341
|
+
* console.log(tt.stateAt().state); // state as that stage left it
|
|
342
|
+
* tt.mark('the bad one');
|
|
343
|
+
* const inner = tt.drill('sf-payment#7'); // a separate cursor, its own log
|
|
344
|
+
*
|
|
345
|
+
* // A pause and its resume, read as ONE axis:
|
|
346
|
+
* const chained = timeTravel([pausedSnapshot, resumedSnapshot]);
|
|
347
|
+
* chained.last(); // the run's real end
|
|
348
|
+
* chained.at()!.sourceIdx; // 1 — which leg the indices are in
|
|
349
|
+
* chained.jumpTo('approve#3'); // finds it in either leg
|
|
350
|
+
* ```
|
|
351
|
+
*/
|
|
352
|
+
function timeTravel(snapshot, options = {}) {
|
|
353
|
+
const sources = Array.isArray(snapshot) ? snapshot : [snapshot];
|
|
354
|
+
// A one-element array is not a chain: it is the plain cursor, and skips the
|
|
355
|
+
// lineage checks a single source cannot fail.
|
|
356
|
+
if (sources.length !== 1) {
|
|
357
|
+
const refusal = (0, chain_js_1.refuseChain)(sources.map((source) => (0, stateAt_js_1.readSource)(source)));
|
|
358
|
+
if (refusal)
|
|
359
|
+
throw new Error(refusal);
|
|
360
|
+
}
|
|
361
|
+
return new Cursor(sources, options);
|
|
362
|
+
}
|
|
363
|
+
exports.timeTravel = timeTravel;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* time-travel/types.ts — the shapes of the reader's cursor.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS MODULE EXISTS (context for future readers, human or LLM):
|
|
6
|
+
* Time travel in foottrace is a READER'S cursor over a FINISHED trace, with
|
|
7
|
+
* a fold at each stop. It is not the Walker — the engine's traversal — and it
|
|
8
|
+
* must never become a second live cursor: nothing here can move, re-run or
|
|
9
|
+
* mutate an execution. Every consumer (a why-panel, a flow view, a chart
|
|
10
|
+
* dashboard) was writing the same commit-index arithmetic by hand, from the
|
|
11
|
+
* same commit log, and disagreeing about the answers. This is that arithmetic,
|
|
12
|
+
* once, in the library that owns the substrate.
|
|
13
|
+
*
|
|
14
|
+
* DAG position: memory ← time-travel. Every file here is a record file
|
|
15
|
+
* (ranked in scripts/layers.json): it may import only record
|
|
16
|
+
* files — memory/, the id grammar (`ids/runtimeStageId`), and the record's two
|
|
17
|
+
* capture/ leaves (`freeze`, `valueKinds`). Recorders, traversal and runner
|
|
18
|
+
* must never be imported here; the snapshot arrives as a plain structural
|
|
19
|
+
* shape ({@link FoldSource}, {@link TimeTravelSource}, the record's own
|
|
20
|
+
* `ExecutionTree` for the tree) so a stored JSON trace works exactly like a
|
|
21
|
+
* live `getSnapshot()`.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.updateNestedValue = exports.setNestedValue = exports.nativeHas = exports.nativeGet = exports.isDeniedSegment = exports.pathSegments = exports.normaliseStateKey = void 0;
|
|
4
|
+
/** Record-path decoding and safe nested writes. Values keep their existing merge law. */
|
|
5
|
+
var paths_js_1 = require("./lib/memory/paths.js");
|
|
6
|
+
Object.defineProperty(exports, "normaliseStateKey", { enumerable: true, get: function () { return paths_js_1.normaliseStateKey; } });
|
|
7
|
+
Object.defineProperty(exports, "pathSegments", { enumerable: true, get: function () { return paths_js_1.pathSegments; } });
|
|
8
|
+
var pathOps_js_1 = require("./lib/memory/pathOps.js");
|
|
9
|
+
Object.defineProperty(exports, "isDeniedSegment", { enumerable: true, get: function () { return pathOps_js_1.isDeniedSegment; } });
|
|
10
|
+
Object.defineProperty(exports, "nativeGet", { enumerable: true, get: function () { return pathOps_js_1.nativeGet; } });
|
|
11
|
+
Object.defineProperty(exports, "nativeHas", { enumerable: true, get: function () { return pathOps_js_1.nativeHas; } });
|
|
12
|
+
var utils_js_1 = require("./lib/memory/utils.js");
|
|
13
|
+
Object.defineProperty(exports, "setNestedValue", { enumerable: true, get: function () { return utils_js_1.setNestedValue; } });
|
|
14
|
+
Object.defineProperty(exports, "updateNestedValue", { enumerable: true, get: function () { return utils_js_1.updateNestedValue; } });
|