footprintjs 4.17.2 → 6.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.
Files changed (167) hide show
  1. package/CLAUDE.md +71 -9
  2. package/dist/advanced.js +2 -3
  3. package/dist/esm/advanced.js +2 -2
  4. package/dist/esm/index.js +3 -3
  5. package/dist/esm/lib/builder/FlowChartBuilder.js +359 -70
  6. package/dist/esm/lib/builder/index.js +1 -1
  7. package/dist/esm/lib/builder/structure/StructureRecorder.js +158 -0
  8. package/dist/esm/lib/builder/structure/StructureRecorderDispatcher.js +171 -0
  9. package/dist/esm/lib/builder/types.js +1 -1
  10. package/dist/esm/lib/decide/decide.js +9 -9
  11. package/dist/esm/lib/decide/evidence.js +2 -2
  12. package/dist/esm/lib/engine/errors/errorInfo.js +4 -3
  13. package/dist/esm/lib/engine/handlers/ChildrenExecutor.js +11 -2
  14. package/dist/esm/lib/engine/handlers/DeciderHandler.js +5 -7
  15. package/dist/esm/lib/engine/handlers/RuntimeStructureManager.js +2 -2
  16. package/dist/esm/lib/engine/handlers/SelectorHandler.js +9 -7
  17. package/dist/esm/lib/engine/handlers/SubflowExecutor.js +7 -1
  18. package/dist/esm/lib/engine/handlers/index.js +1 -3
  19. package/dist/esm/lib/engine/handlers/types.js +2 -2
  20. package/dist/esm/lib/engine/narrative/CombinedNarrativeRecorder.js +14 -4
  21. package/dist/esm/lib/engine/narrative/FlowRecorderDispatcher.js +5 -5
  22. package/dist/esm/lib/engine/narrative/NarrativeFlowRecorder.js +8 -1
  23. package/dist/esm/lib/engine/narrative/types.js +1 -1
  24. package/dist/esm/lib/engine/runtimeStageId.js +64 -2
  25. package/dist/esm/lib/engine/traversal/FlowchartTraverser.js +30 -56
  26. package/dist/esm/lib/engine/types.js +1 -1
  27. package/dist/esm/lib/engine/walkSubflowSpec.js +144 -0
  28. package/dist/esm/lib/memory/StageContext.js +3 -3
  29. package/dist/esm/lib/memory/backtrack.js +1 -1
  30. package/dist/esm/lib/reactive/createTypedScope.js +5 -5
  31. package/dist/esm/lib/reactive/types.js +7 -7
  32. package/dist/esm/lib/recorder/BoundaryStateStore.js +167 -0
  33. package/dist/esm/lib/recorder/BoundaryStateTracker.js +4 -4
  34. package/dist/esm/lib/recorder/CombinedRecorder.js +9 -9
  35. package/dist/esm/lib/recorder/CommitRangeIndex.js +207 -0
  36. package/dist/esm/lib/recorder/CompositeRecorder.js +6 -6
  37. package/dist/esm/lib/recorder/EmitRecorder.js +3 -3
  38. package/dist/esm/lib/recorder/InOutRecorder.js +1 -1
  39. package/dist/esm/lib/recorder/KeyedStore.js +113 -0
  40. package/dist/esm/lib/recorder/QualityRecorder.js +2 -2
  41. package/dist/esm/lib/recorder/SequenceRecorder.js +2 -2
  42. package/dist/esm/lib/recorder/SequenceStore.js +195 -0
  43. package/dist/esm/lib/recorder/TopologyRecorder.js +1 -1
  44. package/dist/esm/lib/recorder/index.js +13 -3
  45. package/dist/esm/lib/runner/ExecutionRuntime.js +1 -1
  46. package/dist/esm/lib/runner/FlowChartExecutor.js +75 -44
  47. package/dist/esm/lib/runner/RunContext.js +2 -2
  48. package/dist/esm/lib/runner/RunnableChart.js +1 -1
  49. package/dist/esm/lib/runner/getSubtreeSnapshot.js +3 -2
  50. package/dist/esm/lib/runner/runId.js +65 -0
  51. package/dist/esm/lib/scope/ScopeFacade.js +6 -6
  52. package/dist/esm/lib/scope/index.js +1 -1
  53. package/dist/esm/lib/scope/recorders/DebugRecorder.js +4 -4
  54. package/dist/esm/lib/scope/recorders/MetricRecorder.js +2 -2
  55. package/dist/esm/lib/scope/recorders/index.js +1 -1
  56. package/dist/esm/lib/scope/types.js +1 -1
  57. package/dist/esm/recorders.js +1 -1
  58. package/dist/esm/trace.js +15 -2
  59. package/dist/index.js +3 -3
  60. package/dist/lib/builder/FlowChartBuilder.js +359 -70
  61. package/dist/lib/builder/index.js +1 -1
  62. package/dist/lib/builder/structure/StructureRecorder.js +159 -0
  63. package/dist/lib/builder/structure/StructureRecorderDispatcher.js +175 -0
  64. package/dist/lib/builder/types.js +1 -1
  65. package/dist/lib/decide/decide.js +9 -9
  66. package/dist/lib/decide/evidence.js +2 -2
  67. package/dist/lib/engine/errors/errorInfo.js +4 -3
  68. package/dist/lib/engine/handlers/ChildrenExecutor.js +11 -2
  69. package/dist/lib/engine/handlers/DeciderHandler.js +5 -7
  70. package/dist/lib/engine/handlers/RuntimeStructureManager.js +2 -2
  71. package/dist/lib/engine/handlers/SelectorHandler.js +9 -7
  72. package/dist/lib/engine/handlers/SubflowExecutor.js +7 -1
  73. package/dist/lib/engine/handlers/index.js +2 -5
  74. package/dist/lib/engine/handlers/types.js +2 -2
  75. package/dist/lib/engine/narrative/CombinedNarrativeRecorder.js +14 -4
  76. package/dist/lib/engine/narrative/FlowRecorderDispatcher.js +5 -5
  77. package/dist/lib/engine/narrative/NarrativeFlowRecorder.js +8 -1
  78. package/dist/lib/engine/narrative/types.js +1 -1
  79. package/dist/lib/engine/runtimeStageId.js +66 -3
  80. package/dist/lib/engine/traversal/FlowchartTraverser.js +30 -56
  81. package/dist/lib/engine/types.js +1 -1
  82. package/dist/lib/engine/walkSubflowSpec.js +148 -0
  83. package/dist/lib/memory/StageContext.js +3 -3
  84. package/dist/lib/memory/backtrack.js +1 -1
  85. package/dist/lib/reactive/createTypedScope.js +5 -5
  86. package/dist/lib/reactive/types.js +7 -7
  87. package/dist/lib/recorder/BoundaryStateStore.js +171 -0
  88. package/dist/lib/recorder/BoundaryStateTracker.js +4 -4
  89. package/dist/lib/recorder/CombinedRecorder.js +9 -9
  90. package/dist/lib/recorder/CommitRangeIndex.js +211 -0
  91. package/dist/lib/recorder/CompositeRecorder.js +6 -6
  92. package/dist/lib/recorder/EmitRecorder.js +3 -3
  93. package/dist/lib/recorder/InOutRecorder.js +1 -1
  94. package/dist/lib/recorder/KeyedStore.js +117 -0
  95. package/dist/lib/recorder/QualityRecorder.js +2 -2
  96. package/dist/lib/recorder/SequenceRecorder.js +2 -2
  97. package/dist/lib/recorder/SequenceStore.js +199 -0
  98. package/dist/lib/recorder/TopologyRecorder.js +1 -1
  99. package/dist/lib/recorder/index.js +19 -6
  100. package/dist/lib/runner/ExecutionRuntime.js +1 -1
  101. package/dist/lib/runner/FlowChartExecutor.js +75 -44
  102. package/dist/lib/runner/RunContext.js +2 -2
  103. package/dist/lib/runner/RunnableChart.js +1 -1
  104. package/dist/lib/runner/getSubtreeSnapshot.js +3 -2
  105. package/dist/lib/runner/runId.js +70 -0
  106. package/dist/lib/scope/ScopeFacade.js +6 -6
  107. package/dist/lib/scope/index.js +1 -1
  108. package/dist/lib/scope/recorders/DebugRecorder.js +4 -4
  109. package/dist/lib/scope/recorders/MetricRecorder.js +2 -2
  110. package/dist/lib/scope/recorders/index.js +1 -1
  111. package/dist/lib/scope/types.js +1 -1
  112. package/dist/recorders.js +1 -1
  113. package/dist/trace.js +21 -2
  114. package/dist/types/advanced.d.ts +4 -4
  115. package/dist/types/index.d.ts +6 -4
  116. package/dist/types/lib/builder/FlowChartBuilder.d.ts +110 -14
  117. package/dist/types/lib/builder/index.d.ts +2 -1
  118. package/dist/types/lib/builder/structure/StructureRecorder.d.ts +349 -0
  119. package/dist/types/lib/builder/structure/StructureRecorderDispatcher.d.ts +77 -0
  120. package/dist/types/lib/builder/types.d.ts +20 -6
  121. package/dist/types/lib/decide/evidence.d.ts +3 -3
  122. package/dist/types/lib/engine/errors/errorInfo.d.ts +3 -2
  123. package/dist/types/lib/engine/handlers/DeciderHandler.d.ts +3 -3
  124. package/dist/types/lib/engine/handlers/RuntimeStructureManager.d.ts +1 -1
  125. package/dist/types/lib/engine/handlers/SelectorHandler.d.ts +2 -2
  126. package/dist/types/lib/engine/handlers/index.d.ts +1 -2
  127. package/dist/types/lib/engine/handlers/types.d.ts +2 -9
  128. package/dist/types/lib/engine/narrative/CombinedNarrativeRecorder.d.ts +1 -1
  129. package/dist/types/lib/engine/narrative/FlowRecorderDispatcher.d.ts +4 -4
  130. package/dist/types/lib/engine/narrative/types.d.ts +43 -4
  131. package/dist/types/lib/engine/runtimeStageId.d.ts +57 -1
  132. package/dist/types/lib/engine/traversal/FlowchartTraverser.d.ts +12 -6
  133. package/dist/types/lib/engine/types.d.ts +0 -26
  134. package/dist/types/lib/engine/walkSubflowSpec.d.ts +95 -0
  135. package/dist/types/lib/memory/StageContext.d.ts +2 -2
  136. package/dist/types/lib/memory/backtrack.d.ts +1 -1
  137. package/dist/types/lib/reactive/types.d.ts +7 -7
  138. package/dist/types/lib/recorder/BoundaryStateStore.d.ts +115 -0
  139. package/dist/types/lib/recorder/BoundaryStateTracker.d.ts +3 -3
  140. package/dist/types/lib/recorder/CombinedRecorder.d.ts +11 -11
  141. package/dist/types/lib/recorder/CommitRangeIndex.d.ts +147 -0
  142. package/dist/types/lib/recorder/CompositeRecorder.d.ts +8 -8
  143. package/dist/types/lib/recorder/EmitRecorder.d.ts +2 -2
  144. package/dist/types/lib/recorder/InOutRecorder.d.ts +1 -1
  145. package/dist/types/lib/recorder/KeyedStore.d.ts +70 -0
  146. package/dist/types/lib/recorder/QualityRecorder.d.ts +4 -4
  147. package/dist/types/lib/recorder/SequenceRecorder.d.ts +1 -1
  148. package/dist/types/lib/recorder/SequenceStore.d.ts +120 -0
  149. package/dist/types/lib/recorder/TopologyRecorder.d.ts +1 -1
  150. package/dist/types/lib/recorder/index.d.ts +5 -2
  151. package/dist/types/lib/runner/ExecutionRuntime.d.ts +1 -1
  152. package/dist/types/lib/runner/FlowChartExecutor.d.ts +40 -30
  153. package/dist/types/lib/runner/RunContext.d.ts +2 -2
  154. package/dist/types/lib/runner/RunnableChart.d.ts +2 -2
  155. package/dist/types/lib/runner/runId.d.ts +45 -0
  156. package/dist/types/lib/scope/ScopeFacade.d.ts +4 -4
  157. package/dist/types/lib/scope/index.d.ts +1 -1
  158. package/dist/types/lib/scope/recorders/DebugRecorder.d.ts +4 -4
  159. package/dist/types/lib/scope/recorders/MetricRecorder.d.ts +4 -4
  160. package/dist/types/lib/scope/recorders/index.d.ts +1 -1
  161. package/dist/types/lib/scope/types.d.ts +1 -1
  162. package/dist/types/recorders.d.ts +1 -1
  163. package/dist/types/trace.d.ts +8 -1
  164. package/package.json +1 -1
  165. package/dist/esm/lib/engine/handlers/ExtractorRunner.js +0 -122
  166. package/dist/lib/engine/handlers/ExtractorRunner.js +0 -126
  167. package/dist/types/lib/engine/handlers/ExtractorRunner.d.ts +0 -41
@@ -0,0 +1,147 @@
1
+ /**
2
+ * CommitRangeIndex<TLabel> — interval index over commit indices.
3
+ *
4
+ * Built incrementally during traversal: `open(label, startIdx)` when a
5
+ * boundary begins, `close(token, endIdx)` when it ends. Query at any
6
+ * commit position with `enclosing(idx)` (returns ranges containing
7
+ * that index, ordered outer→inner) or `overlapping(start, end)`
8
+ * (returns ranges intersecting a slice).
9
+ *
10
+ * See `docs/design/commit-range-index.md` for the full contract. In
11
+ * one paragraph: this is a generic interval data structure for
12
+ * commit-range queries. footprintjs owns ZERO knowledge of what
13
+ * labels mean — consumers (agentfootprint, lens, OTel exporters)
14
+ * pick their own `TLabel` type. Open ranges (mid-run, no end yet)
15
+ * are first-class — query results carry `endIdx: undefined` for them.
16
+ *
17
+ * Pattern: incremental builder + interval query. Same "collect during
18
+ * traversal, never post-process" rule footprintjs's CLAUDE.md
19
+ * requires of every observer.
20
+ * Role: structural primitive for time-travel UIs and per-boundary
21
+ * aggregation.
22
+ * Channel: consumer-driven (no engine subscription).
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * import { CommitRangeIndex } from 'footprintjs/trace';
27
+ *
28
+ * const idx = new CommitRangeIndex<string>();
29
+ * const t = idx.open('LLMCall', executor.getCommitCount());
30
+ * // ... LLM call runs, scope writes happen ...
31
+ * idx.close(t, executor.getCommitCount());
32
+ *
33
+ * idx.enclosing(50); // → ranges containing commit 50, outer→inner
34
+ * idx.overlapping(40, 60); // → ranges sharing the slice [40,60]
35
+ * idx.clear(); // wipe (e.g., on new run)
36
+ * ```
37
+ *
38
+ * REDACTION NOTE: labels are stored verbatim and returned verbatim in
39
+ * query results — the index does NOT redact `TLabel` content. If a
40
+ * consumer attaches a label containing PII (user email, scope reads
41
+ * with sensitive keys, etc.) and then serializes the index for
42
+ * logging or telemetry, that data leaves the trust boundary. Use
43
+ * `RedactionPolicy` (or your own scrubbing) on the consumer side
44
+ * BEFORE attaching labels. The index follows the same contract as
45
+ * other footprintjs storage primitives (SequenceStore, KeyedStore):
46
+ * storage is verbatim; redaction is the caller's responsibility.
47
+ */
48
+ /** Opaque token identifying an open range. Hold onto it; pass to `close()`.
49
+ * Index-scoped — using a token from one CommitRangeIndex on another is
50
+ * a silent no-op (verified by the per-index `_owner` symbol).
51
+ *
52
+ * SECURITY NOTE: the `_owner` symbol is enumerable on the token object
53
+ * via `Object.getOwnPropertySymbols(token)`. This means tokens are NOT
54
+ * adversary-safe — a malicious caller with access to ANY token from
55
+ * this index can recover the owner symbol and forge new tokens. The
56
+ * index is designed for in-process trust boundaries (cooperative
57
+ * recorders sharing one runner), not for hostile-input scenarios.
58
+ * If adversary-safety becomes a requirement, switch to a WeakMap-
59
+ * scoped token model (see security panel review Y2). */
60
+ export interface RangeToken {
61
+ /** Per-index sequential id. Opaque to consumers — they shouldn't read it. */
62
+ readonly _id: number;
63
+ /** Per-index identity — prevents accidental cross-index token misuse. */
64
+ readonly _owner: symbol;
65
+ }
66
+ /** A single range as returned by query methods. Frozen-shape — readonly fields. */
67
+ export interface RangeEntry<TLabel> {
68
+ readonly label: TLabel;
69
+ readonly startIdx: number;
70
+ /** Undefined while the range is still open (mid-run boundary). */
71
+ readonly endIdx?: number;
72
+ }
73
+ export declare class CommitRangeIndex<TLabel> {
74
+ private entries;
75
+ private byId;
76
+ private nextId;
77
+ /** Identity for token scoping — each index gets a fresh symbol so
78
+ * tokens from one index can't accidentally close ranges in another.
79
+ * ROTATED on `clear()` to invalidate stale tokens that survived a
80
+ * run reset (would otherwise hit a recycled id and silently mutate
81
+ * a different range — see DS+logic panel review RED #1). */
82
+ private owner;
83
+ /**
84
+ * Open a new range. Returns a token the caller MUST hold and pass
85
+ * to `close()` later. Each `open()` gets a fresh token; tokens
86
+ * cannot be reused or shared across indices (silent no-op if
87
+ * misused — see Law 2 in the design doc). Tokens from BEFORE
88
+ * the most recent `clear()` are also invalid (owner symbol
89
+ * rotates on clear).
90
+ */
91
+ open(label: TLabel, startIdx: number): RangeToken;
92
+ /**
93
+ * Close an open range at `endIdx` (inclusive). After close, the
94
+ * range is queryable with both bounds. Closing an already-closed
95
+ * token is a no-op. Closing an unknown token (from another index,
96
+ * or fabricated) is a no-op.
97
+ */
98
+ close(token: RangeToken, endIdx: number): void;
99
+ /**
100
+ * Returns ALL ranges enclosing `commitIdx`, ordered outer→inner.
101
+ * Includes both closed and open ranges. For a closed range to
102
+ * enclose: `startIdx <= commitIdx <= endIdx`. For an open range:
103
+ * `startIdx <= commitIdx` (no upper bound check).
104
+ *
105
+ * Ordering rule: ascending by `startIdx`, with TIES BROKEN BY
106
+ * descending `endIdx` (wider range = outer). Open ranges (endIdx
107
+ * undefined) are treated as `+Infinity` for tie-break — they
108
+ * always sort outer of any closed range starting at the same idx.
109
+ * This is the only deterministic outer→inner ordering when two
110
+ * boundaries open at the same commit (e.g., Parallel root +
111
+ * its first branch).
112
+ *
113
+ * Returns a SHALLOW IMMUTABLE COPY — caller mutations don't affect
114
+ * internal state.
115
+ */
116
+ enclosing(commitIdx: number): readonly RangeEntry<TLabel>[];
117
+ /**
118
+ * Returns all ranges OVERLAPPING the slice `[startIdx, endIdx]`. A
119
+ * range overlaps if it shares at least one commit position with the
120
+ * slice. Use for parallel-branch detection, time-window queries,
121
+ * or "what boundaries fired during this slice."
122
+ *
123
+ * Sorted by the SAME outer→inner comparator as `enclosing()`:
124
+ * ascending by `startIdx`, ties broken by descending `endIdx`
125
+ * (wider = outer; open ranges treated as +Infinity).
126
+ *
127
+ * Returns a SHALLOW IMMUTABLE COPY.
128
+ */
129
+ overlapping(startIdx: number, endIdx: number): readonly RangeEntry<TLabel>[];
130
+ /** Total range count (open + closed). */
131
+ get size(): number;
132
+ /**
133
+ * Wipe all ranges + reset the token counter AND rotate the owner
134
+ * symbol so any token from before this clear becomes invalid.
135
+ * Critical: without rotating the owner, a stale token whose `_id`
136
+ * happens to match a recycled id after clear would silently mutate
137
+ * the wrong entry. Owner rotation makes stale-token close a no-op
138
+ * via the `_owner !== this.owner` guard.
139
+ *
140
+ * Call from a consumer's runId guard when a new run starts (e.g.,
141
+ * agentfootprint's `observeRunId(onNewRun)` from Phase 2).
142
+ */
143
+ clear(): void;
144
+ /** O(1) lookup by token id. Always returns the entry that the token
145
+ * references (or undefined if the id was never opened in this index). */
146
+ private findById;
147
+ }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * CompositeRecorder — fan-out a single recorder attachment to multiple child recorders.
3
3
  *
4
- * Implements both Recorder (scope data ops) and FlowRecorder (control flow events)
5
- * so it works with both `executor.attachRecorder()` and `executor.attachFlowRecorder()`.
4
+ * Implements both ScopeRecorder (scope data ops) and FlowRecorder (control flow events)
5
+ * so it works with both `executor.attachScopeRecorder()` and `executor.attachFlowRecorder()`.
6
6
  *
7
7
  * The composite has a single ID for idempotent attach/detach. Child recorders
8
8
  * keep their own IDs internally but are not individually visible to the executor.
@@ -20,7 +20,7 @@
20
20
  * new DebugRecorder({ verbosity: 'minimal' }),
21
21
  * ]);
22
22
  *
23
- * executor.attachRecorder(observability);
23
+ * executor.attachScopeRecorder(observability);
24
24
  *
25
25
  * // Access child recorders by type
26
26
  * const metrics = observability.get(MetricRecorder);
@@ -39,11 +39,11 @@
39
39
  * }
40
40
  *
41
41
  * // Consumer
42
- * executor.attachRecorder(agentObservability());
42
+ * executor.attachScopeRecorder(agentObservability());
43
43
  * ```
44
44
  */
45
45
  import type { FlowBreakEvent, FlowDecisionEvent, FlowErrorEvent, FlowForkEvent, FlowLoopEvent, FlowNextEvent, FlowRecorder, FlowSelectedEvent, FlowStageEvent, FlowSubflowEvent, FlowSubflowRegisteredEvent } from '../engine/narrative/types.js';
46
- import type { CommitEvent, ErrorEvent, ReadEvent, Recorder, StageEvent, WriteEvent } from '../scope/types.js';
46
+ import type { CommitEvent, ErrorEvent, ReadEvent, ScopeRecorder, StageEvent, WriteEvent } from '../scope/types.js';
47
47
  /** Snapshot format for composite recorders — wraps child snapshots. */
48
48
  export interface CompositeSnapshot {
49
49
  name: string;
@@ -55,10 +55,10 @@ export interface CompositeSnapshot {
55
55
  }>;
56
56
  };
57
57
  }
58
- export declare class CompositeRecorder implements Recorder, FlowRecorder {
58
+ export declare class CompositeRecorder implements ScopeRecorder, FlowRecorder {
59
59
  readonly id: string;
60
60
  private readonly children;
61
- constructor(id: string, children: Array<Recorder | FlowRecorder>);
61
+ constructor(id: string, children: Array<ScopeRecorder | FlowRecorder>);
62
62
  /**
63
63
  * Get a child recorder by class type.
64
64
  *
@@ -69,7 +69,7 @@ export declare class CompositeRecorder implements Recorder, FlowRecorder {
69
69
  */
70
70
  get<T>(type: new (...args: any[]) => T): T | undefined;
71
71
  /** Get all child recorders. */
72
- getChildren(): ReadonlyArray<Recorder | FlowRecorder>;
72
+ getChildren(): ReadonlyArray<ScopeRecorder | FlowRecorder>;
73
73
  onRead(event: ReadEvent): void;
74
74
  onWrite(event: WriteEvent): void;
75
75
  onCommit(event: CommitEvent): void;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * EmitRecorder — the third observer channel, alongside `Recorder`
2
+ * EmitRecorder — the third observer channel, alongside `ScopeRecorder`
3
3
  * (scope data-flow) and `FlowRecorder` (control-flow).
4
4
  *
5
5
  * ## Why this exists
@@ -34,7 +34,7 @@
34
34
  *
35
35
  * | Channel | Fires when | Built-in consumers |
36
36
  * |-----------------|------------------------------------|------------------------|
37
- * | `Recorder` | scope read/write/commit | DebugRecorder, MetricRecorder |
37
+ * | `ScopeRecorder` | scope read/write/commit | DebugRecorder, MetricRecorder |
38
38
  * | `FlowRecorder` | traversal transitions | NarrativeFlowRecorder, etc. |
39
39
  * | `EmitRecorder` | consumer calls `scope.$emit(...)` | (none ships today; Phase 3.X adds MemoryEmitRecorder) |
40
40
  *
@@ -111,7 +111,7 @@ export interface InOutEntry {
111
111
  readonly isRoot: boolean;
112
112
  }
113
113
  export interface InOutRecorderOptions {
114
- /** Recorder id. Defaults to `inout-N` (auto-incremented). */
114
+ /** ScopeRecorder id. Defaults to `inout-N` (auto-incremented). */
115
115
  id?: string;
116
116
  }
117
117
  /**
@@ -0,0 +1,70 @@
1
+ /**
2
+ * KeyedStore<T> — concrete, composable 1:1 storage keyed by string id.
3
+ *
4
+ * Pattern: COMPOSITION primitive. Concrete class — instantiate with
5
+ * `new KeyedStore<T>()` and own it as a field on your
6
+ * recorder. Replaces the abstract `KeyedRecorder<T>` base
7
+ * class for the v5 "one purpose per recorder" rule.
8
+ * Role: 1:1 Map keyed by `runtimeStageId` (or any string).
9
+ * Insertion-ordered iteration.
10
+ *
11
+ * **Contrast with `SequenceStore<T>`:** KeyedStore is 1:1 — one entry
12
+ * per key. Use SequenceStore for 1:N (multiple entries per
13
+ * runtimeStageId, ordering matters).
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { KeyedStore } from 'footprintjs/trace';
18
+ *
19
+ * interface TokenEntry { input: number; output: number; }
20
+ *
21
+ * // ONE PURPOSE: typed-event handler. Storage is composed in.
22
+ * class TokenRecorder {
23
+ * readonly id = 'tokens';
24
+ * private readonly store = new KeyedStore<TokenEntry>();
25
+ *
26
+ * onLLMCall(event: LLMCallEvent) {
27
+ * this.store.set(event.runtimeStageId, event.usage);
28
+ * }
29
+ *
30
+ * getForStep(id: string) { return this.store.get(id); }
31
+ * getTotalTokens() { return this.store.aggregate((s, e) => s + e.input + e.output, 0); }
32
+ * getTokensUpTo(keys: Set<string>) {
33
+ * return this.store.accumulate((s, e) => s + e.input + e.output, 0, keys);
34
+ * }
35
+ *
36
+ * clear() { this.store.clear(); }
37
+ * }
38
+ * ```
39
+ */
40
+ export declare class KeyedStore<T> {
41
+ private readonly data;
42
+ /** Store a single entry. Replaces any existing entry for the same key. */
43
+ set(key: string, entry: T): void;
44
+ /** Remove an entry. Returns true if the key existed, false otherwise. */
45
+ delete(key: string): boolean;
46
+ /** O(1) lookup. */
47
+ get(key: string): T | undefined;
48
+ /** True if a value exists for the key. */
49
+ has(key: string): boolean;
50
+ /** All entries as a read-only Map (insertion-ordered). */
51
+ getMap(): ReadonlyMap<string, T>;
52
+ /** All values as an array (insertion-ordered). */
53
+ values(): T[];
54
+ /** Number of entries stored. */
55
+ get size(): number;
56
+ /** Reduce ALL entries to a single value. For dashboards, totals, summaries. */
57
+ aggregate<R>(fn: (acc: R, entry: T, key: string) => R, initial: R): R;
58
+ /**
59
+ * Reduce entries, optionally filtered by a set of keys.
60
+ * For time-travel progressive view: pass the keys visible at the
61
+ * current slider position. Without keys, reduces all entries (same
62
+ * as `aggregate`).
63
+ */
64
+ accumulate<R>(fn: (acc: R, entry: T, key: string) => R, initial: R, keys?: ReadonlySet<string>): R;
65
+ /** Return entries whose keys are in the set, preserving insertion order. */
66
+ filterByKeys(keys: ReadonlySet<string>): T[];
67
+ /** Clear all stored data. Recorders typically call this from their own
68
+ * `clear()` method, which the executor invokes before each run. */
69
+ clear(): void;
70
+ }
@@ -16,7 +16,7 @@
16
16
  * if (event.stageName.includes('llm')) return 0.7;
17
17
  * return 1.0;
18
18
  * });
19
- * executor.attachRecorder(quality);
19
+ * executor.attachScopeRecorder(quality);
20
20
  * await executor.run();
21
21
  *
22
22
  * // Per-step score
@@ -29,7 +29,7 @@
29
29
  * quality.getLowest(); // { runtimeStageId: 'call-llm#5', entry: { score: 0.7, ... } }
30
30
  * ```
31
31
  */
32
- import type { ReadEvent, Recorder, StageEvent, WriteEvent } from '../scope/types.js';
32
+ import type { ReadEvent, ScopeRecorder, StageEvent, WriteEvent } from '../scope/types.js';
33
33
  import { KeyedRecorder } from './KeyedRecorder.js';
34
34
  import type { RecorderOperation } from './RecorderOperation.js';
35
35
  /** Per-step quality data stored by QualityRecorder. */
@@ -64,12 +64,12 @@ export type QualityScoringFn = (runtimeStageId: string, context: {
64
64
  };
65
65
  /** Options for QualityRecorder. */
66
66
  export interface QualityRecorderOptions {
67
- /** Recorder ID. Defaults to auto-increment. */
67
+ /** ScopeRecorder ID. Defaults to auto-increment. */
68
68
  id?: string;
69
69
  /** Preferred UI operation. Defaults to 'accumulate' (progressive quality). */
70
70
  preferredOperation?: RecorderOperation;
71
71
  }
72
- export declare class QualityRecorder extends KeyedRecorder<QualityEntry> implements Recorder {
72
+ export declare class QualityRecorder extends KeyedRecorder<QualityEntry> implements ScopeRecorder {
73
73
  private static _counter;
74
74
  readonly id: string;
75
75
  readonly preferredOperation: RecorderOperation;
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Provides dual-indexed storage: a flat array preserving insertion order plus a
5
5
  * Map<runtimeStageId, T[]> for O(1) per-step lookup. One entry type, multiple entries
6
- * per step. Designed for recorders that implement BOTH Recorder and FlowRecorder
6
+ * per step. Designed for recorders that implement BOTH ScopeRecorder and FlowRecorder
7
7
  * (merging data ops and control flow into a single interleaved sequence).
8
8
  *
9
9
  * **Contrast with KeyedRecorder<T>:**
@@ -0,0 +1,120 @@
1
+ /**
2
+ * SequenceStore<T> — concrete, composable storage for ordered sequence data.
3
+ *
4
+ * Pattern: COMPOSITION primitive. Concrete class — instantiate with
5
+ * `new SequenceStore<T>()` and own it as a field on your
6
+ * recorder. Replaces the abstract `SequenceRecorder<T>` base
7
+ * class for the v5 "one purpose per recorder" rule:
8
+ * stores ARE storage; recorders ARE event handlers; consumers
9
+ * COMPOSE.
10
+ * Role: Dual-indexed append-only sequence: a flat array preserving
11
+ * insertion order plus a `Map<runtimeStageId, T[]>` for O(1)
12
+ * per-step lookup. Plus a precomputed range index for time-
13
+ * travel scrubbing.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { SequenceStore } from 'footprintjs/trace';
18
+ * import type { ScopeRecorder } from 'footprintjs';
19
+ *
20
+ * interface AuditEntry {
21
+ * runtimeStageId?: string;
22
+ * type: 'read' | 'write' | 'decision';
23
+ * detail: string;
24
+ * }
25
+ *
26
+ * // ONE PURPOSE: scope-event handler. Storage is composed in.
27
+ * class AuditRecorder implements ScopeRecorder {
28
+ * readonly id = 'audit';
29
+ * private readonly store = new SequenceStore<AuditEntry>();
30
+ *
31
+ * onRead(event: ReadEvent) {
32
+ * this.store.push({
33
+ * runtimeStageId: event.runtimeStageId,
34
+ * type: 'read',
35
+ * detail: event.key,
36
+ * });
37
+ * }
38
+ * onWrite(event: WriteEvent) {
39
+ * this.store.push({
40
+ * runtimeStageId: event.runtimeStageId,
41
+ * type: 'write',
42
+ * detail: event.key,
43
+ * });
44
+ * }
45
+ *
46
+ * getAudit() { return this.store.getAll(); }
47
+ * getAuditUpTo(ids: ReadonlySet<string>) {
48
+ * return this.store.getEntriesUpTo(ids);
49
+ * }
50
+ *
51
+ * clear() { this.store.clear(); }
52
+ * }
53
+ * ```
54
+ *
55
+ * **Contrast with `KeyedStore<T>`:** SequenceStore stores 1:N entries
56
+ * per runtimeStageId in insertion order. Use KeyedStore for 1:1
57
+ * (one record per step — token counts, metric snapshots).
58
+ */
59
+ export declare class SequenceStore<T extends {
60
+ runtimeStageId?: string;
61
+ }> {
62
+ /** Ordered sequence of all entries (insertion order). */
63
+ private readonly entries;
64
+ /** Per-step index: runtimeStageId → entries for that step. Same objects as `entries[]`. */
65
+ private readonly byRuntimeStageId;
66
+ /** Per-step range index: runtimeStageId → [firstIdx, endIdx) in entries array.
67
+ * endIdx includes trailing keyless entries (structural markers). Maintained during push(). */
68
+ private readonly entryRanges;
69
+ /** The runtimeStageId of the most recently emitted keyed entry. Used to extend
70
+ * ranges for trailing markers (entries without runtimeStageId attached after a step). */
71
+ private lastEmittedId;
72
+ /**
73
+ * Append an entry to both the ordered sequence, keyed index, and range index.
74
+ * All three reference the SAME entry object — no duplication.
75
+ */
76
+ push(entry: T): void;
77
+ /** All entries in insertion order. Returns a shallow copy — entry objects are shared. */
78
+ getAll(): T[];
79
+ /** Number of entries in the sequence. */
80
+ get size(): number;
81
+ /** Zero-copy iteration. Avoids the `getAll()` spread when the caller just needs
82
+ * to walk the entries (e.g., aggregating, rendering). */
83
+ forEach(fn: (entry: T) => void): void;
84
+ /** O(1) lookup: all entries for a specific execution step. Returns a copy. */
85
+ getByKey(runtimeStageId: string): T[];
86
+ /** Number of distinct execution steps that have at least one entry. */
87
+ get keyCount(): number;
88
+ /**
89
+ * Pre-built range index: runtimeStageId → half-open `[firstIdx, endIdx)`
90
+ * range in the entries array. Maintained during `push()` — no rebuild
91
+ * needed. Use for O(1) per-step lookups during time-travel scrubbing.
92
+ * `endIdx` includes trailing keyless entries (structural markers
93
+ * following a step).
94
+ */
95
+ getEntryRanges(): ReadonlyMap<string, {
96
+ readonly firstIdx: number;
97
+ readonly endIdx: number;
98
+ }>;
99
+ /** Reduce ALL entries to a single value. For dashboards, totals, summaries. */
100
+ aggregate<R>(fn: (acc: R, entry: T) => R, initial: R): R;
101
+ /**
102
+ * Reduce entries, optionally filtered by a set of `runtimeStageIds`.
103
+ * For time-travel progressive view: pass the runtimeStageIds visible
104
+ * at the current slider position. Entries without `runtimeStageId`
105
+ * (structural markers) are excluded when keys are provided. Without
106
+ * keys, reduces all entries (same as `aggregate`).
107
+ */
108
+ accumulate<R>(fn: (acc: R, entry: T) => R, initial: R, keys?: ReadonlySet<string>): R;
109
+ /**
110
+ * Progressive reveal: entries whose `runtimeStageId` is in the visible
111
+ * set. Preserves insertion order. Entries without `runtimeStageId`
112
+ * (structural markers) are buffered and included only when surrounded
113
+ * by visible steps on both sides — trailing markers after the last
114
+ * visible step are discarded.
115
+ */
116
+ getEntriesUpTo(visibleIds: ReadonlySet<string>): T[];
117
+ /** Clear all stored data. Recorders typically call this from their own
118
+ * `clear()` method, which the executor invokes before each run. */
119
+ clear(): void;
120
+ }
@@ -97,7 +97,7 @@ export interface Topology {
97
97
  readonly rootId: string | null;
98
98
  }
99
99
  export interface TopologyRecorderOptions {
100
- /** Recorder id. Defaults to `topology-N` (auto-incremented). */
100
+ /** ScopeRecorder id. Defaults to `topology-N` (auto-incremented). */
101
101
  id?: string;
102
102
  }
103
103
  /**
@@ -1,9 +1,12 @@
1
+ export { BoundaryStateStore } from './BoundaryStateStore.js';
2
+ export { KeyedStore } from './KeyedStore.js';
3
+ export { SequenceStore } from './SequenceStore.js';
1
4
  export { BoundaryStateTracker } from './BoundaryStateTracker.js';
5
+ export { KeyedRecorder } from './KeyedRecorder.js';
6
+ export { SequenceRecorder } from './SequenceRecorder.js';
2
7
  export type { CombinedRecorder } from './CombinedRecorder.js';
3
8
  export { hasEmitRecorderMethods, hasFlowRecorderMethods, hasRecorderMethods, isFlowEvent } from './CombinedRecorder.js';
4
9
  export type { CompositeSnapshot } from './CompositeRecorder.js';
5
10
  export { CompositeRecorder } from './CompositeRecorder.js';
6
11
  export type { EmitEvent, EmitRecorder } from './EmitRecorder.js';
7
- export { KeyedRecorder } from './KeyedRecorder.js';
8
12
  export { RecorderOperation } from './RecorderOperation.js';
9
- export { SequenceRecorder } from './SequenceRecorder.js';
@@ -17,7 +17,7 @@ import type { CommitBundle, StageSnapshot } from '../memory/types.js';
17
17
  export interface RecorderSnapshot {
18
18
  id: string;
19
19
  name: string;
20
- /** Recorder type and pattern description (e.g., "Translator (KeyedRecorder) — per-step token usage"). */
20
+ /** ScopeRecorder type and pattern description (e.g., "Translator (KeyedRecorder) — per-step token usage"). */
21
21
  description?: string;
22
22
  /** Preferred read-time operation — hints the UI about which view to show prominently. */
23
23
  preferredOperation?: 'translate' | 'accumulate' | 'aggregate';