foottrace 0.0.0-stage → 1.0.1
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 +292 -0
- package/dist/esm/lib/memory/backtrack.js +466 -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 +80 -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 +473 -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 +84 -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 +292 -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,285 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* slice/serialize.ts — JSON-safe and LLM-safe projections of a slice.
|
|
4
|
+
*
|
|
5
|
+
* WHY (the failure this prevents): `VariableSlice.root` is an in-memory DAG.
|
|
6
|
+
* Nodes are SHARED — a diamond ancestor is one object reached through many
|
|
7
|
+
* paths, and each node appears in both `parents` and `parentEdges[].parent`.
|
|
8
|
+
* `JSON.stringify` knows nothing about sharing: it re-serializes every shared
|
|
9
|
+
* subtree per path, which explodes combinatorially on diamond-heavy slices.
|
|
10
|
+
* The two consumers that would naively stringify are exactly the ones this
|
|
11
|
+
* library exists for — wire transfer (persist / send a slice) and LLM tools
|
|
12
|
+
* (bounded context). Each gets a purpose-built projection:
|
|
13
|
+
*
|
|
14
|
+
* - {@link sliceToJSON} — flat, id-referenced, LINEAR in node count.
|
|
15
|
+
* - {@link formatSlice} — one bounded human/LLM-readable string that also
|
|
16
|
+
* renders the honesty envelope (missing reason, reads coverage, truncation)
|
|
17
|
+
* a raw `formatCausalChain` doesn't know about.
|
|
18
|
+
*
|
|
19
|
+
* The FORWARD half has exactly the same hazard and the same two projections:
|
|
20
|
+
* {@link forwardSliceToJSON} / {@link formatForwardSlice}. A forward DAG
|
|
21
|
+
* shares nodes just as readily — two values read by one stage feed the same
|
|
22
|
+
* child write — so a forward root must never be stringified either.
|
|
23
|
+
*
|
|
24
|
+
* {@link KeyTimeline} is the deliberate exception, stated so nobody adds a
|
|
25
|
+
* pointless twin: it is a FLAT list of plain fields holding no live
|
|
26
|
+
* references and no sharing, so `JSON.stringify(timeline)` is already
|
|
27
|
+
* correct and linear. What it still needs is the bounded LLM projection —
|
|
28
|
+
* {@link formatTimeline} — because "the whole life of a key" is unbounded in
|
|
29
|
+
* a long run.
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.formatTimeline = exports.formatForwardSlice = exports.forwardSliceToJSON = exports.formatSlice = exports.sliceToJSON = void 0;
|
|
33
|
+
const backtrack_js_1 = require("../memory/backtrack.js");
|
|
34
|
+
/**
|
|
35
|
+
* Flat, JSON-safe projection: every DAG node exactly once (keyed by
|
|
36
|
+
* runtimeStageId), edges as id references. Linear in node count — safe to
|
|
37
|
+
* persist, send, or feed to structured consumers. Lossless for everything
|
|
38
|
+
* except the in-memory object graph itself (rebuild adjacency from `edges`).
|
|
39
|
+
*/
|
|
40
|
+
function sliceToJSON(slice) {
|
|
41
|
+
const out = {
|
|
42
|
+
key: slice.key,
|
|
43
|
+
...(slice.before !== undefined && { before: slice.before }),
|
|
44
|
+
...(slice.missing !== undefined && { missing: slice.missing }),
|
|
45
|
+
keysReadKind: slice.keysReadKind,
|
|
46
|
+
...(slice.readsCoverage !== undefined && { readsCoverage: slice.readsCoverage }),
|
|
47
|
+
};
|
|
48
|
+
if (!slice.root)
|
|
49
|
+
return out;
|
|
50
|
+
out.writerId = slice.root.runtimeStageId;
|
|
51
|
+
const nodes = {};
|
|
52
|
+
const edges = [];
|
|
53
|
+
for (const node of (0, backtrack_js_1.flattenCausalDAG)(slice.root)) {
|
|
54
|
+
nodes[node.runtimeStageId] = {
|
|
55
|
+
stageId: node.stageId,
|
|
56
|
+
stageName: node.stageName,
|
|
57
|
+
keysWritten: node.keysWritten,
|
|
58
|
+
depth: node.depth,
|
|
59
|
+
...(node.incompleteSources !== undefined && { incompleteSources: node.incompleteSources }),
|
|
60
|
+
...(node.preRunReads !== undefined && { preRunReads: node.preRunReads }),
|
|
61
|
+
};
|
|
62
|
+
for (const edge of node.parentEdges) {
|
|
63
|
+
edges.push({
|
|
64
|
+
from: node.runtimeStageId,
|
|
65
|
+
to: edge.parent.runtimeStageId,
|
|
66
|
+
kind: edge.kind,
|
|
67
|
+
...(edge.key !== undefined && { key: edge.key }),
|
|
68
|
+
weight: edge.weight,
|
|
69
|
+
...(edge.basis !== undefined && { basis: edge.basis }),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
out.nodes = nodes;
|
|
74
|
+
out.edges = edges;
|
|
75
|
+
if (slice.root.truncated)
|
|
76
|
+
out.truncated = slice.root.truncated;
|
|
77
|
+
if (slice.notes !== undefined)
|
|
78
|
+
out.notes = slice.notes;
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
exports.sliceToJSON = sliceToJSON;
|
|
82
|
+
/**
|
|
83
|
+
* One bounded string for LLM triage tools (the `traceToolpack` consumption
|
|
84
|
+
* pattern: tools return plain strings, never recursive objects). Wraps
|
|
85
|
+
* `formatCausalChain` (which is budget-bounded by causalChain's
|
|
86
|
+
* maxDepth/maxNodes and renders shared nodes once as `↳ … (see above)`),
|
|
87
|
+
* and adds the honesty envelope the raw chain doesn't carry:
|
|
88
|
+
*
|
|
89
|
+
* - missing slices render their reason ("value came from initial state /
|
|
90
|
+
* frozen args / a closure — the commit log cannot see those"),
|
|
91
|
+
* - a reads-less provider (`readTracking: 'off'` signature) renders an
|
|
92
|
+
* explicit "⚠ reads were not recorded" instead of silently showing an
|
|
93
|
+
* anchor with no dependencies,
|
|
94
|
+
* - truncation footers pass through from formatCausalChain.
|
|
95
|
+
*/
|
|
96
|
+
function formatSlice(slice) {
|
|
97
|
+
const lines = [];
|
|
98
|
+
const anchor = slice.before !== undefined ? ` (before commit ${slice.before})` : '';
|
|
99
|
+
lines.push(`SLICE for '${slice.key}'${anchor} — reads via: ${slice.keysReadKind}`);
|
|
100
|
+
if (slice.missing === 'empty-log') {
|
|
101
|
+
lines.push('no slice: the commit log is empty (nothing has executed).');
|
|
102
|
+
return lines.join('\n');
|
|
103
|
+
}
|
|
104
|
+
if (slice.missing === 'never-written') {
|
|
105
|
+
lines.push(`no slice: '${slice.key}' was never written in range — the value came from ` +
|
|
106
|
+
'initial state, frozen run input (args), or a closure; the commit log cannot see those.');
|
|
107
|
+
return lines.join('\n');
|
|
108
|
+
}
|
|
109
|
+
const cov = slice.readsCoverage;
|
|
110
|
+
if (cov && cov.steps > 1 && cov.stepsWithReads === 0) {
|
|
111
|
+
lines.push("⚠ reads were not recorded (readTracking may be 'off') — dependencies below are " + 'unknowable, NOT absent.');
|
|
112
|
+
}
|
|
113
|
+
if (slice.root)
|
|
114
|
+
lines.push((0, backtrack_js_1.formatCausalChain)(slice.root));
|
|
115
|
+
return [...lines, ...noteLines(slice.notes ?? [])].join('\n');
|
|
116
|
+
}
|
|
117
|
+
exports.formatSlice = formatSlice;
|
|
118
|
+
// ── Forward projections ───────────────────────────────────────────────────
|
|
119
|
+
/** How many reads one node lists before the renderer says "… and N more". */
|
|
120
|
+
const READS_RENDERED = 5;
|
|
121
|
+
/** BFS over the forward DAG, each node exactly once (shared nodes included). */
|
|
122
|
+
function flattenForwardDAG(root) {
|
|
123
|
+
const out = [];
|
|
124
|
+
const seen = new Set();
|
|
125
|
+
const queue = [root];
|
|
126
|
+
while (queue.length > 0) {
|
|
127
|
+
const node = queue.shift();
|
|
128
|
+
if (seen.has(node))
|
|
129
|
+
continue;
|
|
130
|
+
seen.add(node);
|
|
131
|
+
out.push(node);
|
|
132
|
+
for (const edge of node.fedEdges)
|
|
133
|
+
if (!seen.has(edge.child))
|
|
134
|
+
queue.push(edge.child);
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
/** `⚠ …` lines for the honesty envelope — same rendering for both doors. */
|
|
139
|
+
function noteLines(notes) {
|
|
140
|
+
return notes.map((n) => `⚠ ${n.detail}`);
|
|
141
|
+
}
|
|
142
|
+
/** One node's headline: who wrote the value, and how long it lived. */
|
|
143
|
+
function nodeHeadline(node) {
|
|
144
|
+
const life = node.nextWriteIdx !== undefined ? `live until commit ${node.nextWriteIdx}` : 'live to the end of the run';
|
|
145
|
+
if (node.origin === 'pre-run') {
|
|
146
|
+
return `'${node.key}' from BEFORE the run (initial state / input / closure) — ${life}`;
|
|
147
|
+
}
|
|
148
|
+
return `'${node.key}' ${node.verb ?? 'set'} by ${node.stageName} (${node.runtimeStageId}) @${node.commitIdx} — ${life}`;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Flat, JSON-safe projection of a {@link ForwardSlice} — the forward twin of
|
|
152
|
+
* {@link sliceToJSON}, linear in node count.
|
|
153
|
+
*
|
|
154
|
+
* Node ids are OPAQUE (`n0`, `n1`, … in BFS order) rather than
|
|
155
|
+
* `runtimeStageId` as in `SliceJSON`, for a structural reason: a forward
|
|
156
|
+
* node is a (key, write) pair, so ONE stage that wrote two keys owns two
|
|
157
|
+
* nodes and a runtimeStageId cannot key the map. Never parse the ids — join
|
|
158
|
+
* on `runtimeStageId` / `commitIdx` / `key`, which every node carries.
|
|
159
|
+
*/
|
|
160
|
+
function forwardSliceToJSON(slice) {
|
|
161
|
+
const out = {
|
|
162
|
+
key: slice.key,
|
|
163
|
+
...(slice.before !== undefined && { before: slice.before }),
|
|
164
|
+
...(slice.missing !== undefined && { missing: slice.missing }),
|
|
165
|
+
keysReadKind: slice.keysReadKind,
|
|
166
|
+
...(slice.readsCoverage !== undefined && { readsCoverage: slice.readsCoverage }),
|
|
167
|
+
notes: slice.notes,
|
|
168
|
+
};
|
|
169
|
+
if (!slice.root)
|
|
170
|
+
return out;
|
|
171
|
+
const flat = flattenForwardDAG(slice.root);
|
|
172
|
+
const ids = new Map();
|
|
173
|
+
flat.forEach((node, i) => ids.set(node, `n${i}`));
|
|
174
|
+
out.rootId = ids.get(slice.root);
|
|
175
|
+
out.nodes = flat.map((node) => ({
|
|
176
|
+
id: ids.get(node),
|
|
177
|
+
key: node.key,
|
|
178
|
+
origin: node.origin,
|
|
179
|
+
...(node.runtimeStageId !== undefined && { runtimeStageId: node.runtimeStageId }),
|
|
180
|
+
...(node.stageId !== undefined && { stageId: node.stageId }),
|
|
181
|
+
...(node.stageName !== undefined && { stageName: node.stageName }),
|
|
182
|
+
...(node.commitIdx !== undefined && { commitIdx: node.commitIdx }),
|
|
183
|
+
...(node.verb !== undefined && { verb: node.verb }),
|
|
184
|
+
...(node.nextWriteIdx !== undefined && { nextWriteIdx: node.nextWriteIdx }),
|
|
185
|
+
depth: node.depth,
|
|
186
|
+
reads: node.reads,
|
|
187
|
+
...(node.incompleteSources !== undefined && { incompleteSources: node.incompleteSources }),
|
|
188
|
+
}));
|
|
189
|
+
out.edges = flat.flatMap((node) => node.fedEdges.map((edge) => ({ from: ids.get(node), to: ids.get(edge.child), basis: edge.basis })));
|
|
190
|
+
if (slice.root.truncated)
|
|
191
|
+
out.truncated = slice.root.truncated;
|
|
192
|
+
if (slice.notes !== undefined)
|
|
193
|
+
out.notes = slice.notes;
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
exports.forwardSliceToJSON = forwardSliceToJSON;
|
|
197
|
+
/**
|
|
198
|
+
* One bounded string for LLM triage tools — the forward twin of
|
|
199
|
+
* {@link formatSlice}. Shared nodes render once (`↳ … (see above)`, the same
|
|
200
|
+
* convention `formatCausalChain` uses), read lists are capped, and every
|
|
201
|
+
* honesty note renders as a `⚠` line — including the one that says a `fed`
|
|
202
|
+
* edge is conservative, which is why edges print `[exact]` / `[conservative]`
|
|
203
|
+
* rather than looking alike.
|
|
204
|
+
*/
|
|
205
|
+
function formatForwardSlice(slice) {
|
|
206
|
+
const lines = [];
|
|
207
|
+
const anchor = slice.before !== undefined ? ` (as of before commit ${slice.before})` : '';
|
|
208
|
+
lines.push(`FORWARD SLICE for '${slice.key}'${anchor} — reads via: ${slice.keysReadKind}`);
|
|
209
|
+
if (slice.missing === 'empty-log') {
|
|
210
|
+
lines.push('no forward slice: the commit log is empty (nothing has executed).');
|
|
211
|
+
return [...lines, ...noteLines(slice.notes)].join('\n');
|
|
212
|
+
}
|
|
213
|
+
if (slice.missing === 'never-written') {
|
|
214
|
+
lines.push(`no forward slice: '${slice.key}' was never written and never read in this log.`);
|
|
215
|
+
return [...lines, ...noteLines(slice.notes)].join('\n');
|
|
216
|
+
}
|
|
217
|
+
const seen = new Set();
|
|
218
|
+
const walk = (node, indent, lead = '') => {
|
|
219
|
+
const pad = ' '.repeat(indent);
|
|
220
|
+
const head = `${pad}${lead}${nodeHeadline(node)}`;
|
|
221
|
+
if (seen.has(node)) {
|
|
222
|
+
lines.push(`${head} ↳ (see above)`);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
seen.add(node);
|
|
226
|
+
lines.push(head);
|
|
227
|
+
if (node.incompleteSources && node.incompleteSources.length > 0) {
|
|
228
|
+
lines.push(`${pad} ⚠ its writer also consumed ${node.incompleteSources.join('/')} — untracked`);
|
|
229
|
+
}
|
|
230
|
+
if (node.reads.length === 0) {
|
|
231
|
+
lines.push(`${pad} (no recorded read of this value)`);
|
|
232
|
+
}
|
|
233
|
+
for (const read of node.reads.slice(0, READS_RENDERED)) {
|
|
234
|
+
lines.push(`${pad} read by ${read.stageName} (${read.runtimeStageId}) @${read.commitIdx}`);
|
|
235
|
+
}
|
|
236
|
+
if (node.reads.length > READS_RENDERED) {
|
|
237
|
+
lines.push(`${pad} … and ${node.reads.length - READS_RENDERED} more reads`);
|
|
238
|
+
}
|
|
239
|
+
for (const edge of node.fedEdges) {
|
|
240
|
+
walk(edge.child, indent + 1, `→ fed ${edge.basis === 'per-write' ? '[exact]' : '[conservative]'} `);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
if (slice.root)
|
|
244
|
+
walk(slice.root, 0);
|
|
245
|
+
return [...lines, ...noteLines(slice.notes)].join('\n');
|
|
246
|
+
}
|
|
247
|
+
exports.formatForwardSlice = formatForwardSlice;
|
|
248
|
+
/** How many moments {@link formatTimeline} renders before it says "… and N more". */
|
|
249
|
+
const MOMENTS_RENDERED = 60;
|
|
250
|
+
/**
|
|
251
|
+
* One bounded string for a {@link KeyTimeline} — the LLM/human projection.
|
|
252
|
+
* (The timeline's JSON needs no helper: it is already flat and JSON-safe —
|
|
253
|
+
* see the file header.) Truncation is stated, never silent.
|
|
254
|
+
*/
|
|
255
|
+
function formatTimeline(timeline) {
|
|
256
|
+
const lines = [];
|
|
257
|
+
const bound = timeline.before !== undefined ? ` (before commit ${timeline.before})` : '';
|
|
258
|
+
lines.push(`TIMELINE for '${timeline.key}'${bound} — reads via: ${timeline.keysReadKind}`);
|
|
259
|
+
if (timeline.missing === 'empty-log') {
|
|
260
|
+
lines.push('no timeline: the commit log is empty (nothing has executed).');
|
|
261
|
+
return [...lines, ...noteLines(timeline.notes)].join('\n');
|
|
262
|
+
}
|
|
263
|
+
if (timeline.missing === 'never-written') {
|
|
264
|
+
lines.push(`no timeline: '${timeline.key}' was never written and never read in this log.`);
|
|
265
|
+
return [...lines, ...noteLines(timeline.notes)].join('\n');
|
|
266
|
+
}
|
|
267
|
+
const moments = timeline.moments ?? [];
|
|
268
|
+
if (moments.length === 0)
|
|
269
|
+
lines.push('(no recorded moment for this key)');
|
|
270
|
+
for (const moment of moments.slice(0, MOMENTS_RENDERED)) {
|
|
271
|
+
const where = `${moment.stageName} (${moment.runtimeStageId})`;
|
|
272
|
+
if (moment.kind === 'write') {
|
|
273
|
+
lines.push(`@${moment.commitIdx} write ${moment.verb ?? 'set'} — ${where}`);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
const from = moment.fromWriteIdx !== undefined ? `value from commit ${moment.fromWriteIdx}` : 'value from BEFORE the run';
|
|
277
|
+
lines.push(`@${moment.commitIdx} read — ${where} (${from})`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (moments.length > MOMENTS_RENDERED) {
|
|
281
|
+
lines.push(`… and ${moments.length - MOMENTS_RENDERED} more moments (bounded output)`);
|
|
282
|
+
}
|
|
283
|
+
return [...lines, ...noteLines(timeline.notes)].join('\n');
|
|
284
|
+
}
|
|
285
|
+
exports.formatTimeline = formatTimeline;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* slice/sliceForKey.ts — the variable-first backward slice query.
|
|
4
|
+
*
|
|
5
|
+
* "Why is `key` what it is?" — the ONE question every triage surface asks:
|
|
6
|
+
* a human clicking a key in explainable-ui, an LLM calling a `backtrack`
|
|
7
|
+
* tool mid-conversation, an offline autopsy agent over a stored run.
|
|
8
|
+
*
|
|
9
|
+
* WHY this is deliberately a composition and not a new algorithm:
|
|
10
|
+
* `findLastWriter` already answers "who made the value what it is" and
|
|
11
|
+
* `causalChain` already answers "what influenced that step" (thin backward
|
|
12
|
+
* slicing, Weiser 1984). What was missing is the shared CONTRACT — without
|
|
13
|
+
* it, every surface hand-rolls the anchor step differently and their answers
|
|
14
|
+
* disagree. The value of this function is that they can't.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.sliceForKey = void 0;
|
|
18
|
+
const backtrack_js_1 = require("../memory/backtrack.js");
|
|
19
|
+
const commitLogUtils_js_1 = require("../memory/commitLogUtils.js");
|
|
20
|
+
const paths_js_1 = require("../memory/paths.js");
|
|
21
|
+
const keyIndex_js_1 = require("./keyIndex.js");
|
|
22
|
+
const keysReadSources_js_1 = require("./keysReadSources.js");
|
|
23
|
+
/**
|
|
24
|
+
* Backward slice for one state key: anchor at the key's last writer, then
|
|
25
|
+
* walk read→write (and optional control) dependencies transitively.
|
|
26
|
+
*
|
|
27
|
+
* Honest-absence contract: a missing slice is a RESULT, not an error —
|
|
28
|
+
* `missing: 'never-written'` tells a triage tool (or its LLM caller) that
|
|
29
|
+
* the value came from initial state / frozen args / a closure, which is
|
|
30
|
+
* itself diagnostic information. See {@link VariableSlice} — including its
|
|
31
|
+
* SERIALIZATION WARNING before you stringify the result.
|
|
32
|
+
*/
|
|
33
|
+
function sliceForKey(commitLog, key, keysRead, options) {
|
|
34
|
+
const source = (0, keysReadSources_js_1.resolveKeysReadSource)(keysRead);
|
|
35
|
+
const normalisedKey = (0, paths_js_1.normaliseStateKey)(key);
|
|
36
|
+
const base = {
|
|
37
|
+
key: normalisedKey,
|
|
38
|
+
...(options?.before !== undefined && { before: options.before }),
|
|
39
|
+
keysReadKind: source.kind,
|
|
40
|
+
...(source.coverage !== undefined && { readsCoverage: source.coverage }),
|
|
41
|
+
};
|
|
42
|
+
if (commitLog.length === 0)
|
|
43
|
+
return { ...base, missing: 'empty-log' };
|
|
44
|
+
const { writer, basis } = (0, commitLogUtils_js_1.findLastWriterWithBasis)(commitLog, normalisedKey, options?.before);
|
|
45
|
+
// F4b: what the value at the slice's point rests on, from the value twin — a redaction, the pre-run base.
|
|
46
|
+
const end = Math.min(options?.before ?? commitLog.length, commitLog.length) - 1;
|
|
47
|
+
const value = end >= 0 ? (0, commitLogUtils_js_1.commitValueAtWithBasis)(commitLog, end, normalisedKey).basis : [];
|
|
48
|
+
if (!writer) {
|
|
49
|
+
// 'never-written' already says the value came from before the run; a redaction that hid the write does not.
|
|
50
|
+
return {
|
|
51
|
+
...base,
|
|
52
|
+
missing: 'never-written',
|
|
53
|
+
...(value.includes('redacted') && { notes: [(0, keyIndex_js_1.redactedValueNote)(normalisedKey)] }),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// A writer that reached the key only through rows inside it made part of the value — say so.
|
|
57
|
+
const notes = basis.includes('nested-rows') ? [(0, keyIndex_js_1.nestedRowsNote)(normalisedKey, [commitLog.indexOf(writer)])] : [];
|
|
58
|
+
if (value.includes('from-initial-state'))
|
|
59
|
+
notes.push((0, keyIndex_js_1.fromInitialStateNote)(normalisedKey));
|
|
60
|
+
if (value.includes('redacted'))
|
|
61
|
+
notes.push((0, keyIndex_js_1.redactedValueNote)(normalisedKey));
|
|
62
|
+
// INVARIANT: `writer` came FROM this commitLog, so its runtimeStageId is
|
|
63
|
+
// always present in causalChain's index — causalChain cannot return
|
|
64
|
+
// undefined here (it only does for ids absent from the log). The non-null
|
|
65
|
+
// assertion keeps the VariableSlice contract airtight: `missing` is the
|
|
66
|
+
// ONLY absent-root state. Options pass straight through (controlDeps /
|
|
67
|
+
// weigh / maxDepth / maxNodes keep their causalChain semantics).
|
|
68
|
+
//
|
|
69
|
+
// edgeAttribution defaults to 'per-write' HERE (unlike causalChain's
|
|
70
|
+
// 'stage' default): a variable-first slice knows exactly which written key
|
|
71
|
+
// anchors the question, and per-write attribution is strictly-safe — logs
|
|
72
|
+
// without `TraceEntry.readKeys` (the writeProvenance dial off) fall back
|
|
73
|
+
// to stage-level expansion per node, byte-identically. Consumers can force
|
|
74
|
+
// 'stage' via options.
|
|
75
|
+
// Preserve this exact writer's metadata/read prefix, even when it is a
|
|
76
|
+
// continuation. Causal expansion separately uses the execution's first commit.
|
|
77
|
+
const root = (0, backtrack_js_1.causalChainFromCommit)(commitLog, writer, source.lookup, {
|
|
78
|
+
...options,
|
|
79
|
+
edgeAttribution: options?.edgeAttribution ?? 'per-write',
|
|
80
|
+
rootLinkKeys: options?.rootLinkKeys ?? [normalisedKey],
|
|
81
|
+
});
|
|
82
|
+
return { ...base, writer, root, ...(notes.length > 0 && { notes }) };
|
|
83
|
+
}
|
|
84
|
+
exports.sliceForKey = sliceForKey;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* slice/types.ts — data shapes of the variable-first slicing layer.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS LIBRARY EXISTS (context for future readers, human or LLM):
|
|
6
|
+
* `causalChain()` (memory/backtrack.ts) answers "what influenced this STEP?"
|
|
7
|
+
* — its address space is runtimeStageIds. But every triage consumer starts
|
|
8
|
+
* from a VARIABLE: a human clicks the key `creditTier` in a UI; an LLM tool
|
|
9
|
+
* call asks "where did history[7] come from?". This library is the address
|
|
10
|
+
* translation layer: variable in → slice out. It deliberately adds NO new
|
|
11
|
+
* capture and NO new graph algorithm — it composes the commit-log primitives
|
|
12
|
+
* (findLastWriter / causalChain / the per-key verb fold) so every surface
|
|
13
|
+
* (UI panel, LLM tool, offline autopsy agent) asks the question the same way
|
|
14
|
+
* and gets the same, honestly-labeled answer.
|
|
15
|
+
*
|
|
16
|
+
* Both directions live here: BACKWARD ("why is `key` what it is" —
|
|
17
|
+
* {@link VariableSlice}) and FORWARD ("who read it, and what did it feed" —
|
|
18
|
+
* {@link ForwardSlice}, {@link KeyTimeline}). Same anchor idiom, same
|
|
19
|
+
* `KeysReadSource` strategies, same honesty vocabulary.
|
|
20
|
+
*
|
|
21
|
+
* DAG position: memory ← slice. This module may import ONLY from memory/.
|
|
22
|
+
* Recorders, engine, and runner must never be imported here — that is what
|
|
23
|
+
* keeps it a tiny, separately-evolvable library (see README.md).
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* axis.ts — the shape of an axis, and the one safe way to narrow it.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS FILE EXISTS. `commitStops` returns `Stop[]`, and every strategy
|
|
6
|
+
* that COMPOSES it (keep the stages my domain recognises, drop the rest) rests
|
|
7
|
+
* on a shape the type never stated: `[start, …stages, end]` for a non-empty
|
|
8
|
+
* log, empty for an empty one. Each composer therefore wrote its own runtime
|
|
9
|
+
* guard against its own reading of the contract, plus a mocked-substrate test
|
|
10
|
+
* to prove the guard fires. That is one invariant, discovered three times, and
|
|
11
|
+
* the library that owns the invariant should be the one that states it.
|
|
12
|
+
*
|
|
13
|
+
* {@link splitAxis} states it. {@link filterStops} then does the whole
|
|
14
|
+
* composition — check the shape, keep what the caller keeps, RE-PARTITION the
|
|
15
|
+
* log so the dropped stages' commits are still folded somewhere, and mark the
|
|
16
|
+
* `'start'` bookend as carrying a prologue — because getting the
|
|
17
|
+
* re-partitioning right by hand is the other thing every composer had to
|
|
18
|
+
* rediscover.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.filterStops = exports.splitAxis = void 0;
|
|
22
|
+
/**
|
|
23
|
+
* Split a strategy's stops into `{ start, stages, end }`, or say why it cannot.
|
|
24
|
+
*
|
|
25
|
+
* THE INVARIANT, stated once: for a non-EMPTY log, `commitStops` returns at
|
|
26
|
+
* least the two bookends, `'start'` first and `'end'` last, with one stop per
|
|
27
|
+
* executed stage in execution order between them. For an EMPTY log it returns
|
|
28
|
+
* `[]` — no bookends around nothing, because a cursor with nowhere to stand
|
|
29
|
+
* should say `reason: 'empty'` rather than offer two positions that are the
|
|
30
|
+
* same position.
|
|
31
|
+
*
|
|
32
|
+
* The two refusals are different facts and must not be collapsed: `'empty'`
|
|
33
|
+
* means the run committed nothing, `'not-bookended'` means a strategy broke
|
|
34
|
+
* the contract. A composer that treats them alike reports a broken strategy as
|
|
35
|
+
* an empty run.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* import { commitStops, splitAxis } from 'foottrace';
|
|
40
|
+
*
|
|
41
|
+
* const axis = splitAxis(commitStops(snapshot.commitLog, snapshot.executionTree));
|
|
42
|
+
* if (axis.ok) {
|
|
43
|
+
* axis.start.commitIdx; // -1 — the fold base
|
|
44
|
+
* axis.stages.map((s) => s.label);
|
|
45
|
+
* axis.end.lastCommitIdx; // the last bundle in the log
|
|
46
|
+
* } else {
|
|
47
|
+
* axis.reason; // 'empty' | 'not-bookended'
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
function splitAxis(stops) {
|
|
52
|
+
const kinds = Object.freeze(stops.map((stop) => stop.kind));
|
|
53
|
+
if (stops.length === 0)
|
|
54
|
+
return { ok: false, reason: 'empty', kinds };
|
|
55
|
+
const start = stops[0];
|
|
56
|
+
const end = stops[stops.length - 1];
|
|
57
|
+
if (start.kind !== 'start' || end.kind !== 'end' || stops.length < 2) {
|
|
58
|
+
return { ok: false, reason: 'not-bookended', kinds };
|
|
59
|
+
}
|
|
60
|
+
return { ok: true, start, stages: Object.freeze(stops.slice(1, -1)), end };
|
|
61
|
+
}
|
|
62
|
+
exports.splitAxis = splitAxis;
|
|
63
|
+
/**
|
|
64
|
+
* `stop` retyped to the OUTPUT vocabulary, with any meta it already carried
|
|
65
|
+
* left behind. The input axis may itself be a filtered one (a Beat axis being
|
|
66
|
+
* narrowed into a Tag axis), and a `true` decision keeps the STOP, not the
|
|
67
|
+
* other strategy's classification: a `Stop<Tag>` whose `meta` is a Beat would
|
|
68
|
+
* be a lie the type could not catch. Only the decision may supply a meta.
|
|
69
|
+
*/
|
|
70
|
+
function retyped(stop) {
|
|
71
|
+
if (stop.meta === undefined)
|
|
72
|
+
return stop;
|
|
73
|
+
const out = {};
|
|
74
|
+
const fields = stop;
|
|
75
|
+
for (const key of Object.keys(fields))
|
|
76
|
+
if (key !== 'meta')
|
|
77
|
+
out[key] = fields[key];
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Build a FILTERING strategy's axis out of another strategy's stops.
|
|
82
|
+
*
|
|
83
|
+
* The rule, in one sentence: keep the stages `keep` recognises, and give the
|
|
84
|
+
* survivors the log back.
|
|
85
|
+
*
|
|
86
|
+
* **Meta is the decision's, never inherited.** The input may already be a
|
|
87
|
+
* filtered axis carrying another strategy's `meta`; a `true` (or a meta-less
|
|
88
|
+
* object) keeps the stop and DROPS that meta, so a `Stop<TMeta>` never carries
|
|
89
|
+
* a value of some other type under `TMeta`. The bookends carry none either.
|
|
90
|
+
*
|
|
91
|
+
* **The survivors re-partition the log.** A dropped stage's commits still
|
|
92
|
+
* happened, so they fold into the stop that PRECEDES them: every kept stop's
|
|
93
|
+
* `lastCommitIdx` runs to just before the next kept stop begins. Nothing is
|
|
94
|
+
* orphaned and `stateAt(stop)` stays "the state that existed when the next
|
|
95
|
+
* kept stage started".
|
|
96
|
+
*
|
|
97
|
+
* **What that costs `'start'`, said out loud.** The commits before the first
|
|
98
|
+
* survivor have nowhere else to go, so `'start'` absorbs them: its fold is the
|
|
99
|
+
* run's base PLUS that prologue — the state the first stop READ, not the raw
|
|
100
|
+
* base. The returned start therefore carries `prologue: true` whenever it
|
|
101
|
+
* actually absorbed a stage, and a renderer that means "before anything ran"
|
|
102
|
+
* checks `kind === 'start' && !prologue`. That flag is the whole reason this
|
|
103
|
+
* helper exists rather than a `.filter()` call.
|
|
104
|
+
*
|
|
105
|
+
* **A log with nothing kept** yields the two bookends and nothing between
|
|
106
|
+
* them: `'start'` folds the whole log, `'end'` folds the whole log, and
|
|
107
|
+
* `jumpTo` on any stage id refuses with `reason: 'miss'`. An axis with nowhere
|
|
108
|
+
* meaningful to stand — which is the truthful shape for a run this strategy
|
|
109
|
+
* recognises nothing in, and NOT the `[]` that says the log was empty.
|
|
110
|
+
*
|
|
111
|
+
* @throws when `stops` is non-empty but not bookended — see {@link splitAxis}.
|
|
112
|
+
* Only a strategy can cause that; a run cannot, so it is a contract error
|
|
113
|
+
* and not a condition to fold into the data.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```typescript
|
|
117
|
+
* import { commitStops, filterStops } from 'foottrace';
|
|
118
|
+
* import type { Stop, TimeTravelStrategy } from 'foottrace';
|
|
119
|
+
*
|
|
120
|
+
* type Milestone = { kind: 'llm-turn' | 'tool-call'; label: string };
|
|
121
|
+
* declare function milestoneFor(runtimeStageId: string): Milestone | null;
|
|
122
|
+
*
|
|
123
|
+
* const milestoneStops: TimeTravelStrategy<Milestone> = {
|
|
124
|
+
* stopsFor: (log, tree) =>
|
|
125
|
+
* filterStops<Milestone>(commitStops(log, tree), (stop) => {
|
|
126
|
+
* const m = milestoneFor(stop.runtimeStageId);
|
|
127
|
+
* return m ? { label: m.label, meta: m } : null;
|
|
128
|
+
* }),
|
|
129
|
+
* };
|
|
130
|
+
*
|
|
131
|
+
* const cursor = timeTravel(snapshot, { strategy: milestoneStops });
|
|
132
|
+
* cursor.at()?.meta?.kind; // 'llm-turn' — no re-derivation
|
|
133
|
+
* cursor.stops[0].prologue; // true when stages ran before the first turn
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
function filterStops(stops, keep) {
|
|
137
|
+
const axis = splitAxis(stops);
|
|
138
|
+
if (!axis.ok) {
|
|
139
|
+
if (axis.reason === 'empty')
|
|
140
|
+
return [];
|
|
141
|
+
throw new Error('filterStops: expected a bookended axis — [start, …stages, end] — got kinds ' +
|
|
142
|
+
`[${axis.kinds.join(', ')}]. Only a strategy can produce that shape; a run cannot.`);
|
|
143
|
+
}
|
|
144
|
+
const kept = [];
|
|
145
|
+
for (const stop of axis.stages) {
|
|
146
|
+
const decision = keep(stop);
|
|
147
|
+
if (!decision)
|
|
148
|
+
continue;
|
|
149
|
+
if (decision === true)
|
|
150
|
+
kept.push({ stop });
|
|
151
|
+
else
|
|
152
|
+
kept.push({ stop, label: decision.label, meta: decision.meta });
|
|
153
|
+
}
|
|
154
|
+
// The log's last index, taken from the `'end'` bookend rather than a length:
|
|
155
|
+
// `end` is the one stop guaranteed to fold the whole log, and this function
|
|
156
|
+
// is never handed the log itself.
|
|
157
|
+
const lastIdx = axis.end.lastCommitIdx;
|
|
158
|
+
const first = kept[0];
|
|
159
|
+
const out = [
|
|
160
|
+
{
|
|
161
|
+
...retyped(axis.start),
|
|
162
|
+
step: 0,
|
|
163
|
+
lastCommitIdx: first ? first.stop.commitIdx - 1 : lastIdx,
|
|
164
|
+
// Only when a STAGE was actually absorbed. An axis that kept everything
|
|
165
|
+
// has the same start it always had, and says nothing it need not say.
|
|
166
|
+
...(first && first.stop.step > 1 ? { prologue: true } : {}),
|
|
167
|
+
...(!first && axis.stages.length > 0 ? { prologue: true } : {}),
|
|
168
|
+
},
|
|
169
|
+
];
|
|
170
|
+
for (const [i, row] of kept.entries()) {
|
|
171
|
+
const next = kept[i + 1];
|
|
172
|
+
out.push({
|
|
173
|
+
...retyped(row.stop),
|
|
174
|
+
step: out.length,
|
|
175
|
+
lastCommitIdx: next ? next.stop.commitIdx - 1 : lastIdx,
|
|
176
|
+
...(row.label !== undefined ? { label: row.label } : {}),
|
|
177
|
+
...(row.meta !== undefined ? { meta: row.meta } : {}),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
out.push({ ...retyped(axis.end), step: out.length });
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
exports.filterStops = filterStops;
|