footprintjs 9.48.2 → 10.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/AGENTS.md +12 -10
- package/CLAUDE.md +21 -23
- package/README.md +1 -1
- package/dist/advanced.js +71 -128
- package/dist/esm/advanced.d.ts +24 -58
- package/dist/esm/advanced.js +13 -51
- package/dist/esm/index.d.ts +5 -8
- package/dist/esm/index.js +3 -3
- package/dist/esm/lib/builder/FlowChartBuilder.d.ts +1 -1
- package/dist/esm/lib/builder/FlowChartBuilder.js +3 -3
- package/dist/esm/lib/builder/structure/StructureRecorder.d.ts +1 -1
- package/dist/esm/lib/builder/structure/StructureRecorder.js +1 -1
- package/dist/esm/lib/capture/index.d.ts +0 -1
- package/dist/esm/lib/capture/index.js +1 -2
- package/dist/esm/lib/engine/graph/prefixNodeTree.js +2 -2
- package/dist/esm/lib/engine/handlers/ResumeEntry.d.ts +3 -3
- package/dist/esm/lib/engine/handlers/ResumeEntry.js +4 -4
- package/dist/esm/lib/engine/handlers/SubflowExecutor.d.ts +1 -2
- package/dist/esm/lib/engine/handlers/SubflowExecutor.js +4 -8
- package/dist/esm/lib/engine/index.d.ts +1 -1
- package/dist/esm/lib/engine/index.js +1 -1
- package/dist/esm/lib/engine/narrative/CombinedNarrativeRecorder.d.ts +4 -4
- package/dist/esm/lib/engine/narrative/CombinedNarrativeRecorder.js +1 -1
- package/dist/esm/lib/engine/narrative/index.d.ts +1 -1
- package/dist/esm/lib/engine/narrative/index.js +1 -1
- package/dist/esm/lib/engine/narrative/narrativeTypes.d.ts +1 -11
- package/dist/esm/lib/engine/narrative/narrativeTypes.js +1 -1
- package/dist/esm/lib/engine/traversal/FlowchartTraverser.d.ts +2 -10
- package/dist/esm/lib/engine/traversal/FlowchartTraverser.js +4 -6
- package/dist/esm/lib/engine/traversalContext.d.ts +1 -1
- package/dist/esm/lib/engine/traversalContext.js +2 -2
- package/dist/esm/lib/engine/types.d.ts +2 -10
- package/dist/esm/lib/engine/types.js +1 -1
- package/dist/esm/lib/engine/walkSubflowSpec.js +2 -2
- package/dist/esm/lib/ids/reservedIds.js +2 -2
- package/dist/esm/lib/memory/DiagnosticCollector.js +2 -2
- package/dist/esm/lib/memory/StageContext.d.ts +2 -4
- package/dist/esm/lib/memory/StageContext.js +2 -2
- package/dist/esm/lib/memory/borrowedMutation.js +2 -2
- package/dist/esm/lib/memory/index.d.ts +6 -21
- package/dist/esm/lib/memory/index.js +4 -22
- package/dist/esm/lib/memory/redaction.d.ts +1 -1
- package/dist/esm/lib/memory/redaction.js +2 -2
- package/dist/esm/lib/memory/runPolicy.d.ts +3 -2
- package/dist/esm/lib/memory/runPolicy.js +1 -1
- package/dist/esm/lib/reactive/createTypedScope.js +2 -2
- package/dist/esm/lib/reactive/writeTraps.js +2 -2
- package/dist/esm/lib/recorder/ControlDepRecorder.d.ts +3 -2
- package/dist/esm/lib/recorder/ControlDepRecorder.js +3 -2
- package/dist/esm/lib/recorder/EmitRecorder.d.ts +1 -1
- package/dist/esm/lib/recorder/EmitRecorder.js +1 -1
- package/dist/esm/lib/recorder/InOutRecorder.js +2 -2
- package/dist/esm/lib/recorder/qualityTrace.d.ts +1 -1
- package/dist/esm/lib/recorder/qualityTrace.js +2 -2
- package/dist/esm/lib/runner/DeferredObserverTier.js +2 -2
- package/dist/esm/lib/runner/ExecutionRuntime.d.ts +4 -5
- package/dist/esm/lib/runner/ExecutionRuntime.js +2 -5
- package/dist/esm/lib/runner/FlowChartExecutor.d.ts +1 -1
- package/dist/esm/lib/runner/FlowChartExecutor.js +1 -1
- package/dist/esm/lib/runner/getSubtreeSnapshot.d.ts +1 -1
- package/dist/esm/lib/runner/getSubtreeSnapshot.js +4 -4
- package/dist/esm/lib/runner/resume.js +2 -2
- package/dist/esm/lib/runner/snapshot.js +3 -2
- package/dist/esm/lib/scope/ScopeFacade.js +2 -2
- package/dist/esm/lib/scope/state/zod/scopeFactory.js +2 -2
- package/dist/esm/recorders.d.ts +2 -3
- package/dist/esm/recorders.js +1 -1
- package/dist/esm/trace.d.ts +11 -57
- package/dist/esm/trace.js +10 -68
- package/dist/index.js +3 -3
- package/dist/lib/builder/FlowChartBuilder.js +5 -5
- package/dist/lib/builder/structure/StructureRecorder.js +1 -1
- package/dist/lib/capture/index.js +2 -4
- package/dist/lib/engine/graph/prefixNodeTree.js +5 -5
- package/dist/lib/engine/handlers/ResumeEntry.js +4 -4
- package/dist/lib/engine/handlers/SubflowExecutor.js +5 -9
- package/dist/lib/engine/index.js +1 -1
- package/dist/lib/engine/narrative/CombinedNarrativeRecorder.js +1 -1
- package/dist/lib/engine/narrative/index.js +1 -1
- package/dist/lib/engine/narrative/narrativeTypes.js +1 -1
- package/dist/lib/engine/traversal/FlowchartTraverser.js +8 -10
- package/dist/lib/engine/traversalContext.js +4 -4
- package/dist/lib/engine/types.js +1 -1
- package/dist/lib/engine/walkSubflowSpec.js +3 -3
- package/dist/lib/ids/reservedIds.js +5 -5
- package/dist/lib/memory/DiagnosticCollector.js +3 -3
- package/dist/lib/memory/StageContext.js +3 -3
- package/dist/lib/memory/borrowedMutation.js +3 -3
- package/dist/lib/memory/index.js +7 -42
- package/dist/lib/memory/redaction.js +3 -3
- package/dist/lib/memory/runPolicy.js +1 -1
- package/dist/lib/reactive/createTypedScope.js +3 -3
- package/dist/lib/reactive/writeTraps.js +4 -4
- package/dist/lib/recorder/ControlDepRecorder.js +3 -2
- package/dist/lib/recorder/EmitRecorder.js +1 -1
- package/dist/lib/recorder/InOutRecorder.js +3 -3
- package/dist/lib/recorder/qualityTrace.js +4 -4
- package/dist/lib/runner/DeferredObserverTier.js +3 -3
- package/dist/lib/runner/ExecutionRuntime.js +6 -9
- package/dist/lib/runner/FlowChartExecutor.js +1 -1
- package/dist/lib/runner/getSubtreeSnapshot.js +6 -6
- package/dist/lib/runner/resume.js +3 -3
- package/dist/lib/runner/snapshot.js +6 -5
- package/dist/lib/scope/ScopeFacade.js +3 -3
- package/dist/lib/scope/state/zod/scopeFactory.js +4 -4
- package/dist/recorders.js +1 -1
- package/dist/trace.js +18 -120
- package/dist/types/advanced.d.ts +24 -58
- package/dist/types/index.d.ts +5 -8
- package/dist/types/lib/builder/FlowChartBuilder.d.ts +1 -1
- package/dist/types/lib/builder/structure/StructureRecorder.d.ts +1 -1
- package/dist/types/lib/capture/index.d.ts +0 -1
- package/dist/types/lib/engine/handlers/ResumeEntry.d.ts +3 -3
- package/dist/types/lib/engine/handlers/SubflowExecutor.d.ts +1 -2
- package/dist/types/lib/engine/index.d.ts +1 -1
- package/dist/types/lib/engine/narrative/CombinedNarrativeRecorder.d.ts +4 -4
- package/dist/types/lib/engine/narrative/index.d.ts +1 -1
- package/dist/types/lib/engine/narrative/narrativeTypes.d.ts +1 -11
- package/dist/types/lib/engine/traversal/FlowchartTraverser.d.ts +2 -10
- package/dist/types/lib/engine/traversalContext.d.ts +1 -1
- package/dist/types/lib/engine/types.d.ts +2 -10
- package/dist/types/lib/memory/StageContext.d.ts +2 -4
- package/dist/types/lib/memory/index.d.ts +6 -21
- package/dist/types/lib/memory/redaction.d.ts +1 -1
- package/dist/types/lib/memory/runPolicy.d.ts +3 -2
- package/dist/types/lib/recorder/ControlDepRecorder.d.ts +3 -2
- package/dist/types/lib/recorder/EmitRecorder.d.ts +1 -1
- package/dist/types/lib/recorder/qualityTrace.d.ts +1 -1
- package/dist/types/lib/runner/ExecutionRuntime.d.ts +4 -5
- package/dist/types/lib/runner/FlowChartExecutor.d.ts +1 -1
- package/dist/types/lib/runner/getSubtreeSnapshot.d.ts +1 -1
- package/dist/types/recorders.d.ts +2 -3
- package/dist/types/trace.d.ts +11 -57
- package/package.json +6 -15
- package/dist/esm/lib/capture/freeze.d.ts +0 -91
- package/dist/esm/lib/capture/freeze.js +0 -314
- package/dist/esm/lib/capture/ownData.d.ts +0 -7
- package/dist/esm/lib/capture/ownData.js +0 -15
- package/dist/esm/lib/capture/valueKinds.d.ts +0 -57
- package/dist/esm/lib/capture/valueKinds.js +0 -142
- package/dist/esm/lib/ids/runtimeStageId.d.ts +0 -173
- package/dist/esm/lib/ids/runtimeStageId.js +0 -194
- package/dist/esm/lib/memory/EventLog.d.ts +0 -105
- package/dist/esm/lib/memory/EventLog.js +0 -148
- package/dist/esm/lib/memory/RecordFrame.d.ts +0 -232
- package/dist/esm/lib/memory/RecordFrame.js +0 -296
- package/dist/esm/lib/memory/SharedMemory.d.ts +0 -46
- package/dist/esm/lib/memory/SharedMemory.js +0 -88
- package/dist/esm/lib/memory/TransactionBuffer.d.ts +0 -521
- package/dist/esm/lib/memory/TransactionBuffer.js +0 -946
- package/dist/esm/lib/memory/admission.d.ts +0 -81
- package/dist/esm/lib/memory/admission.js +0 -153
- package/dist/esm/lib/memory/backtrack.d.ts +0 -286
- package/dist/esm/lib/memory/backtrack.js +0 -457
- package/dist/esm/lib/memory/commitLogUtils.d.ts +0 -217
- package/dist/esm/lib/memory/commitLogUtils.js +0 -384
- package/dist/esm/lib/memory/deltaEncoding.d.ts +0 -66
- package/dist/esm/lib/memory/deltaEncoding.js +0 -122
- package/dist/esm/lib/memory/equality.d.ts +0 -93
- package/dist/esm/lib/memory/equality.js +0 -262
- package/dist/esm/lib/memory/eventPosition.d.ts +0 -18
- package/dist/esm/lib/memory/eventPosition.js +0 -2
- package/dist/esm/lib/memory/honesty.d.ts +0 -93
- package/dist/esm/lib/memory/honesty.js +0 -92
- package/dist/esm/lib/memory/keyPaths.d.ts +0 -125
- package/dist/esm/lib/memory/keyPaths.js +0 -227
- package/dist/esm/lib/memory/logModel.d.ts +0 -76
- package/dist/esm/lib/memory/logModel.js +0 -364
- package/dist/esm/lib/memory/merge.d.ts +0 -26
- package/dist/esm/lib/memory/merge.js +0 -60
- package/dist/esm/lib/memory/pathOps.d.ts +0 -115
- package/dist/esm/lib/memory/pathOps.js +0 -290
- package/dist/esm/lib/memory/paths.d.ts +0 -52
- package/dist/esm/lib/memory/paths.js +0 -57
- package/dist/esm/lib/memory/placeholders.d.ts +0 -39
- package/dist/esm/lib/memory/placeholders.js +0 -40
- package/dist/esm/lib/memory/recordCommit.d.ts +0 -45
- package/dist/esm/lib/memory/recordCommit.js +0 -61
- package/dist/esm/lib/memory/scrub.d.ts +0 -53
- package/dist/esm/lib/memory/scrub.js +0 -74
- package/dist/esm/lib/memory/types.d.ts +0 -209
- package/dist/esm/lib/memory/types.js +0 -10
- package/dist/esm/lib/memory/utils.d.ts +0 -73
- package/dist/esm/lib/memory/utils.js +0 -159
- package/dist/esm/lib/memory/verbs.d.ts +0 -333
- package/dist/esm/lib/memory/verbs.js +0 -427
- package/dist/esm/lib/recorder/CommitRangeIndex.d.ts +0 -147
- package/dist/esm/lib/recorder/CommitRangeIndex.js +0 -207
- package/dist/esm/lib/scope/detectCircular.d.ts +0 -13
- package/dist/esm/lib/scope/detectCircular.js +0 -14
- package/dist/esm/lib/scope/recorders/summarizeValue.d.ts +0 -5
- package/dist/esm/lib/scope/recorders/summarizeValue.js +0 -6
- package/dist/esm/lib/slice/elementProvenance.d.ts +0 -81
- package/dist/esm/lib/slice/elementProvenance.js +0 -190
- package/dist/esm/lib/slice/forwardSliceForKey.d.ts +0 -88
- package/dist/esm/lib/slice/forwardSliceForKey.js +0 -243
- package/dist/esm/lib/slice/index.d.ts +0 -25
- package/dist/esm/lib/slice/index.js +0 -25
- package/dist/esm/lib/slice/keyIndex.d.ts +0 -137
- package/dist/esm/lib/slice/keyIndex.js +0 -293
- package/dist/esm/lib/slice/keyTimeline.d.ts +0 -49
- package/dist/esm/lib/slice/keyTimeline.js +0 -113
- package/dist/esm/lib/slice/keysReadSources.d.ts +0 -51
- package/dist/esm/lib/slice/keysReadSources.js +0 -95
- package/dist/esm/lib/slice/serialize.d.ts +0 -78
- package/dist/esm/lib/slice/serialize.js +0 -278
- package/dist/esm/lib/slice/sliceForKey.d.ts +0 -47
- package/dist/esm/lib/slice/sliceForKey.js +0 -88
- package/dist/esm/lib/slice/types.d.ts +0 -567
- package/dist/esm/lib/slice/types.js +0 -25
- package/dist/esm/lib/time-travel/axis.d.ts +0 -119
- package/dist/esm/lib/time-travel/axis.js +0 -179
- package/dist/esm/lib/time-travel/bundles.d.ts +0 -77
- package/dist/esm/lib/time-travel/bundles.js +0 -136
- package/dist/esm/lib/time-travel/chain.d.ts +0 -93
- package/dist/esm/lib/time-travel/chain.js +0 -188
- package/dist/esm/lib/time-travel/commitStops.d.ts +0 -55
- package/dist/esm/lib/time-travel/commitStops.js +0 -202
- package/dist/esm/lib/time-travel/index.d.ts +0 -22
- package/dist/esm/lib/time-travel/index.js +0 -21
- package/dist/esm/lib/time-travel/stateAt.d.ts +0 -102
- package/dist/esm/lib/time-travel/stateAt.js +0 -153
- package/dist/esm/lib/time-travel/tagStops.d.ts +0 -72
- package/dist/esm/lib/time-travel/tagStops.js +0 -102
- package/dist/esm/lib/time-travel/timeTravel.d.ts +0 -70
- package/dist/esm/lib/time-travel/timeTravel.js +0 -360
- package/dist/esm/lib/time-travel/types.d.ts +0 -470
- package/dist/esm/lib/time-travel/types.js +0 -23
- package/dist/esm/write.d.ts +0 -47
- package/dist/esm/write.js +0 -46
- package/dist/lib/capture/freeze.js +0 -320
- package/dist/lib/capture/ownData.js +0 -19
- package/dist/lib/capture/valueKinds.js +0 -146
- package/dist/lib/ids/runtimeStageId.js +0 -210
- package/dist/lib/memory/EventLog.js +0 -152
- package/dist/lib/memory/RecordFrame.js +0 -300
- package/dist/lib/memory/SharedMemory.js +0 -92
- package/dist/lib/memory/TransactionBuffer.js +0 -950
- package/dist/lib/memory/admission.js +0 -159
- package/dist/lib/memory/backtrack.js +0 -463
- package/dist/lib/memory/commitLogUtils.js +0 -399
- package/dist/lib/memory/deltaEncoding.js +0 -128
- package/dist/lib/memory/equality.js +0 -266
- package/dist/lib/memory/eventPosition.js +0 -3
- package/dist/lib/memory/honesty.js +0 -95
- package/dist/lib/memory/keyPaths.js +0 -243
- package/dist/lib/memory/logModel.js +0 -372
- package/dist/lib/memory/merge.js +0 -64
- package/dist/lib/memory/pathOps.js +0 -306
- package/dist/lib/memory/paths.js +0 -62
- package/dist/lib/memory/placeholders.js +0 -43
- package/dist/lib/memory/recordCommit.js +0 -65
- package/dist/lib/memory/scrub.js +0 -79
- package/dist/lib/memory/types.js +0 -11
- package/dist/lib/memory/utils.js +0 -178
- package/dist/lib/memory/verbs.js +0 -444
- package/dist/lib/recorder/CommitRangeIndex.js +0 -211
- package/dist/lib/scope/detectCircular.js +0 -21
- package/dist/lib/scope/recorders/summarizeValue.js +0 -10
- package/dist/lib/slice/elementProvenance.js +0 -195
- package/dist/lib/slice/forwardSliceForKey.js +0 -247
- package/dist/lib/slice/index.js +0 -42
- package/dist/lib/slice/keyIndex.js +0 -313
- package/dist/lib/slice/keyTimeline.js +0 -117
- package/dist/lib/slice/keysReadSources.js +0 -101
- package/dist/lib/slice/serialize.js +0 -286
- package/dist/lib/slice/sliceForKey.js +0 -93
- package/dist/lib/slice/types.js +0 -26
- package/dist/lib/time-travel/axis.js +0 -184
- package/dist/lib/time-travel/bundles.js +0 -144
- package/dist/lib/time-travel/chain.js +0 -193
- package/dist/lib/time-travel/commitStops.js +0 -206
- package/dist/lib/time-travel/index.js +0 -32
- package/dist/lib/time-travel/stateAt.js +0 -160
- package/dist/lib/time-travel/tagStops.js +0 -106
- package/dist/lib/time-travel/timeTravel.js +0 -364
- package/dist/lib/time-travel/types.js +0 -24
- package/dist/types/lib/capture/freeze.d.ts +0 -91
- package/dist/types/lib/capture/ownData.d.ts +0 -7
- package/dist/types/lib/capture/valueKinds.d.ts +0 -57
- package/dist/types/lib/ids/runtimeStageId.d.ts +0 -173
- package/dist/types/lib/memory/EventLog.d.ts +0 -105
- package/dist/types/lib/memory/RecordFrame.d.ts +0 -232
- package/dist/types/lib/memory/SharedMemory.d.ts +0 -46
- package/dist/types/lib/memory/TransactionBuffer.d.ts +0 -521
- package/dist/types/lib/memory/admission.d.ts +0 -81
- package/dist/types/lib/memory/backtrack.d.ts +0 -286
- package/dist/types/lib/memory/commitLogUtils.d.ts +0 -217
- package/dist/types/lib/memory/deltaEncoding.d.ts +0 -66
- package/dist/types/lib/memory/equality.d.ts +0 -93
- package/dist/types/lib/memory/eventPosition.d.ts +0 -18
- package/dist/types/lib/memory/honesty.d.ts +0 -93
- package/dist/types/lib/memory/keyPaths.d.ts +0 -125
- package/dist/types/lib/memory/logModel.d.ts +0 -76
- package/dist/types/lib/memory/merge.d.ts +0 -26
- package/dist/types/lib/memory/pathOps.d.ts +0 -115
- package/dist/types/lib/memory/paths.d.ts +0 -52
- package/dist/types/lib/memory/placeholders.d.ts +0 -39
- package/dist/types/lib/memory/recordCommit.d.ts +0 -45
- package/dist/types/lib/memory/scrub.d.ts +0 -53
- package/dist/types/lib/memory/types.d.ts +0 -209
- package/dist/types/lib/memory/utils.d.ts +0 -73
- package/dist/types/lib/memory/verbs.d.ts +0 -333
- package/dist/types/lib/recorder/CommitRangeIndex.d.ts +0 -147
- package/dist/types/lib/scope/detectCircular.d.ts +0 -13
- package/dist/types/lib/scope/recorders/summarizeValue.d.ts +0 -5
- package/dist/types/lib/slice/elementProvenance.d.ts +0 -81
- package/dist/types/lib/slice/forwardSliceForKey.d.ts +0 -88
- package/dist/types/lib/slice/index.d.ts +0 -25
- package/dist/types/lib/slice/keyIndex.d.ts +0 -137
- package/dist/types/lib/slice/keyTimeline.d.ts +0 -49
- package/dist/types/lib/slice/keysReadSources.d.ts +0 -51
- package/dist/types/lib/slice/serialize.d.ts +0 -78
- package/dist/types/lib/slice/sliceForKey.d.ts +0 -47
- package/dist/types/lib/slice/types.d.ts +0 -567
- package/dist/types/lib/time-travel/axis.d.ts +0 -119
- package/dist/types/lib/time-travel/bundles.d.ts +0 -77
- package/dist/types/lib/time-travel/chain.d.ts +0 -93
- package/dist/types/lib/time-travel/commitStops.d.ts +0 -55
- package/dist/types/lib/time-travel/index.d.ts +0 -22
- package/dist/types/lib/time-travel/stateAt.d.ts +0 -102
- package/dist/types/lib/time-travel/tagStops.d.ts +0 -72
- package/dist/types/lib/time-travel/timeTravel.d.ts +0 -70
- package/dist/types/lib/time-travel/types.d.ts +0 -470
- package/dist/types/write.d.ts +0 -47
- package/dist/write.js +0 -52
package/AGENTS.md
CHANGED
|
@@ -10,19 +10,21 @@ This is the footprint.js library — the flowchart pattern for backend code. Sel
|
|
|
10
10
|
|
|
11
11
|
## Architecture — Library of Libraries
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
FootPrint 10 has six engine doors; the record has three separate `foottrace` doors. Import from the package that owns the declaration; FootPrint does not re-export record declarations.
|
|
14
14
|
|
|
15
15
|
| Import | What it is for |
|
|
16
16
|
|---|---|
|
|
17
17
|
| `footprintjs` | The main door: `flowChart`, `FlowChartExecutor`, `decide` / `select`, `narrative`, the built-in recorder classes, `interrupt`, and the public types |
|
|
18
18
|
| `footprintjs/recorders` | Recorder factories — `narrative()`, `metrics()`, `debug()`, `manifest()`, `adaptive()`, `milestone()`, `windowed()` — and `CompositeRecorder` |
|
|
19
|
-
| `footprintjs/trace` |
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
19
|
+
| `footprintjs/trace` | Engine tracing: recorder stores (`KeyedStore`, `SequenceStore`, `BoundaryStateStore`), `walkSubflowSpec`, `topologyRecorder()` / `inOutRecorder()`, control-dependency and quality recorders |
|
|
20
|
+
| `foottrace` | Record shapes (`CommitBundle`, `TraceEntry`, `MemoryPatch`), runtime-ID helpers, commit queries, causal/variable slices, `stateAt`, `timeTravel`, `CommitRangeIndex`, `HONESTY_CODES` |
|
|
21
|
+
| `foottrace/write` | Writing a record yourself: the record layer the engine writes with — `SharedMemory` (the heap), `EventLog` (the log), `RecordFrame` (one step's frame) and their option types |
|
|
22
|
+
| `foottrace/paths` | Record-path encoding/decoding, safe nested access, `setNestedValue` / `updateNestedValue` for diagnostic bags |
|
|
23
|
+
| `footprintjs/advanced` | Engine internals (`StageContext`, `FlowchartTraverser`, `ScopeFacade`, scope providers, `SCOPE_METHOD_NAMES`, `ArrayMergeMode`, run policy and `RedactionRule`). No record declarations are exported here. |
|
|
22
24
|
| `footprintjs/detach` | Fire-and-forget child charts and their drivers |
|
|
23
25
|
| `footprintjs/zod` | Opt-in zod bridge (`defineScopeFromZod`, …) — the core never imports zod |
|
|
24
26
|
|
|
25
|
-
The module map
|
|
27
|
+
The engine module map and layering rule live in `CLAUDE.md` and `scripts/layering.config.cjs`, enforced by lint and `npm run check:layering`. The standalone record has its own fence in foottrace. Engine code must use named public foottrace imports, never a private path or a local copy of a record rule.
|
|
26
28
|
|
|
27
29
|
## Key API
|
|
28
30
|
|
|
@@ -464,7 +466,7 @@ for (const item of walkSubflowSpec(mounted!.subflowSpec!, mounted!.subflowPath ?
|
|
|
464
466
|
|
|
465
467
|
`CombinedNarrativeRecorder` flushes a stage's buffered reads and writes when that stage's flow event arrives: `onStageExecuted` for LINEAR stages, `onDecision` / `onSelected` / `onFork` / `onSubflowEntry` for the others.
|
|
466
468
|
|
|
467
|
-
## Execution Tracing (`
|
|
469
|
+
## Execution Tracing (record queries from `foottrace`)
|
|
468
470
|
|
|
469
471
|
Every stage execution gets a unique `runtimeStageId` — the universal key that links recorder events, commit log entries, and execution tree nodes.
|
|
470
472
|
|
|
@@ -483,7 +485,7 @@ tick#9 — same stageId, different execution (loop)
|
|
|
483
485
|
|
|
484
486
|
```typescript
|
|
485
487
|
import { flowChart, FlowChartExecutor, decide, getSubtreeSnapshot } from 'footprintjs';
|
|
486
|
-
import { parseRuntimeStageId, buildRuntimeStageId, splitStageId, findLastWriter, findCommit, findCommits, isCommitBundle } from '
|
|
488
|
+
import { parseRuntimeStageId, buildRuntimeStageId, splitStageId, findLastWriter, findCommit, findCommits, isCommitBundle } from 'foottrace';
|
|
487
489
|
|
|
488
490
|
const inner = flowChart<{ v: number; t?: number; out?: number }>('InnerStart', (scope) => { scope.t = scope.v + 1; }, 'inner-start')
|
|
489
491
|
.addFunction('InnerEnd', (scope) => { scope.out = (scope.t ?? 0) * 2; }, 'inner-end')
|
|
@@ -542,7 +544,7 @@ console.log(findCommit(commitLog, 'tick', 'count')?.runtimeStageId, findCommits(
|
|
|
542
544
|
// tick#6 [ 'tick#6', 'tick#9' ]
|
|
543
545
|
```
|
|
544
546
|
|
|
545
|
-
**
|
|
547
|
+
**Record queries and engine recorders have different owners.** In the table below, ID helpers, commit queries and `CommitRangeIndex` come from `foottrace`. `walkSubflowSpec`, stores, topology/in-out recorders and control-dependency/quality recorders remain on `footprintjs/trace`. The slice and time-travel contracts now live in the [foottrace repository](https://github.com/footprintjs/foottrace).
|
|
546
548
|
|
|
547
549
|
| Export | Returns | Use |
|
|
548
550
|
|--------|---------|-----|
|
|
@@ -857,7 +859,7 @@ console.log(live.isInFlight(), live.concurrent(), JSON.stringify(audit.forStep('
|
|
|
857
859
|
|
|
858
860
|
**`getEntryRanges()`** returns a precomputed `Map<runtimeStageId, {firstIdx, endIdx}>` maintained during `push()`. Use for O(1) per-step range lookups during time-travel scrubbing. Same shape as `buildEntryRangeIndex()` in `footprint-explainable-ui`.
|
|
859
861
|
|
|
860
|
-
**`CombinedNarrativeEntry.direction`** — subflow entries carry `direction: 'entry' | 'exit'` (and `subflowId`). Use for programmatic subflow boundary detection instead of text scanning (which breaks with a custom `NarrativeFormatter
|
|
862
|
+
**`CombinedNarrativeEntry.direction`** — subflow entries carry `direction: 'entry' | 'exit'` (and `subflowId`). Use for programmatic subflow boundary detection instead of text scanning (which breaks with a custom `NarrativeFormatter`).
|
|
861
863
|
|
|
862
864
|
**`footprint-explainable-ui` narrative utilities** (read from that package's source, 0.38.0) — for consumers building custom shells without `ExplainableShell`:
|
|
863
865
|
- `buildEntryRangeIndex(entries)` — build range index from flat array (when no recorder access)
|
|
@@ -1083,7 +1085,7 @@ Built on `CombinedRecorder`: `CombinedNarrativeRecorder` (the `executor.enableNa
|
|
|
1083
1085
|
|
|
1084
1086
|
## Anti-Patterns
|
|
1085
1087
|
|
|
1086
|
-
- Never post-process the tree — use recorders (or the `
|
|
1088
|
+
- Never post-process the tree — use recorders (or the `foottrace` queries over the recorded log)
|
|
1087
1089
|
- Don't use `getValue()`/`setValue()` for keys you know in TypedScope stages — use typed property access (`$getValue`/`$setValue` are for dynamic keys; plain `scope.getValue` does not exist there)
|
|
1088
1090
|
- Don't give a state key the name of a `$` method (`scope.$break = 1` throws "conflicts with a reserved TypedScope method") — the reserved names are `SCOPE_METHOD_NAMES` in `footprintjs/advanced`; avoid `$`-prefixed state keys altogether
|
|
1089
1091
|
- Don't write a state key that is also a `run({ input })` key — input keys are read-only for the run
|
package/CLAUDE.md
CHANGED
|
@@ -4,39 +4,37 @@
|
|
|
4
4
|
Self-explaining flowchart engine: a fluent builder emits a static chart; one DFS pass executes it while observer channels + a commit log capture everything ("collect during traversal, never post-process"). This file maps seams and blast radius; for API description read the code/README — **trust the code** where any doc disagrees.
|
|
5
5
|
|
|
6
6
|
## Module map
|
|
7
|
-
Entry points: `footprintjs` (
|
|
7
|
+
Entry points: `footprintjs` (engine API), `/recorders`, `/trace` (engine recorders, stores and structure walkers), `/advanced` (engine internals), `/detach`, `/zod`. The separate `foottrace` package owns records: root for readers/types/IDs, `/write` for writer primitives, `/paths` for path rules and nested diagnostic writes. No record declaration is re-exported by FootPrint 10. See `docs/design/2026-10-trace-extraction.md` for the extraction design and migration map.
|
|
8
8
|
|
|
9
9
|
| src/lib/ | one job |
|
|
10
10
|
|---|---|
|
|
11
|
-
| capture/ | retention policies
|
|
12
|
-
| memory/ |
|
|
11
|
+
| capture/ | engine retention policies, bounded payload summarization, hook invocation and dev-mode helpers. Value-kind classification and record freezing now belong to foottrace. |
|
|
12
|
+
| memory/ | engine composition and policy: StageContext composes `RecordFrame` from `foottrace/write`; ExecutionRuntime owns run state and observer wiring; run addresses, redaction verdicts, frame types and diagnostics remain here. DiagnosticCollector uses `foottrace/paths`; replay/write rules are not copied here. |
|
|
13
13
|
| schema/ | duck-typed validation (`.safeParse/.parse` ⇒ 'parseable') |
|
|
14
14
|
| builder/ | fluent DSL → FlowChartSpec → StageNode graph; build-time StructureRecorder channel. NOT standalone: imports runner's makeRunnable (FlowChartBuilder.ts:18) |
|
|
15
15
|
| scope/ | ScopeFacade per stage (tracked get/set, $emit, frozen args/env); built-in ScopeRecorders in scope/recorders/ |
|
|
16
16
|
| reactive/ | TypedScope deep Proxy over the facade — four write laws + the honest boundary (what `find`/`for…of` hands back is raw and WARNED ABOUT in dev mode, not intercepted; a proxy held past its stage IS refused — `ScopeFacade` · `assertLive`); handles.ts = the registry of every proxy the factories built → the value behind it, asked wherever an app value enters the record (the set traps since 9.24.0 — no JSON round-trip any more; fan-out `resolveItems`, subflow in/out mappers, `$setValue`/`$update`/`$batchArray` since 9.23.3, after a fan-out over object items seeded the handle and lost every child); see src/lib/reactive/README.md |
|
|
17
17
|
| decide/ | decide()/select() evidence capture |
|
|
18
|
-
| recorder/ | stores (KeyedStore/SequenceStore/BoundaryStateStore), CombinedRecorder routing, built-
|
|
19
|
-
| slice/ | variable-first backward slicing (triage query layer): sliceForKey (anchor at last writer → causalChain) + arrayProvenance/elementProvenance (append-fold element births — the agent mega-key fix) + KeysReadSource strategies. Pure post-hoc queries; imports memory/ ONLY (record files; its tree is `ExecutionTree`, C6) (see src/lib/slice/README.md) |
|
|
20
|
-
| time-travel/ | the READER'S cursor over a finished trace (9.17.0): `timeTravel()` stops/moves/marks/`drill()`, `stateAt()` fold + honesty basis, `TimeTravelStrategy<TMeta>` seam (one shipped: `commitStops`). 9.18.0 composer's side: axis.ts (`splitAxis` = the ONE guard of the `[start,…stages,end]` contract; `filterStops` = filter + re-partition + `Stop.meta` vocabulary + `Stop.prologue` on an absorbing start — a FLAG, deliberately not a 5th StopKind), bundles.ts (per-bundle narrowing of `unknown[]` rows → index-holding GAPS in `FoldedState.skipped`), chain.ts (`timeTravel([paused, resumed])`: commit indices stay RUN-LOCAL + `Stop.sourceIdx`; refused unless execution indices are monotonic across legs and no id repeats). Read-time only — never a second live cursor. Record files only (`RECORD_FILES`, C6): memory/ + `ids/runtimeStageId` + `capture/freeze` + `capture/valueKinds`; the tree it reads is the record's `ExecutionTree`, a supertype of `StageSnapshot` (9.48.0) (see src/lib/time-travel/README.md) |
|
|
18
|
+
| recorder/ | engine stores (KeyedStore/SequenceStore/BoundaryStateStore), CombinedRecorder routing, and built-in Topology/InOut/ControlDep/Quality recorders. `CommitRangeIndex` is owned and exported by foottrace. |
|
|
21
19
|
| pause/ | PauseSignal / FlowchartCheckpoint types + `interrupt(scope, payload)` (throws InterruptSignal; converted to PauseSignal at the StageRunner boundary, `pausedBy:'interrupt'`) |
|
|
22
20
|
| engine/ | FlowchartTraverser + handlers/; narrative/ = the whole FlowRecorder channel (dispatcher + 9 strategies), not just text; `lib/ids/branchSegment.ts` (moved to the `ids/` leaf in 9.37.0) = the ONE owner of the generated-segment grammar (`<stageId>~<index>`, marker `~` RESERVED); `executeStage` (:720) = the ONE funnel every stage fn passes through — and therefore the ONE owner of the declarative `retry` loop; `handlers/ResumeEntry.ts` = the ONE owner of a resume's ONE-SHOT re-entry (M2, 9.28.0: start + one hop per subflow on the pause path, each level's dispatcher continuation read from the chart, waiting sibling pauses queued at their fan-out) |
|
|
23
21
|
| runner/ | FlowChartExecutor wiring — composes `attach.ts` (RunObservers) · `resume.ts` · `checkpoint.ts` · `snapshot.ts` · `options.ts` (F9, 9.41.0); (runtime, recorders, pause/resume, snapshot, DeferredObserverTier). RunContext.ts is a fluent run builder, NOT the threaded per-run context |
|
|
24
22
|
| observer-queue/ | pure deferred-delivery pipeline (engine imports IT, never reverse) |
|
|
25
23
|
| contract/, detach/ | I/O schema normalization (OpenAPI generation belongs to runner/RunnableChart); fire-and-forget child charts (drivers passed explicitly) |
|
|
26
24
|
|
|
27
|
-
**The fence
|
|
25
|
+
**The fence:** engine files import only their own layer or below, using `scripts/layering.config.cjs` as the single table for lint, the checker and architecture tests. Named engine exceptions remain explicit and live-checked. The record is no longer a local L0–L3 subset: it lives in the standalone foottrace repository, whose closed source graph cannot import this engine, Node APIs or other packages. All engine access to the record uses named imports from `foottrace`, `foottrace/write` or `foottrace/paths`, never package internals. Export tests reject any FootPrint door handing out a declaration owned by foottrace. See the extraction plan for the E3/E6 published-door gate.
|
|
28
26
|
|
|
29
27
|
## Core state & flow
|
|
30
|
-
- `SharedMemory` (
|
|
31
|
-
- `StageContext` (memory/StageContext.ts
|
|
32
|
-
- `CommitBundle
|
|
28
|
+
- `SharedMemory` (`foottrace/write`) is the copy-on-write live heap. StageContext decides the address with the engine's `runAddress` rule and composes a `RecordFrame`. Record encoding, staging, commit and replay laws are implemented only in foottrace; the engine supplies policy and commit identity.
|
|
29
|
+
- `StageContext` (`memory/StageContext.ts`) owns per-stage run context. `stageWrite` decides redaction and identity inheritance, passes the resulting scrub bytes to `RecordFrame.write`, then records the engine's write marks. Commit delegates to the record frame, delivers engine hooks, and releases stage-local tracking. `runId` is readonly; the engine decides the record address once.
|
|
30
|
+
- `CommitBundle` (`foottrace`) / `EventLog` (`foottrace/write`) retain the existing bundle/index and full/delta encoding law. Neither is a FootPrint export.
|
|
33
31
|
- `TraversalContext` (engine/narrative/types.ts:172) — THE per-event context (runId, runtimeStageId, parentRuntimeStageId, subflowPath, loopIteration); stamped on every FlowRecorder event.
|
|
34
32
|
- `runtimeStageId` = `[subflowPath/]stageId#executionIndex`; counter shared by reference into subflow traversers → globally unique; NOT reset on resume.
|
|
35
33
|
- Subflows get a fresh isolated ExecutionRuntime (SubflowExecutor.ts:119); inputMapper seeds, outputMapper merges back (arrays CONCAT unless `arrayMerge: Replace`).
|
|
36
34
|
|
|
37
35
|
**Retry (9.15.0):** `StageNode.retry` is a POLICY on a stage, not a kind — no new node type, no `computeNodeType` edit. The loop lives in `FlowchartTraverser.executeStage` (:720), so linear/streaming/pausable/decider/selector/fork-child/subflow-inner all get it from one implementation. A failed non-final attempt calls `StageContext.discardStaged()` (`RecordFrame · discard`, which drops the buffer, the first-touch base and readKeys, plus _stageWrites/_stageReads + _untrackedSources); the final attempt is today's law untouched. PauseSignal and abort are NEVER retried. One runtimeStageId + one CommitBundle per stage regardless of attempts; retries do not consume `maxIterations`.
|
|
38
36
|
|
|
39
|
-
**Declared tags (9.21.0):** `StageNode.tags?: readonly string[]` is a NAME on a stage, declared at build time — POLICY-like (the retry precedent): no node type, no `computeNodeType` edit, no type-union site, and the one prefixer (`engine/graph/prefixNodeTree.ts`, 9.37.0) spreads `{ ...node }` so it rides subflow prefixing untouched. ONE landing site `FlowChartBuilder.ts · applyTags` (twin of `applyRetryPolicy`) serves the `.tag(...names)` cursor modifier and every `options.tags` site; refuses empty/non-string/`~`/duplicate/double-declare at build. Stamp: `FlowchartTraverser.executeNodeStep` sets `context.tags = node.tags` beside `runtimeStageId` (unconditionally — a re-used context must not keep a prior node's names); `memory/recordCommit.ts · recordCommit` (C1, 9.44.3; was `StageContext · bundleFor`) stamps `tags` beside `untrackedSources` on BOTH commit paths and `StageContext · commit` RELEASES them, so a double-commit path (fork child fan-out repeat, mount exit) records it once — absent when empty (`CommitBundle.tags?`; pinned byte-identical to 9.20.0 by test/lib/engine/scenario/declared-tags-byte-identity.test.ts). NOT inherited by createNext/createChild; the one synthetic node that could lose it is `FlowChartExecutor.resume`'s stand-in for the paused stage — built by `runner/resume.ts · standInFor` from the paused node ITSELF (fn swapped), so it keeps the tags on both re-entries; it runs ONCE — a later visit of the paused id runs the chart's real node, which carries its own tags. The spec carries `tags` as-is (Map advertises the vocabulary; `RuntimeStructureManager.stageNodeToStructure` copies it for run-time-resolved nodes). Reader: `
|
|
37
|
+
**Declared tags (9.21.0):** `StageNode.tags?: readonly string[]` is a NAME on a stage, declared at build time — POLICY-like (the retry precedent): no node type, no `computeNodeType` edit, no type-union site, and the one prefixer (`engine/graph/prefixNodeTree.ts`, 9.37.0) spreads `{ ...node }` so it rides subflow prefixing untouched. ONE landing site `FlowChartBuilder.ts · applyTags` (twin of `applyRetryPolicy`) serves the `.tag(...names)` cursor modifier and every `options.tags` site; refuses empty/non-string/`~`/duplicate/double-declare at build. Stamp: `FlowchartTraverser.executeNodeStep` sets `context.tags = node.tags` beside `runtimeStageId` (unconditionally — a re-used context must not keep a prior node's names); `memory/recordCommit.ts · recordCommit` (C1, 9.44.3; was `StageContext · bundleFor`) stamps `tags` beside `untrackedSources` on BOTH commit paths and `StageContext · commit` RELEASES them, so a double-commit path (fork child fan-out repeat, mount exit) records it once — absent when empty (`CommitBundle.tags?`; pinned byte-identical to 9.20.0 by test/lib/engine/scenario/declared-tags-byte-identity.test.ts). NOT inherited by createNext/createChild; the one synthetic node that could lose it is `FlowChartExecutor.resume`'s stand-in for the paused stage — built by `runner/resume.ts · standInFor` from the paused node ITSELF (fn swapped), so it keeps the tags on both re-entries; it runs ONCE — a later visit of the paused id runs the chart's real node, which carries its own tags. The spec carries `tags` as-is (Map advertises the vocabulary; `RuntimeStructureManager.stageNodeToStructure` copies it for run-time-resolved nodes). Reader: foottrace's `tagStops` = `filterStops(commitStops(...), any-of over log[stop.commitIdx].tags, meta = the array)`, exported from `foottrace`. Never a runtime `$tag()` — a value would ride past every redaction point. Design: docs/design/2026-09-declared-tags.md.
|
|
40
38
|
|
|
41
39
|
**Event order (code-verified; older docs had 3/4 swapped):** onStageStart → onRead/onWrite (live, PRE-commit) → **onStageEnd (StageRunner.ts:81) → onCommit (`StageContext · commit`, after `RecordFrame · commit`)** → FlowRecorder onDecision/onFork/onSelected/onSubflowEntry → onStageExecuted (uniform, with stageType). Error path: commit STILL happens (FlowchartTraverser.ts:1088) before onError+rethrow — a failing stage's writes land. For ANY thrown value (null, null-prototype, hostile Proxy) catch blocks describe it with `errors/errorInfo · thrownText` (never throws), onError fires, and run() rejects with the ORIGINAL value; a failed commit (e.g. an uncloneable write) also fires onError (`handlers/commitStage.ts`, 9.40.0). `interrupt()` rides the ERROR path's shape, not the pausable one: the stage function threw, so onStageEnd does NOT fire (the `addPausableFunction` pause DOES fire it — there the function returned normally).
|
|
42
40
|
|
|
@@ -53,15 +51,15 @@ Entry points: `footprintjs` (public API) · `/recorders` (factories) · `/trace`
|
|
|
53
51
|
- **Closed seams that look open**: ExecutionEnv (fixed type); decide() operators (evaluator.ts:19 module-private); METHOD_ROUTES; the executeNodeStep phase chain; emit dispatcher (rides scope channel).
|
|
54
52
|
|
|
55
53
|
## Change-impact map
|
|
56
|
-
- **Nested diagnostic writes** → `
|
|
57
|
-
- **Trace verbs** (`set|merge|append|delete`, `
|
|
54
|
+
- **Nested diagnostic writes** → `DiagnosticCollector` uses the public `foottrace/paths` nested writers. They refuse denied selector segments, create own properties without inherited setters, and keep diagnostic concatenation/shallow-merge semantics separate from record union-merge semantics. Path rules have one implementation in foottrace; the engine does not copy them.
|
|
55
|
+
- **Trace verbs** (`set|merge|append|delete`, `TraceEntry.verb`) → the record law lives only in foottrace. Its writer, replay, keyed fold, provenance and unknown-verb refusal share that implementation. Use the public record doors; do not add a verb switch to the engine. The canonical record contract and record-only differential controls moved with the package; FootPrint retains the engine byte-identity and admission witnesses.
|
|
58
56
|
- **Write address** → ONE constant, `runAddress.ts · RUN_NAMESPACE` (an L4 leaf, C4), read by `runAddress` / `useAddressOf` and by `RedactionRule · verdictOfRead` / `retainState`; the frame's `RecordFrame` holds the address (C3). `SharedMemory` and the nested writers take the address as data and refuse a non-array; `TransactionBuffer` takes it too (C2, 9.45.0). `test/architecture/write-address.test.ts` refuses the namespace below L4, as a literal or a property — no exceptions since C4.
|
|
59
57
|
- **StageContext.commit / dials** → ONE owner since 9.35.0 (F5): `memory/runPolicy.ts` — a frozen `RunPolicy` (the four dials + the `RedactionRule` + the mirror flag) built ONCE by `FlowChartExecutor · createTraverser` (run AND resume), handed to `ExecutionRuntime`, held BY REFERENCE by every `StageContext` (`inheritRun` in createNext/createChild) and by its `RecordFrame` as its encoding (`useEncoding`, through `StageContext · adoptPolicy`, the one place a frame's policy changes) and passed by `SubflowExecutor · executeSubflow` to the nested runtime BEFORE the seed. A fifth dial = the policy type + its consumer. The old per-dial `use*`/`get*` and `enableRedactedMirror` were removed in 9.36.0 (no compat shims — owner ruling). `commit()`'s FIRST act (9.23.0) is `materialiseWrites` — the `writeTracking` retained form is taken there, once per key, from `_pendingWrites` (bare references + the write-time verdict); anything that reads `_stageWrites` goes through `retainedWrites()` (a non-consuming fold, so a mid-run snapshot is honest), and `discardStaged` drops the pending map with the rest.
|
|
60
|
-
- **runtimeStageId format** (`#`/`/` delimiters) →
|
|
58
|
+
- **runtimeStageId format** (`#`/`/` delimiters) → foottrace owns build/parse/read and the brands `RuntimeStageId`/`ExecutionIndex`/`CommitIdx`, all on its root door. The engine's `ids/reservedIds.ts` owns `refuseReservedId`, and `ids/branchSegment.ts` owns the generated `~` grammar. Every engine reader uses the public codec; no local parsing copy. R5: every ID door (22 at build, plus the run-time dynamic-StageNode door) refuses `#` and `/`; stage IDs may contain `~`. `TraversalContext` has one constructor (`engine/traversalContext.ts`), with depth read from the runtime ID on every stamp.
|
|
61
59
|
- **Subflow id prefixing** → ONE prefixer since 9.37.0: `engine/graph/prefixNodeTree.ts`, used by the builder AND the traverser (eager, lazy and `parallelForEach` mounts — lazy/branch roots were unprefixed before 9.37.0, so branch ids collided); change it + stageMap key composition + resume drilling; the builder's eager mounts all go through `FlowChartBuilder · _mountSubflow` (F10). test/lib/engine/branch-segment-prefixer-equivalence.test.ts stays as a guard.
|
|
62
60
|
- **Checkpoint shape** (format 2 since 9.44.0, LEAN: only what resume reads + the pause's record — no `executionTree`/`subflowResults`; size independent of run length, pinned by test/lib/pause/checkpoint-size.test.ts; 9.39–9.43 refuse format 2, so upgrade readers before writers) → `runner/checkpoint.ts · buildPauseCheckpoint` ↔ `pause/record.ts · decodeCheckpoint` (one upcaster drops v0/v1 fields) ↔ `runner/resume.ts · planResume` (validation via `pause/record.ts · decodeCheckpoint`) ↔ `ResumeEntry.plan` (the path's mounts + captures + dispatcher continuations, the `pendingPauses` queue; refused up front when unwalkable, ambiguous, or a pending record is not a sibling) ↔ `SubflowExecutor · executeSubflow` resume hop (seed from the capture in place of the mapper's values — the mapper still runs, for the args — FIRST entry only) ↔ `ChildrenExecutor · raisePauses` (fills `pendingPauses`). 9.28.0 adds ONE optional field, `pendingPauses` (present only when parallel siblings wait); the redaction law adds `redactionMarks` (names only, present only when the run marked something); every stored checkpoint enters through `pause/record.ts · decodeCheckpoint` (`checkpointVersion: 1`, one upcaster from unversioned v0 which drops the legacy `continuationStageId`, one validator, one message set; 9.39.0). Pinned: a 9.27.0 checkpoint resumes, test/lib/pause/resume-real-chart-9.27.0-checkpoints.test.ts.
|
|
63
|
-
- **Committed-state writers**
|
|
64
|
-
- **Redaction** (9.19.0; the law restored to owner ruling (a)) has ONE owner of the DECISION, `memory/redaction.ts` (L4 since C4): `RedactionRule` (verdict per user-level path: `clear | whole | fields`) and, per staged write, `decideWrite` → `inheritByIdentity` → (the record writes) → `markStagedWrite` (the marks, only once the write staged — a failed `$update` leaves no mark in `redactionMarks` or the report); the BYTES are the record's (`RecordFrame · write` registers `scrubOf(verdict)`, `memory/scrub.ts · scrubPatch`
|
|
61
|
+
- **Committed-state writers** → copy-on-write staging and commit belong to foottrace. The engine composes `RecordFrame` rather than editing its internals or the shared heap. Engine tests retain the committed-generation, borrowed-reference and counted-cost witnesses; record-only mutation controls live with the record implementation.
|
|
62
|
+
- **Redaction** (9.19.0; the law restored to owner ruling (a)) has ONE owner of the DECISION, `memory/redaction.ts` (L4 since C4): `RedactionRule` (verdict per user-level path: `clear | whole | fields`) and, per staged write, `decideWrite` → `inheritByIdentity` → (the record writes) → `markStagedWrite` (the marks, only once the write staged — a failed `$update` leaves no mark in `redactionMarks` or the report); the BYTES are the record's (`RecordFrame · write` registers `scrubOf(verdict)`, foottrace's `memory/scrub.ts · scrubPatch` writes the log's placeholder at commit; no record file imports `redaction.ts`), carried on the run's `RunPolicy` with the four dials (F5). THE LAW: a policy covers EVERYTHING the library retains or serves — log (both encodings), mirror, stageReads/stageWrites, every recorder event inline and deferred (and the rows built from them), narrative, snapshots, diagnostics, pause payloads, boundary records, logger lines — and NEVER the live heap or the resume checkpoint. The two true exceptions are the caller's own values: the `run()` rejection and the live fork result. It selects by NAME, never by content: state by path (`verdict`); a record handed out whole (root input/output — `FlowchartTraverser.execute`; subflow seed/exit — `SubflowExecutor`; pause payload — `engine/handlers/servedPause.ts`; thrown value — `retainStageError`) by its keys at every depth plus declared `fields` under a key of that name (`retainBoundary` — one decision per PATH via `servedByPath`: copy-on-write per path, the visited set only the current path's ancestor stack, a cycle edge landing on the served copy; a path rule is exact under shared references, a key-NAME rule masks a shared node everywhere); a diagnostic entry as the record `{ [name]: value }` (`retainDiagnostic`; `policy.diagnostics` ADDS diagnostic-only selectors, the only ones that reach flow-message text); a subflow mapper's copy by TAINT (`MapperTaint`, asked by `SubflowInputMapper` for both mappers; the `parallelForEach` items selector counts selected reads via `StageContext · selectedReads` and marks `item`/`into`): a value passed under its own name keeps that name's verdict, an object by reference (or the whole handed record, as fields) keeps its source's, anything computed after a selected read is marked whole (conservative); never value equality across names. A mark is a NAME, run-wide, until the key is deleted — and a pause carries the marks (names only) to the resumed run (`FlowchartCheckpoint.redactionMarks`, checked by `pause/record.ts`). A thrown value is walked with its `cause`/`errors`; an object a mapper passes on by reference is scanned for the selected values it read (planted → fields). Name-only patterns (`needsPath` false — no `.`, lookaround, or escape/class that can match `.`) are decided per object, so the walk shares decisions per (object, targets) — linear on a DAG or a linked history; only a path-dependent pattern walks paths, and past 1,000,000 visits it serves the unvisited remainder as the placeholder (one dev-mode warning), never fails the run. Inside a stage, an OBJECT read under a selected name and written under another keeps its rule (`redaction.ts · inheritByIdentity`, by identity — decided before the write); a primitive or a new object copied across names is selected by its own name only. `StageContext.stageWrite` is the ONE write funnel (facade writes, subflow seed, outputMapper merge-back, resume re-seed) and `getValue` retention asks the same rule; the facade asks it for read/write recorder payloads. An inert rule never enumerates a payload. `emitPatterns` is the one separate scrub (`$emit` payloads, by event name). Two placeholders, historical, one per owner: log/mirror `'REDACTED'` (`placeholders.ts · LOG_PLACEHOLDER`, a record byte), scope-tier `'[REDACTED]'` (`redaction.ts · SCOPE_PLACEHOLDER`). See `docs/guides/scope.md` → "The one law". Capture the retained form at runtime; no second tree walk or recorder-local policy.
|
|
65
63
|
- **Diagnostic retention** → `DiagnosticCollector` asks the current run rule before merging/storing: a named entry maps to the state key of its name (the run's keys/patterns/marks/fields, at every depth of the value), then the opt-in `RedactionPolicy.diagnostics` selectors (same path algorithm, rooted at the channel). Its internal retained-write bridge supplies legacy facade emits. Existing writers stay void. Only flow-message description/rationale are payloads (no name — diagnostic selectors only); keys/topology/timing stay present. Dynamic capture uses a per-visit engine flag, not a diagnostic bag. A redacted `logs.deciderRationale` also reaches decision explanations. Checkpoints keep retained diagnostic bags but preserve operational state; no retroactive scrub. See the scope guide's diagnostic contract.
|
|
66
64
|
- **Traverser phases** → node-shape reads must use eff* overlay accessors (:646-703) or dynamic StageNode returns break; tail continuations must return ContinuationHops or stacks regrow; decider flat-dispatch must keep InvokerStamp or pause loses its invoker.
|
|
67
65
|
|
|
@@ -71,17 +69,17 @@ run: executor.run (FlowChartExecutor.ts:1455): re-entrancy guard → fresh runId
|
|
|
71
69
|
per stage: executeNodeStep stamps runtimeStageId (:818) → StageRunner.run: scopeFactory → onStageStart → stage fn (writes stage into TransactionBuffer via StageContext.setObject; onWrite fires live) → onStageEnd → traverser calls context.commit() (:1094) → onCommit → onStageExecuted (narrative flushes buffered ops here).
|
|
72
70
|
decider: DeciderHandler.prepareDispatch (:89) — runs stage, **commits BEFORE branch resolution** (:124), matches branchId, fires onDecision(+evidence) → onStageExecuted('decider') → flat hop with InvokerStamp.
|
|
73
71
|
loop: Phase 6 sees isLoopRef → ContinuationResolver.resolveTarget (:107): id-map lookup, iteration guard (max 1000), onLoop → hop; zero stack, state carries forward (never rewound). A decider WITH its own next runs its branch as a BRANCH FRAME (9.40.0): a `loop`-flagged hop leaves the frame only when an ENCLOSING driver already ran the target — the decider follows it flat and skips its next, which runs once after the non-looping pass; any other jump stays in the frame. A selector's own loopTo resolves the same way and fires onLoop.
|
|
74
|
-
end: onRunEnd (throw → onRunFailed; pause → neither) → deferred terminalFlush → getSnapshot() = {sharedState (LIVE view; dev-mode frozen clone), initialState (the fold base), commitLog (detached+frozen; a bundle or base holding what freezing cannot seal — a Date, Map, buffer … — is served as a copy of its open paths, `runner/snapshot.ts · servedSnapshot` → `
|
|
72
|
+
end: onRunEnd (throw → onRunFailed; pause → neither) → deferred terminalFlush → getSnapshot() = {sharedState (LIVE view; dev-mode frozen clone), initialState (the fold base), commitLog (detached+frozen; a bundle or base holding what freezing cannot seal — a Date, Map, buffer … — is served as a copy of its open paths, `runner/snapshot.ts · servedSnapshot` → `foottrace · serveRecord`, 9.44.2), executionTree, subflowResults (dual-keyed; `globalContext` = the subflow's LIVE heap, or under `redact: true` its own nested mirror — 9.20.0, `servedSubflowResults`), recorders}.
|
|
75
73
|
|
|
76
74
|
## Backtracking
|
|
77
|
-
Seven mechanisms, no state rollback anywhere: **M1** TransactionBuffer staging + net-change commit — commit-on-error by design, explicitly NOT rollback (TransactionBuffer.ts:13-18; error path commits then rethrows). **M2** Pause/Resume checkpointing — the only resume-from-prior-point; TWO raise shapes, ONE checkpoint: `addPausableFunction` (resume runs `resumeFn`) and `interrupt(scope, payload)` (resume RE-RUNS THE STAGE FUNCTION FROM ITS TOP — stages are atomic; the answer returns out of the `interrupt()` call via a WeakMap keyed by the scope), discriminated by `checkpoint.pausedBy`; checkpoint is one detached structuredClone; resume rebuilds the cursor from pausedStageId+subflowPath and re-enters ONCE (9.28.0, `ResumeEntry`): the paused stage's stand-in (its own node, fn swapped — so a dispatcher that interrupted still dispatches) is only where the resumed traversal STARTS — never in a node map — and each subflow on the path takes its capture + entry point on its FIRST entry (a 2+-deep pause re-enters each outer subflow AT the next mount; no pre-mount stage re-runs); every level's entry carries its DISPATCHER's continuation, read from the chart (a branch/fork-child mount hands back to its parent's `next`/join at the parent's level); parallel siblings that paused together wait in `checkpoint.pendingPauses` and are asked in turn, the join after the last; the rest of the run walks the REAL chart (a loop back to the paused stage reaches it and it pauses again; later entries run the inputMapper). Not rebuilt (pinned in test/lib/pause/resume-known-limitations.test.ts): lazy-subflow / parallelForEach-branch pauses (refused), a parallel-branch STAGE's namespace on resume, fan-out error containment for the resumed child. **M3** commit-log replay (EventLog.materialise, commitValueAt — required under delta mode). **M4** loopTo re-entry — forward execution over accumulated state, bounded by maxIterations + dynamicNextHops. **M5** causalChain backward slicing (read-only analysis; honesty flags for untracked reads; `edgeAttribution: 'per-write'` refines edges via `TraceEntry.readKeys` when the writeProvenance dial recorded them — worklist, subset-of-ceiling safe). **M6 (query layer)** slice/ — variable-first triage: sliceForKey + append-fold element provenance + the ONLY safe serializations (sliceToJSON/formatSlice — never JSON.stringify a slice root). **M7 (read-time cursor)** time-travel/ — `timeTravel(snapshot)` walks stops over the finished log with `stateAt()` folding M3's replay at each one (base = `RuntimeSnapshot.initialState`, which now travels with the log); it is a READER, never a second live cursor, and `drill(mount)` opens a separate cursor over a subflow's own isolated log. 9.18.0: `timeTravel([paused, resumed])` chains a cross-executor M2 resume into ONE axis — the resumed leg's `initialState` (= the state at the pause) restarts the fold, commit indices stay run-local (`Stop.sourceIdx` names the leg), and the chain is refused unless the never-reset execution counter is monotonic across legs. Deep dive + step tables: [.claude/rules/backtracking.md](.claude/rules/backtracking.md).
|
|
75
|
+
Seven mechanisms, no state rollback anywhere: **M1** TransactionBuffer staging + net-change commit — commit-on-error by design, explicitly NOT rollback (TransactionBuffer.ts:13-18; error path commits then rethrows). **M2** Pause/Resume checkpointing — the only resume-from-prior-point; TWO raise shapes, ONE checkpoint: `addPausableFunction` (resume runs `resumeFn`) and `interrupt(scope, payload)` (resume RE-RUNS THE STAGE FUNCTION FROM ITS TOP — stages are atomic; the answer returns out of the `interrupt()` call via a WeakMap keyed by the scope), discriminated by `checkpoint.pausedBy`; checkpoint is one detached structuredClone; resume rebuilds the cursor from pausedStageId+subflowPath and re-enters ONCE (9.28.0, `ResumeEntry`): the paused stage's stand-in (its own node, fn swapped — so a dispatcher that interrupted still dispatches) is only where the resumed traversal STARTS — never in a node map — and each subflow on the path takes its capture + entry point on its FIRST entry (a 2+-deep pause re-enters each outer subflow AT the next mount; no pre-mount stage re-runs); every level's entry carries its DISPATCHER's continuation, read from the chart (a branch/fork-child mount hands back to its parent's `next`/join at the parent's level); parallel siblings that paused together wait in `checkpoint.pendingPauses` and are asked in turn, the join after the last; the rest of the run walks the REAL chart (a loop back to the paused stage reaches it and it pauses again; later entries run the inputMapper). Not rebuilt (pinned in test/lib/pause/resume-known-limitations.test.ts): lazy-subflow / parallelForEach-branch pauses (refused), a parallel-branch STAGE's namespace on resume, fan-out error containment for the resumed child. **M3** commit-log replay (EventLog.materialise, commitValueAt — required under delta mode). **M4** loopTo re-entry — forward execution over accumulated state, bounded by maxIterations + dynamicNextHops. **M5** causalChain backward slicing (read-only analysis; honesty flags for untracked reads; `edgeAttribution: 'per-write'` refines edges via `TraceEntry.readKeys` when the writeProvenance dial recorded them — worklist, subset-of-ceiling safe). **M6 (query layer)** foottrace's slice/ — variable-first triage: sliceForKey + append-fold element provenance + the ONLY safe serializations (sliceToJSON/formatSlice — never JSON.stringify a slice root). **M7 (read-time cursor)** foottrace's time-travel/ — `timeTravel(snapshot)` walks stops over the finished log with `stateAt()` folding M3's replay at each one (base = `RuntimeSnapshot.initialState`, which now travels with the log); it is a READER, never a second live cursor, and `drill(mount)` opens a separate cursor over a subflow's own isolated log. 9.18.0: `timeTravel([paused, resumed])` chains a cross-executor M2 resume into ONE axis — the resumed leg's `initialState` (= the state at the pause) restarts the fold, commit indices stay run-local (`Stop.sourceIdx` names the leg), and the chain is refused unless the never-reset execution counter is monotonic across legs. Deep dive + step tables: [.claude/rules/backtracking.md](.claude/rules/backtracking.md).
|
|
78
76
|
|
|
79
77
|
## Invariants (assumed, not stated)
|
|
80
|
-
- **The admitted record (9.30.0)** — a commit's bundle, replayed onto the state its stage began from, gives back the stage's read-your-writes view (its working copy, by `deepEqual`, as a record can hold it — through `structuredClone`, so an `Error`'s dropped own fields never count) at every path it touched and at every container and array slot its writes made on the way there, below the stage's address (`runs/<id>` is where a namespaced stage writes, not a value it reads). VERIFIED for every commit that staged a `merge` or a nested op (`TransactionBuffer · admit` → `admission.ts · lossyFamilies`: one `dryFold`, no clone, families of TOUCHED paths compared at their roots); a family that does not fold back is re-encoded as `set` rows of the read-back at its last touch (`emitReadBack`). BY CONSTRUCTION for a stage of `set`/`delete` only, directly under its address (no fold). Every commit that already folded back keeps its bytes. NOT covered: an in-place edit of a value the stage read but never wrote at or above (M2 — still lost), and readers (`commitValueAt`) that disagree with the fold. Pinned by test/lib/memory/property/record-equals-read-back.property.test.ts (independent oracle) and, end to end on both engines, by the witness in property/copy-on-write-fixture.ts.
|
|
81
|
-
- Committed state immutable-after-swap — a generation is NEVER edited, and since 9.29.0 generations SHARE every untouched subtree (copy-on-write). First-touch views AND the transaction buffer's net-change diff base hold bare references on that guarantee — an in-place mutation of a committed container corrupts every generation that shares it and blinds the diff. Every writer copies before it writes (`pathOps · ownSpine` / `ownedRootOf`); pinned by the generations property in test/lib/memory/scenario/copy-on-write-commit.test.ts and the counted guard in test/lib/memory/boundary/commit-cost-independent-of-state.test.ts.
|
|
78
|
+
- **The admitted record (9.30.0)** — a commit's bundle, replayed onto the state its stage began from, gives back the stage's read-your-writes view (its working copy, by `deepEqual`, as a record can hold it — through `structuredClone`, so an `Error`'s dropped own fields never count) at every path it touched and at every container and array slot its writes made on the way there, below the stage's address (`runs/<id>` is where a namespaced stage writes, not a value it reads). VERIFIED for every commit that staged a `merge` or a nested op (foottrace's `TransactionBuffer · admit` → `admission.ts · lossyFamilies`: one `dryFold`, no clone, families of TOUCHED paths compared at their roots); a family that does not fold back is re-encoded as `set` rows of the read-back at its last touch (`emitReadBack`). BY CONSTRUCTION for a stage of `set`/`delete` only, directly under its address (no fold). Every commit that already folded back keeps its bytes. NOT covered: an in-place edit of a value the stage read but never wrote at or above (M2 — still lost), and readers (`commitValueAt`) that disagree with the fold. Pinned by test/lib/memory/property/record-equals-read-back.property.test.ts (independent oracle) and, end to end on both engines, by the witness in property/copy-on-write-fixture.ts.
|
|
79
|
+
- Committed state immutable-after-swap — a generation is NEVER edited, and since 9.29.0 generations SHARE every untouched subtree (copy-on-write). First-touch views AND the transaction buffer's net-change diff base hold bare references on that guarantee — an in-place mutation of a committed container corrupts every generation that shares it and blinds the diff. Every writer copies before it writes (foottrace's `pathOps · ownSpine` / `ownedRootOf`); pinned by the generations property in test/lib/memory/scenario/copy-on-write-commit.test.ts and the counted guard in test/lib/memory/boundary/commit-cost-independent-of-state.test.ts.
|
|
82
80
|
- Reads are borrowed live references (recorders get them un-cloned) — never mutate.
|
|
83
|
-
- The commit log is never the reader's to edit
|
|
84
|
-
- executionIndex globally monotonic per run; continues across resume. The FIRST CommitBundle per runtimeStageId is the stage's own: no `phase`, carries its tags. Only a later bundle of the same execution is a continuation, stamped by the WRITER (`phase: 'exit'` = a mount's exit after its merge-back; `phase: 'repeat'` = a fork child's fan-out settle), so a mount with no merge-back (lazy, `parallelForEach` branch, no `outputMapper`) is one bundle. Readers group by runtimeStageId and read `phase`, never the log's shape (9.39.0, F8); a pre-9.39.0 log is inferred only by `inferLegacyPhases` (on
|
|
81
|
+
- The commit log is never the reader's to edit. Foottrace owns `freezeRecord` (`foottrace/write`) and `serveRecord` (`foottrace`), including copy-on-serve handling for values freezing cannot seal. `runner/snapshot.ts · servedSnapshot` serves `getSnapshot().commitLog` / `.initialState` through that shared law. The engine's internal snapshot (`ExecutionRuntime · getSnapshot`, via `EventLog.recorded`, once per subflow mount) is not a served public record. Known open surfaces are unchanged by extraction: the advanced runtime snapshot, subflow results, execution-tree reads/writes/diagnostics/messages, recorder rows, checkpoints and narrative entries are not all covered by this served-record law. Pinned by `test/lib/memory/scenario/served-record-doors.test.ts` and its property companion.
|
|
82
|
+
- executionIndex globally monotonic per run; continues across resume. The FIRST CommitBundle per runtimeStageId is the stage's own: no `phase`, carries its tags. Only a later bundle of the same execution is a continuation, stamped by the WRITER (`phase: 'exit'` = a mount's exit after its merge-back; `phase: 'repeat'` = a fork child's fan-out settle), so a mount with no merge-back (lazy, `parallelForEach` branch, no `outputMapper`) is one bundle. Readers group by runtimeStageId and read `phase`, never the log's shape (9.39.0, F8); a pre-9.39.0 log is inferred only by `inferLegacyPhases` (on `foottrace`), only without a tree; tree-less, a `subflowResults` entry marks a mount. Empty commits are deliberate cursor stops.
|
|
85
83
|
- runtimeStageId stamped BEFORE the stage runs — the entire event-correlation model rests on scope+flow events sharing it.
|
|
86
84
|
- One executor = one run at a time (_isExecuting guard). State values must survive structuredClone.
|
|
87
85
|
- A resume re-enters ONCE (`ResumeEntry`, 9.28.0): the stand-in carries the paused stage's id but is only a START node — every graph lookup (node map, loop targets, subflow roots) must go through the real chart, never through it; a pause-path capture seeds its subflow's first entry only; what runs after an entry is read from the chart, never from the checkpoint. A paused-and-resumed run equals the never-paused run — same trace, same final state — for linear, decider-branch, selector-branch and fork-child placements at every depth and with parallel siblings pausing together (fast-check property, test/lib/pause/resume-real-chart.property.test.ts). Qualified: the RECORD differs by design (the stand-in is one more execution of the paused stage; the loop budget is per leg), and the three pinned limitations above hold.
|
|
@@ -95,7 +93,7 @@ Seven mechanisms, no state rollback anywhere: **M1** TransactionBuffer staging +
|
|
|
95
93
|
1. **NOT a landmine any more (9.24.0), recorded because it was one for eight releases:** TypedScope's set trap used to JSON-round-trip the ASSIGNED VALUE of every object write (Date→string, Map→{}, own undefined dropped) while `$setValue` did not — two write paths, two byte shapes. The round-trip existed only to keep the scope's own Proxies out of the buffer; `reactive/handles.ts` recognises them now (O(1)), so `writeTraps.ts · sinkSetTrap`, `arrayTraps.ts` (index set, `MUTATING_METHODS` arguments, element-proxy leaf) and `createTypedScope · assignStateKey` hand the value to the sink AS ASSIGNED and the buffer's `structuredClone` at commit is the one detaching step — both doors store the same bytes (pinned: test/lib/reactive/scenario/assigned-value-only.test.ts, a fast-check property over `fc.jsonValue()`). Consequences to keep in mind: (a) a value nothing can clone — a function, a foreign Proxy such as a framework's reactive object — now fails the stage at COMMIT through the trap too (it always did through `$setValue`; the trap used to JSON-copy it silently, dropping the function); (b) `s.k = o; o.x = 1` commits `x: 1` — what the stage reads back — the same law 9.23.0 gave `$setValue`, and since 9.30.0 (owner ruling R1, the admitted record) so does `$update`: an in-place edit of a merged value within its stage is recorded; (c) the shape work the round-trip did for free stays explicit: `delete arr[i]`, a write past the end and `length` growth spell holes as `null` (`arrayTraps` · `fillHoles`). The surrounding state reaches the buffer by reference and is detached with `structuredClone` at commit (9.22.0 / 9.23.0).
|
|
96
94
|
2. Fork "parent breaks when ALL children broke" is implemented (ChildrenExecutor.ts:53) but **unwired** — every live call site passes parentBreakFlag=undefined; fork breaks do NOT propagate in real runs.
|
|
97
95
|
3. **First bite CLOSED (9.23.0 — clone once at commit):** `TransactionBuffer` · `set` now stores the REFERENCE in `overwritePatch` too (it always did in `workingCopy`) and the record's copy is taken ONCE per surviving path when the payload leaves the buffer (`toChangeOnlyPayload` / `toDeltaPayload`); `StageContext` · `trackWrite` holds the reference + verdict in `_pendingWrites` and `materialiseWrites` takes the retained form once per key at `commit()`. So `$setValue(k, o); o.x = 1` commits `x: 1` — what the stage read back — instead of a stale write-time snapshot (pinned: test/lib/memory/scenario/clone-once-at-commit.test.ts). The law "the record never aliases a caller's object" holds at the commit boundary; a mutation AFTER commit reaches nothing. Two things to keep in mind: (a) the engine's OWN nested ops (`set a` then `merge a.b` — the buffer driven directly; the proxy writes root keys) must not edit through a shared container, because only `workingCopy` receives a nested merge's result — `detachHeldAncestors` clones the held ancestor in `overwritePatch` first (the design page missed this; the `set-merge-interleaved` 9.22.0 reference pins it); (b) an UNCLONEABLE value (function, foreign Proxy) now fails at COMMIT, not at the write — the run still fails loudly with the `DataCloneError`, the stage cannot catch it, and none of that stage's writes land. The scope's OWN proxies are not uncloneable values at the boundaries the library owns (9.23.3, `reactive/handles.ts`): a handle handed to the fan-out selector, a subflow mapper or `$setValue` is swapped for the value behind it first — a handle buried inside a hand-built value elsewhere (an `interrupt` payload, a `$emit` payload) still fails at the clone. The element-write loop's other O(N)-per-write cost, the array commit callbacks' whole-array JSON round-trip, went in the same release (landmine 1, second moved behaviour); what remains per element write is the array proxy's own shallow copy-on-write (`arrayTraps` · `replaceInElement` / the traps' `[...getCurrent()]`, ~9 µs at N = 10k) — bench 1k 5.7 ms, 10k 104 ms. **Second bite — a raw element held past its stage:** (within ONE stage, 9.29.0: a raw read BEFORE the stage's first write is the committed object itself — edited in place and written back it records NO change, because the diff base is that object (M7; dev mode warns, `StageContext · warnOnCommittedMutation`); a raw read AFTER the first write is the stage's private copy, `TransactionBuffer · privatise` — option D, 9.28.0's behaviour; one the working copy cannot answer (a key the stage deleted) is LIVE state, as on 9.28.0, with the diff base detached there first, `TransactionBuffer · detachBase`, so a write-back is recorded as it was.) A value obtained through `find`/`filter`/`for…of`/`$getValue`/`$read`/`$toRaw` in stage A and mutated in stage B is the committed object itself, so the mutation edits committed state with no row, and the dev-mode guard (`StageContext` · `warnOnBorrowedMutation`) sees it only if B also READS that key. Cannot be trapped; documented in src/lib/reactive/README.md ("A handle is bound to its stage"). A PROXY held that long is refused at the write (`ScopeFacade` · `assertLive`, sealed by the commit observer — the refusal lives on the facade, not `stageWrite`, because the frame's post-commit re-use is what the engine's double-commit paths need).
|
|
98
|
-
4. **NOT a landmine any more (9.22.0), recorded because it was one:** an indexed array read (`arr[i]`) used to hand back the RAW element, and a nested array write committed with the `merge` verb whose array arm is a set UNION. Both are fixed at the root — `reactive/arrayTraps.ts` wraps elements, and a nested array commits as a `set` of its ROOT KEY (`createTypedScope` · nested array commit callback). Consequences to keep in mind: an array ASSIGNMENT now REPLACES at every depth (`$update` is still the append), and a nested array write appears in `bundle.overwrite`/verb `set`, not `bundle.updates`/verb `merge`. What the proxy still cannot see — an element reached via `find`/`filter`/`for…of`/`forEach` — is REPORTED loudly instead: `StageContext` · `warnOnBorrowedMutation` (a dev-mode WARNING at the frame's first commit, under `readTracking: 'full'` only — a report after the fact, not a refusal). `memory/equality.ts` · `equalPairs` has one arm per value KIND (`capture/valueKinds.ts · kindOf`, the one classifier — every kind `structuredClone` keeps since 9.44.2; before it, a replaced RegExp/Error/boxed primitive/buffer/view/Blob was a no-op) so a `$setValue` of a new Date/Set/RegExp is a change, not a no-op; an opaque value (Blob) compares by identity in the replacement check and as equal between two COPIES of one record (`OpaqueRule`: the writer rule, element provenance, the borrowed-mutation guard, admission). Every proxy (nested/terminal/element) READS LIVE through `reactive/liveView.ts` — a held handle sees its own writes; it answers from the captured object only when the path is gone. See src/lib/reactive/README.md. **Named, not fixed (out of contract):** a cyclic self-reference reached through a terminal proxy commits array/element writes to the WRONG place (row recorded, fold agrees, value lands beside the cycle edge); a top-level key containing U+001F (the path separator) throws at the seed; an expando on an array (`arr.foo = 1`) is dropped by the array proxy's set trap (indices and `length` only).
|
|
96
|
+
4. **NOT a landmine any more (9.22.0), recorded because it was one:** an indexed array read (`arr[i]`) used to hand back the RAW element, and a nested array write committed with the `merge` verb whose array arm is a set UNION. Both are fixed at the root — `reactive/arrayTraps.ts` wraps elements, and a nested array commits as a `set` of its ROOT KEY (`createTypedScope` · nested array commit callback). Consequences to keep in mind: an array ASSIGNMENT now REPLACES at every depth (`$update` is still the append), and a nested array write appears in `bundle.overwrite`/verb `set`, not `bundle.updates`/verb `merge`. What the proxy still cannot see — an element reached via `find`/`filter`/`for…of`/`forEach` — is REPORTED loudly instead: `StageContext` · `warnOnBorrowedMutation` (a dev-mode WARNING at the frame's first commit, under `readTracking: 'full'` only — a report after the fact, not a refusal). Foottrace's `memory/equality.ts` · `equalPairs` has one arm per value KIND (`capture/valueKinds.ts · kindOf`, the one classifier — every kind `structuredClone` keeps since 9.44.2; before it, a replaced RegExp/Error/boxed primitive/buffer/view/Blob was a no-op) so a `$setValue` of a new Date/Set/RegExp is a change, not a no-op; an opaque value (Blob) compares by identity in the replacement check and as equal between two COPIES of one record (`OpaqueRule`: the writer rule, element provenance, the borrowed-mutation guard, admission). Every proxy (nested/terminal/element) READS LIVE through `reactive/liveView.ts` — a held handle sees its own writes; it answers from the captured object only when the path is gone. See src/lib/reactive/README.md. **Named, not fixed (out of contract):** a cyclic self-reference reached through a terminal proxy commits array/element writes to the WRONG place (row recorded, fold agrees, value lands beside the cycle edge); a top-level key containing U+001F (the path separator) throws at the seed; an expando on an array (`arr.foo = 1`) is dropped by the array proxy's set trap (indices and `length` only).
|
|
99
97
|
|
|
100
98
|
## Pointers
|
|
101
99
|
- [.claude/rules/backtracking.md](.claude/rules/backtracking.md) — the 5 mechanisms with step tables + pseudocode
|
package/README.md
CHANGED
|
@@ -233,7 +233,7 @@ Import one thing, ship one thing. footprintjs is built so your bundle grows only
|
|
|
233
233
|
|
|
234
234
|
- **Dual build, true ESM.** Ships CommonJS (`require`) **and** real ECMAScript Modules (`import`) with TypeScript types. The ESM build is marked `type:module` and every internal import carries an explicit `.js` extension, so it loads as true ESM under Node, Vite, Next, Deno, and Bun — no compatibility shims.
|
|
235
235
|
- **Per-file modules + `sideEffects:false`.** The dist is emitted file-by-file (never pre-bundled) and declares zero side effects, so bundlers can drop every export you don't touch.
|
|
236
|
-
- **
|
|
236
|
+
- **Package doors.** Read execution records with `foottrace`, write them with `foottrace/write`, and handle record paths with `foottrace/paths`. Recorder stores and topology belong to `footprintjs/trace`, fire-and-forget children to `footprintjs/detach`, and engine internals to `footprintjs/advanced`. Each door is independently tree-shakeable.
|
|
237
237
|
|
|
238
238
|
**Proven, not promised.** A CI smoke test bundles a minimal `import { flowChart } from 'footprintjs'` and asserts the recorder, detach, and trace layers are pruned — your flowchart core doesn't drag them in. See [`test/esm-packaging.test.ts`](test/esm-packaging.test.ts).
|
|
239
239
|
|