footprintjs 6.1.2 → 7.1.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 (51) hide show
  1. package/CLAUDE.md +31 -26
  2. package/README.md +17 -1
  3. package/dist/advanced.js +2 -6
  4. package/dist/esm/advanced.js +1 -3
  5. package/dist/esm/lib/builder/structure/StructureRecorder.js +2 -2
  6. package/dist/esm/lib/engine/narrative/CombinedNarrativeRecorder.js +59 -24
  7. package/dist/esm/lib/memory/TransactionBuffer.js +92 -9
  8. package/dist/esm/lib/memory/utils.js +62 -1
  9. package/dist/esm/lib/recorder/InOutRecorder.js +31 -15
  10. package/dist/esm/lib/recorder/QualityRecorder.js +40 -13
  11. package/dist/esm/lib/recorder/RecorderOperation.js +3 -3
  12. package/dist/esm/lib/recorder/index.js +1 -8
  13. package/dist/esm/lib/recorder/qualityTrace.js +1 -1
  14. package/dist/esm/lib/runner/ExecutionRuntime.js +1 -1
  15. package/dist/esm/lib/scope/recorders/MetricRecorder.js +37 -9
  16. package/dist/esm/trace.js +1 -12
  17. package/dist/lib/builder/structure/StructureRecorder.js +2 -2
  18. package/dist/lib/engine/narrative/CombinedNarrativeRecorder.js +59 -24
  19. package/dist/lib/memory/TransactionBuffer.js +91 -8
  20. package/dist/lib/memory/utils.js +64 -2
  21. package/dist/lib/recorder/InOutRecorder.js +31 -15
  22. package/dist/lib/recorder/QualityRecorder.js +40 -13
  23. package/dist/lib/recorder/RecorderOperation.js +3 -3
  24. package/dist/lib/recorder/index.js +2 -12
  25. package/dist/lib/recorder/qualityTrace.js +1 -1
  26. package/dist/lib/runner/ExecutionRuntime.js +1 -1
  27. package/dist/lib/scope/recorders/MetricRecorder.js +37 -9
  28. package/dist/trace.js +2 -16
  29. package/dist/types/advanced.d.ts +0 -2
  30. package/dist/types/lib/builder/structure/StructureRecorder.d.ts +1 -1
  31. package/dist/types/lib/engine/narrative/CombinedNarrativeRecorder.d.ts +26 -5
  32. package/dist/types/lib/memory/TransactionBuffer.d.ts +67 -1
  33. package/dist/types/lib/memory/utils.d.ts +29 -0
  34. package/dist/types/lib/recorder/InOutRecorder.d.ts +17 -6
  35. package/dist/types/lib/recorder/QualityRecorder.d.ts +17 -3
  36. package/dist/types/lib/recorder/RecorderOperation.d.ts +2 -2
  37. package/dist/types/lib/recorder/index.d.ts +0 -3
  38. package/dist/types/lib/recorder/qualityTrace.d.ts +5 -2
  39. package/dist/types/lib/runner/ExecutionRuntime.d.ts +1 -1
  40. package/dist/types/lib/scope/recorders/MetricRecorder.d.ts +18 -3
  41. package/dist/types/trace.d.ts +0 -3
  42. package/package.json +1 -1
  43. package/dist/esm/lib/recorder/BoundaryStateTracker.js +0 -260
  44. package/dist/esm/lib/recorder/KeyedRecorder.js +0 -94
  45. package/dist/esm/lib/recorder/SequenceRecorder.js +0 -193
  46. package/dist/lib/recorder/BoundaryStateTracker.js +0 -264
  47. package/dist/lib/recorder/KeyedRecorder.js +0 -98
  48. package/dist/lib/recorder/SequenceRecorder.js +0 -197
  49. package/dist/types/lib/recorder/BoundaryStateTracker.d.ts +0 -212
  50. package/dist/types/lib/recorder/KeyedRecorder.d.ts +0 -56
  51. package/dist/types/lib/recorder/SequenceRecorder.d.ts +0 -112
@@ -51,5 +51,3 @@ export { applyOutputMapping, ChildrenExecutor, computeNodeType, ContinuationReso
51
51
  export type { ExecutionCounter } from './lib/engine/runtimeStageId.js';
52
52
  export { buildRuntimeStageId, createExecutionCounter, parseRuntimeStageId } from './lib/engine/runtimeStageId.js';
53
53
  export { findCommit, findCommits, findLastWriter } from './lib/memory/commitLogUtils.js';
54
- export { BoundaryStateTracker } from './lib/recorder/BoundaryStateTracker.js';
55
- export { KeyedRecorder } from './lib/recorder/KeyedRecorder.js';
@@ -14,7 +14,7 @@
14
14
  *
15
15
  * - Runtime events carry `runtimeStageId` (`[subflowPath/]stageId#N`),
16
16
  * `iteration`, `traversalContext` — load-bearing for time-travel
17
- * UIs, commit-log indices, and `SequenceRecorder<T>` consumers.
17
+ * UIs, commit-log indices, and `SequenceStore<T>` consumers.
18
18
  * NONE of these exist at build time.
19
19
  *
20
20
  * - Build events carry only `stageId` + structural fields. No
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * CombinedNarrativeRecorder — Inline narrative builder that merges flow + data during traversal.
3
3
  *
4
- * Extends SequenceRecorder<CombinedNarrativeEntry> for dual-indexed storage (ordered sequence
5
- * + O(1) per-step lookup by runtimeStageId). Implements `CombinedRecorder` the library's
6
- * first-class abstraction for observers that span both data-flow and control-flow streams.
4
+ * Composes a SequenceStore<CombinedNarrativeEntry> for dual-indexed storage (ordered sequence
5
+ * + O(1) per-step lookup by runtimeStageId) Convention 1, one purpose per recorder.
6
+ * Implements `CombinedRecorder` — the library's first-class abstraction for observers that span
7
+ * both data-flow and control-flow streams.
7
8
  *
8
9
  * Event ordering guarantees this works:
9
10
  * 1. Scope events (onRead, onWrite) fire DURING stage execution
@@ -15,7 +16,6 @@
15
16
  */
16
17
  import type { CombinedRecorder } from '../../recorder/CombinedRecorder.js';
17
18
  import type { EmitEvent } from '../../recorder/EmitRecorder.js';
18
- import { SequenceRecorder } from '../../recorder/SequenceRecorder.js';
19
19
  import type { ErrorEvent, PauseEvent, ReadEvent, ResumeEvent, WriteEvent } from '../../scope/types.js';
20
20
  import type { CombinedNarrativeEntry, NarrativeRenderer } from './narrativeTypes.js';
21
21
  import type { FlowBreakEvent, FlowDecisionEvent, FlowErrorEvent, FlowForkEvent, FlowLoopEvent, FlowPauseEvent, FlowResumeEvent, FlowSelectedEvent, FlowStageEvent, FlowSubflowEvent } from './types.js';
@@ -42,8 +42,10 @@ export interface CombinedNarrativeRecorderOptions {
42
42
  * Scope variants of these events are deliberately ignored here — the
43
43
  * narrative only surfaces control-flow lifecycle events.
44
44
  */
45
- export declare class CombinedNarrativeRecorder extends SequenceRecorder<CombinedNarrativeEntry> implements CombinedRecorder {
45
+ export declare class CombinedNarrativeRecorder implements CombinedRecorder {
46
46
  readonly id: string;
47
+ /** Dual-indexed ordered storage (Convention 1 — composed, not inherited). */
48
+ private readonly store;
47
49
  /**
48
50
  * Pending scope ops keyed by runtimeStageId. Flushed in onStageExecuted/onDecision.
49
51
  *
@@ -100,6 +102,25 @@ export declare class CombinedNarrativeRecorder extends SequenceRecorder<Combined
100
102
  * Root-level entries have subflowId = undefined.
101
103
  */
102
104
  getEntriesBySubflow(): Record<string, CombinedNarrativeEntry[]>;
105
+ /** All narrative entries in execution order. */
106
+ getEntries(): CombinedNarrativeEntry[];
107
+ /** Total number of narrative entries. */
108
+ get entryCount(): number;
109
+ /** O(1) lookup: all narrative entries for one execution step. */
110
+ getEntriesForStep(runtimeStageId: string): CombinedNarrativeEntry[];
111
+ /** Number of distinct execution steps that produced entries. */
112
+ get stepCount(): number;
113
+ /** Pre-built per-step range index — O(1) lookups for time-travel scrubbing. */
114
+ getEntryRanges(): ReadonlyMap<string, {
115
+ readonly firstIdx: number;
116
+ readonly endIdx: number;
117
+ }>;
118
+ /** Reduce ALL entries to a single value. */
119
+ aggregate<R>(fn: (acc: R, entry: CombinedNarrativeEntry) => R, initial: R): R;
120
+ /** Reduce entries, optionally filtered to a set of visible runtimeStageIds. */
121
+ accumulate<R>(fn: (acc: R, entry: CombinedNarrativeEntry) => R, initial: R, keys?: ReadonlySet<string>): R;
122
+ /** Progressive reveal: entries whose runtimeStageId is in the visible set. */
123
+ getEntriesUpTo(visibleIds: ReadonlySet<string>): CombinedNarrativeEntry[];
103
124
  /** Clears all state. Called automatically before each run. */
104
125
  clear(): void;
105
126
  private incrementStageCounter;
@@ -23,7 +23,60 @@ export declare class TransactionBuffer {
23
23
  /** Read current value at path (includes uncommitted changes). */
24
24
  get(path: (string | number)[], defaultValue?: any): any;
25
25
  /**
26
- * Flush all staged mutations and return the commit bundle.
26
+ * Flush all staged mutations and return the commit bundle — recording the
27
+ * stage's NET CHANGE, not its raw write log.
28
+ *
29
+ * ── WHY (the defect this fixes) ─────────────────────────────────────────
30
+ * Previously every `set`/`merge` was recorded verbatim, so the commit bundle
31
+ * was a log of *operations* rather than *changes*. Two operations produce no
32
+ * net change yet were still committed as "mutations":
33
+ *
34
+ * 1. No-op write — writing a key the value it already holds (e.g. an
35
+ * agent context slot re-emitting identical content every
36
+ * turn). base K=1, stage writes K=1.
37
+ * 2. Write-revert — changing then restoring a key within one stage.
38
+ * base K=1, stage writes K=2 then K=1.
39
+ *
40
+ * Recording these as mutations (a) bloated causal slicing / backtracking with
41
+ * spurious dependencies on intermediate values that never reach final state,
42
+ * and (b) made downstream "what changed here?" consumers light up stages that
43
+ * changed nothing — most visibly the lens highlight flagging every slot.
44
+ *
45
+ * ── HOW ─────────────────────────────────────────────────────────────────
46
+ * At commit we hold BOTH `baseSnapshot` (state when the stage began) and
47
+ * `workingCopy` (state after all its writes). For each path the stage touched
48
+ * we keep it in the bundle ONLY if its final value differs from the base
49
+ * value ({@link deepEqual}). No-op AND write-revert paths drop out, because
50
+ * both compare equal to base. This is a single net-delta diff at commit time
51
+ * — one deep compare per touched path, O(changed state), paid once per stage
52
+ * (NOT per write). A naive per-write deep-equal skip would be more expensive
53
+ * and would still miss write-revert (the intermediate write differs from the
54
+ * value present at the moment of writing).
55
+ *
56
+ * ── TWO HONEST TIERS (by design — do not "unify" them) ──────────────────
57
+ * • commit (here) = CHANGE-level — truthful net delta. Feeds the commit
58
+ * log, causal chain, narrative, and the lens highlight.
59
+ * • `onWrite` event = OP-level — fires on EVERY write attempt regardless of
60
+ * net change. Feeds metrics / behavioural observability
61
+ * (a debugger wants to see "wrote 2, then reverted").
62
+ * `onWrite` is unchanged by this method; only the COMMIT becomes change-only.
63
+ *
64
+ * ── EMPTY COMMITS ARE INTENTIONAL ───────────────────────────────────────
65
+ * A stage that nets no change commits an EMPTY patch — NOT nothing.
66
+ * {@link StageContext.commit} still records the bundle unconditionally, so
67
+ * every executed stage remains a time-travel cursor stop (its `runtimeStageId`
68
+ * marker is preserved); only its PATCH is empty. This is what keeps the
69
+ * commit-indexed slider stable while making the highlight truthful.
70
+ *
71
+ * ── KNOWN LIMITATIONS / FUTURE ──────────────────────────────────────────
72
+ * • Explicit key DELETION is still unrepresentable in MemoryPatch (a removed
73
+ * key cannot be expressed). Setting a key to `undefined` is treated as a
74
+ * change (value differs from base), not a deletion. Tracked for a future
75
+ * `delete` verb.
76
+ * • Array-merge dedup in {@link deepSmartMerge} still uses reference equality
77
+ * (`new Set`), so deep-equal *objects* in a merged array are not deduped.
78
+ * Orthogonal to this change; tracked separately.
79
+ *
27
80
  * Resets the buffer to empty state after commit.
28
81
  */
29
82
  commit(): {
@@ -35,4 +88,17 @@ export declare class TransactionBuffer {
35
88
  verb: 'set' | 'merge';
36
89
  }[];
37
90
  };
91
+ /**
92
+ * Rebuild overwrite / updates / trace keeping ONLY paths whose final value
93
+ * differs from the base value — i.e. the stage's net change. See
94
+ * {@link TransactionBuffer.commit} for the rationale.
95
+ *
96
+ * Paths are compared at the exact granularity they were written (each trace
97
+ * entry's path), against `workingCopy` (final) vs `baseSnapshot` (start).
98
+ * Surviving `set` paths copy their final value from `overwritePatch`;
99
+ * surviving `merge` paths copy their accumulated delta from `updatePatch` —
100
+ * preserving the set-vs-merge verb so replay ({@link applySmartMerge}) is
101
+ * byte-for-byte identical to recording only the real changes.
102
+ */
103
+ private toChangeOnlyPayload;
38
104
  }
@@ -57,6 +57,35 @@ export declare function redactPatch(patch: MemoryPatch, redactedSet: Set<string>
57
57
  * Normalises an array path into a stable string key using DELIM.
58
58
  */
59
59
  export declare function normalisePath(path: (string | number)[]): string;
60
+ /**
61
+ * Structural deep equality for committed-state values.
62
+ *
63
+ * Used by {@link TransactionBuffer} to decide whether a stage actually CHANGED
64
+ * a path or merely re-wrote / reverted it to the value it already held (a
65
+ * "no-op write"). Committed state is JSON-shaped — it must survive
66
+ * `structuredClone` — so this only needs to handle the shapes that can reach a
67
+ * commit: primitives, arrays, and plain objects.
68
+ *
69
+ * Semantics:
70
+ * - reference / identical-primitive short-circuits first (cheap fast path)
71
+ * - `NaN` equals `NaN` (primitive compare falls back to `Object.is`)
72
+ * - arrays: equal length AND deep-equal element-wise (order-sensitive)
73
+ * - objects: identical own-key set AND deep-equal per key
74
+ * - mismatched kinds (array vs object, object vs null) → not equal
75
+ *
76
+ * Cost & safety:
77
+ * - Allocates NOTHING but transient `Object.keys` arrays — no clones. It is
78
+ * strictly cheaper than the `structuredClone` the commit already performs.
79
+ * - Primitive comparisons (the bulk of state) are O(1) via the `===` /
80
+ * `Object.is` fast paths; only nested objects/arrays incur a walk, bounded
81
+ * by the value's own size.
82
+ * - Assumes ACYCLIC, JSON-shaped values — the same contract the memory layer
83
+ * already relies on (committed state is `structuredClone`d and must be
84
+ * JSON-serialisable for checkpoints). A cyclic value is out of contract
85
+ * here exactly as it is for checkpointing; dev mode flags cycles at write
86
+ * time via `ScopeFacade.setValue`.
87
+ */
88
+ export declare function deepEqual(a: any, b: any): boolean;
60
89
  /**
61
90
  * Deep union merge helper.
62
91
  * - Arrays (non-empty): union without duplicates (encounter order preserved)
@@ -61,7 +61,6 @@
61
61
  * ```
62
62
  */
63
63
  import type { FlowRecorder, FlowRunEvent, FlowSubflowEvent } from '../engine/narrative/types.js';
64
- import { SequenceRecorder } from './SequenceRecorder.js';
65
64
  export type InOutPhase = 'entry' | 'exit';
66
65
  /** Synthetic id for the top-level run's boundary pair. */
67
66
  export declare const ROOT_SUBFLOW_ID = "__root__";
@@ -120,21 +119,22 @@ export interface InOutRecorderOptions {
120
119
  export declare function inOutRecorder(options?: InOutRecorderOptions): InOutRecorder;
121
120
  /**
122
121
  * Stateful accumulator that watches `FlowRecorder` chart-boundary events
123
- * (run start/end + subflow entry/exit) and emits `InOutEntry` records to
124
- * its underlying `SequenceRecorder` storage.
122
+ * (run start/end + subflow entry/exit) and pushes `InOutEntry` records to
123
+ * its composed `SequenceStore` (Convention 1 — one purpose per recorder).
125
124
  *
126
125
  * Attach via `executor.attachCombinedRecorder(recorder)` — footprintjs
127
126
  * detects the `FlowRecorder` method shape and routes events.
128
127
  */
129
- export declare class InOutRecorder extends SequenceRecorder<InOutEntry> implements FlowRecorder {
128
+ export declare class InOutRecorder implements FlowRecorder {
130
129
  readonly id: string;
130
+ /** 1:N ordered storage with per-step index (Convention 1 — composed). */
131
+ private readonly store;
131
132
  constructor(options?: InOutRecorderOptions);
132
133
  onRunStart(event: FlowRunEvent): void;
133
134
  onRunEnd(event: FlowRunEvent): void;
134
135
  onSubflowEntry(event: FlowSubflowEvent): void;
135
136
  onSubflowExit(event: FlowSubflowEvent): void;
136
- /** All entries in execution order (entry+exit interleaved).
137
- * Alias for `getEntries()` with domain-clearer naming. */
137
+ /** All entries in execution order (entry+exit interleaved). */
138
138
  getBoundaries(): InOutEntry[];
139
139
  /** Entry/exit pair for one chart execution.
140
140
  * Returns `{ entry, exit }` — `exit` is `undefined` for in-progress / paused
@@ -148,6 +148,17 @@ export declare class InOutRecorder extends SequenceRecorder<InOutEntry> implemen
148
148
  * for slider / scrubbing UIs. The top-level run's entry is the first
149
149
  * step (depth 0). */
150
150
  getSteps(): InOutEntry[];
151
+ /** O(1) lookup: all boundary entries (both phases) for one runtimeStageId. */
152
+ getEntriesForStep(runtimeStageId: string): InOutEntry[];
153
+ /** Pre-built per-step range index — O(1) lookups for time-travel scrubbing. */
154
+ getEntryRanges(): ReadonlyMap<string, {
155
+ readonly firstIdx: number;
156
+ readonly endIdx: number;
157
+ }>;
158
+ /** Progressive reveal: boundaries whose runtimeStageId is in the visible set. */
159
+ getEntriesUpTo(visibleIds: ReadonlySet<string>): InOutEntry[];
160
+ /** Clear all stored boundaries — called by the executor before each run(). */
161
+ clear(): void;
151
162
  /** The root run's entry/exit pair, if the run has started.
152
163
  * Convenience for consumers that want to bracket the timeline by the
153
164
  * outermost in/out. */
@@ -4,7 +4,8 @@
4
4
  * Collects quality scores during traversal (accumulate pattern).
5
5
  * After execution, use qualityTrace() to backtrack from any low-scoring step.
6
6
  *
7
- * Extends KeyedRecorder<QualityEntry> for O(1) lookup and standard operations:
7
+ * Composes a `KeyedStore<QualityEntry>` for O(1) lookup and standard operations
8
+ * (Convention 1 — one purpose per recorder):
8
9
  * - **Translate**: `getByKey('call-llm#5')` — quality at this step
9
10
  * - **Accumulate**: progressive quality up to slider position
10
11
  * - **Aggregate**: overall pipeline quality score
@@ -30,7 +31,6 @@
30
31
  * ```
31
32
  */
32
33
  import type { ReadEvent, ScopeRecorder, StageEvent, WriteEvent } from '../scope/types.js';
33
- import { KeyedRecorder } from './KeyedRecorder.js';
34
34
  import type { RecorderOperation } from './RecorderOperation.js';
35
35
  /** Per-step quality data stored by QualityRecorder. */
36
36
  export interface QualityEntry {
@@ -69,10 +69,12 @@ export interface QualityRecorderOptions {
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 ScopeRecorder {
72
+ export declare class QualityRecorder implements ScopeRecorder {
73
73
  private static _counter;
74
74
  readonly id: string;
75
75
  readonly preferredOperation: RecorderOperation;
76
+ /** 1:1 per-step storage (Convention 1 — composed, not inherited). */
77
+ private readonly store;
76
78
  private readonly scoringFn;
77
79
  private currentRuntimeStageId;
78
80
  private currentStageId;
@@ -84,6 +86,18 @@ export declare class QualityRecorder extends KeyedRecorder<QualityEntry> impleme
84
86
  onRead(event: ReadEvent): void;
85
87
  onWrite(event: WriteEvent): void;
86
88
  onStageEnd(event: StageEvent): void;
89
+ /** Translate: quality entry for a specific runtimeStageId. */
90
+ getByKey(runtimeStageId: string): QualityEntry | undefined;
91
+ /** All per-step quality entries as a read-only Map (insertion-ordered). */
92
+ getMap(): ReadonlyMap<string, QualityEntry>;
93
+ /** All per-step quality entries (insertion-ordered). */
94
+ values(): QualityEntry[];
95
+ /** Number of scored steps. */
96
+ get size(): number;
97
+ /** Aggregate: reduce ALL scored steps to a single value. */
98
+ aggregate<R>(fn: (acc: R, entry: QualityEntry, key: string) => R, initial: R): R;
99
+ /** Accumulate: reduce scored steps up to a slider position. */
100
+ accumulate<R>(fn: (acc: R, entry: QualityEntry, key: string) => R, initial: R, keys?: ReadonlySet<string>): R;
87
101
  /** Overall quality score — average of all step scores. */
88
102
  getOverallScore(): number;
89
103
  /** Find the lowest-scoring step. */
@@ -4,9 +4,9 @@
4
4
  * Data is collected during the single DFS traversal. The consumer chooses the
5
5
  * operation at READ time:
6
6
  *
7
- * | Operation | KeyedRecorder method | SequenceRecorder method | Use case |
7
+ * | Operation | KeyedStore method | SequenceStore method | Use case |
8
8
  * |-------------|--------------------------|--------------------------------|-----------------------------|
9
- * | Translate | `getByKey(id)` | `getEntriesForStep(id)` | Per-step detail |
9
+ * | Translate | `get(id)` | `getByKey(id)` | Per-step detail |
10
10
  * | Accumulate | `accumulate(fn, init, k)` | `accumulate(fn, init, k)` | Running total up to slider |
11
11
  * | Aggregate | `aggregate(fn, init)` | `aggregate(fn, init)` | Grand total for dashboards |
12
12
  *
@@ -1,9 +1,6 @@
1
1
  export { BoundaryStateStore } from './BoundaryStateStore.js';
2
2
  export { KeyedStore } from './KeyedStore.js';
3
3
  export { SequenceStore } from './SequenceStore.js';
4
- export { BoundaryStateTracker } from './BoundaryStateTracker.js';
5
- export { KeyedRecorder } from './KeyedRecorder.js';
6
- export { SequenceRecorder } from './SequenceRecorder.js';
7
4
  export type { CombinedRecorder } from './CombinedRecorder.js';
8
5
  export { hasEmitRecorderMethods, hasFlowRecorderMethods, hasRecorderMethods, isFlowEvent } from './CombinedRecorder.js';
9
6
  export type { CompositeSnapshot } from './CompositeRecorder.js';
@@ -14,8 +14,11 @@
14
14
  * ```
15
15
  */
16
16
  import type { CommitBundle } from '../memory/types.js';
17
- import type { KeyedRecorder } from './KeyedRecorder.js';
18
17
  import type { QualityEntry } from './QualityRecorder.js';
18
+ /** Minimal per-step quality lookup — satisfied by `QualityRecorder` (structural, decoupled). */
19
+ export interface QualityLookup {
20
+ getByKey(runtimeStageId: string): QualityEntry | undefined;
21
+ }
19
22
  /** A single frame in the quality stack trace. */
20
23
  export interface QualityFrame {
21
24
  runtimeStageId: string;
@@ -44,7 +47,7 @@ export interface QualityStackTrace {
44
47
  * @param startId runtimeStageId to start from
45
48
  * @param maxDepth Maximum backtracking depth (default: 20)
46
49
  */
47
- export declare function qualityTrace(commitLog: CommitBundle[], qualityRecorder: KeyedRecorder<QualityEntry>, startId: string, maxDepth?: number): QualityStackTrace;
50
+ export declare function qualityTrace(commitLog: CommitBundle[], qualityRecorder: QualityLookup, startId: string, maxDepth?: number): QualityStackTrace;
48
51
  /**
49
52
  * Format a QualityStackTrace as human-readable text.
50
53
  */
@@ -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
- /** ScopeRecorder type and pattern description (e.g., "Translator (KeyedRecorder) — per-step token usage"). */
20
+ /** ScopeRecorder type and pattern description (e.g., "Translator (KeyedStore) — 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';
@@ -2,7 +2,9 @@
2
2
  * MetricRecorder — per-step timing and execution counts, keyed by runtimeStageId.
3
3
  *
4
4
  * Stores per-invocation data during traversal. Aggregated views computed on read.
5
- * Extends KeyedRecorder<StepMetrics> for O(1) lookup and standard operations.
5
+ * Composes a `KeyedStore<StepMetrics>` for O(1) lookup and standard operations
6
+ * (Convention 1 — one purpose per recorder: this is the ScopeRecorder; the store
7
+ * is the storage).
6
8
  *
7
9
  * @example
8
10
  * ```typescript
@@ -20,7 +22,6 @@
20
22
  * metric.accumulate((sum, m) => sum + m.duration, 0, visibleKeys);
21
23
  * ```
22
24
  */
23
- import { KeyedRecorder } from '../../recorder/KeyedRecorder.js';
24
25
  import type { RecorderOperation } from '../../recorder/RecorderOperation.js';
25
26
  import type { CommitEvent, PauseEvent, ReadEvent, ScopeRecorder, StageEvent, WriteEvent } from '../types.js';
26
27
  /** Per-invocation metrics for a single execution step. */
@@ -67,10 +68,12 @@ export interface MetricRecorderOptions {
67
68
  /** Preferred UI operation. Defaults to 'aggregate' (dashboard totals). */
68
69
  preferredOperation?: RecorderOperation;
69
70
  }
70
- export declare class MetricRecorder extends KeyedRecorder<StepMetrics> implements ScopeRecorder {
71
+ export declare class MetricRecorder implements ScopeRecorder {
71
72
  private static _counter;
72
73
  readonly id: string;
73
74
  readonly preferredOperation: RecorderOperation;
75
+ /** 1:1 per-step storage (Convention 1 — composed, not inherited). */
76
+ private readonly store;
74
77
  private stageStartTimes;
75
78
  private currentRuntimeStageId;
76
79
  private stageFilter?;
@@ -78,6 +81,18 @@ export declare class MetricRecorder extends KeyedRecorder<StepMetrics> implement
78
81
  private shouldRecord;
79
82
  /** Get or create the StepMetrics for the current stage. */
80
83
  private current;
84
+ /** Translate: per-step metrics by runtimeStageId. */
85
+ getByKey(runtimeStageId: string): StepMetrics | undefined;
86
+ /** All per-step metrics as a read-only Map (insertion-ordered). */
87
+ getMap(): ReadonlyMap<string, StepMetrics>;
88
+ /** All per-step metrics (insertion-ordered). */
89
+ values(): StepMetrics[];
90
+ /** Number of recorded steps. */
91
+ get size(): number;
92
+ /** Aggregate: reduce ALL steps to a single value (dashboards, totals). */
93
+ aggregate<R>(fn: (acc: R, entry: StepMetrics, key: string) => R, initial: R): R;
94
+ /** Accumulate: reduce steps up to a slider position (progressive view). */
95
+ accumulate<R>(fn: (acc: R, entry: StepMetrics, key: string) => R, initial: R, keys?: ReadonlySet<string>): R;
81
96
  onStageStart(event: StageEvent): void;
82
97
  onRead(event: ReadEvent): void;
83
98
  onWrite(event: WriteEvent): void;
@@ -41,9 +41,6 @@ export { KeyedStore } from './lib/recorder/KeyedStore.js';
41
41
  export { SequenceStore } from './lib/recorder/SequenceStore.js';
42
42
  export type { RangeEntry, RangeToken } from './lib/recorder/CommitRangeIndex.js';
43
43
  export { CommitRangeIndex } from './lib/recorder/CommitRangeIndex.js';
44
- export { KeyedRecorder } from './lib/recorder/KeyedRecorder.js';
45
- export { SequenceRecorder } from './lib/recorder/SequenceRecorder.js';
46
- export { BoundaryStateTracker } from './lib/recorder/BoundaryStateTracker.js';
47
44
  export type { Topology, TopologyEdge, TopologyIncomingKind, TopologyNode, TopologyRecorderOptions, } from './lib/recorder/TopologyRecorder.js';
48
45
  export { TopologyRecorder, topologyRecorder } from './lib/recorder/TopologyRecorder.js';
49
46
  export type { InOutEntry, InOutPhase, InOutRecorderOptions } from './lib/recorder/InOutRecorder.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "footprintjs",
3
- "version": "6.1.2",
3
+ "version": "7.1.0",
4
4
  "description": "Explainable backend flows — automatic causal traces, decision evidence, and MCP tool generation for AI agents",
5
5
  "license": "MIT",
6
6
  "author": "Sanjay Krishna Anbalagan",