footprintjs 5.0.0 → 6.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 (91) hide show
  1. package/CLAUDE.md +51 -8
  2. package/README.md +1 -0
  3. package/dist/advanced.js +2 -3
  4. package/dist/esm/advanced.js +2 -2
  5. package/dist/esm/index.js +2 -2
  6. package/dist/esm/lib/builder/FlowChartBuilder.js +552 -103
  7. package/dist/esm/lib/builder/index.js +2 -2
  8. package/dist/esm/lib/builder/structure/StructureRecorder.js +158 -0
  9. package/dist/esm/lib/builder/structure/StructureRecorderDispatcher.js +171 -0
  10. package/dist/esm/lib/builder/types.js +1 -1
  11. package/dist/esm/lib/engine/errors/errorInfo.js +4 -3
  12. package/dist/esm/lib/engine/handlers/ChildrenExecutor.js +14 -3
  13. package/dist/esm/lib/engine/handlers/DeciderHandler.js +5 -7
  14. package/dist/esm/lib/engine/handlers/RuntimeStructureManager.js +2 -2
  15. package/dist/esm/lib/engine/handlers/SelectorHandler.js +13 -7
  16. package/dist/esm/lib/engine/handlers/SubflowExecutor.js +7 -1
  17. package/dist/esm/lib/engine/handlers/index.js +1 -3
  18. package/dist/esm/lib/engine/handlers/types.js +2 -2
  19. package/dist/esm/lib/engine/index.js +1 -1
  20. package/dist/esm/lib/engine/narrative/CombinedNarrativeRecorder.js +12 -2
  21. package/dist/esm/lib/engine/narrative/FlowRecorderDispatcher.js +17 -3
  22. package/dist/esm/lib/engine/narrative/NarrativeFlowRecorder.js +8 -1
  23. package/dist/esm/lib/engine/narrative/NullControlFlowNarrativeGenerator.js +2 -1
  24. package/dist/esm/lib/engine/narrative/index.js +1 -1
  25. package/dist/esm/lib/engine/narrative/types.js +1 -1
  26. package/dist/esm/lib/engine/runtimeStageId.js +64 -2
  27. package/dist/esm/lib/engine/traversal/FlowchartTraverser.js +35 -57
  28. package/dist/esm/lib/engine/types.js +1 -1
  29. package/dist/esm/lib/engine/walkSubflowSpec.js +168 -0
  30. package/dist/esm/lib/recorder/CombinedRecorder.js +8 -1
  31. package/dist/esm/lib/runner/FlowChartExecutor.js +23 -17
  32. package/dist/esm/lib/runner/getSubtreeSnapshot.js +3 -2
  33. package/dist/esm/trace.js +3 -2
  34. package/dist/index.js +3 -2
  35. package/dist/lib/builder/FlowChartBuilder.js +554 -104
  36. package/dist/lib/builder/index.js +3 -2
  37. package/dist/lib/builder/structure/StructureRecorder.js +159 -0
  38. package/dist/lib/builder/structure/StructureRecorderDispatcher.js +175 -0
  39. package/dist/lib/builder/types.js +1 -1
  40. package/dist/lib/engine/errors/errorInfo.js +4 -3
  41. package/dist/lib/engine/handlers/ChildrenExecutor.js +14 -3
  42. package/dist/lib/engine/handlers/DeciderHandler.js +5 -7
  43. package/dist/lib/engine/handlers/RuntimeStructureManager.js +2 -2
  44. package/dist/lib/engine/handlers/SelectorHandler.js +13 -7
  45. package/dist/lib/engine/handlers/SubflowExecutor.js +7 -1
  46. package/dist/lib/engine/handlers/index.js +2 -5
  47. package/dist/lib/engine/handlers/types.js +2 -2
  48. package/dist/lib/engine/index.js +1 -1
  49. package/dist/lib/engine/narrative/CombinedNarrativeRecorder.js +12 -2
  50. package/dist/lib/engine/narrative/FlowRecorderDispatcher.js +17 -3
  51. package/dist/lib/engine/narrative/NarrativeFlowRecorder.js +8 -1
  52. package/dist/lib/engine/narrative/NullControlFlowNarrativeGenerator.js +2 -1
  53. package/dist/lib/engine/narrative/index.js +1 -1
  54. package/dist/lib/engine/narrative/types.js +1 -1
  55. package/dist/lib/engine/runtimeStageId.js +66 -3
  56. package/dist/lib/engine/traversal/FlowchartTraverser.js +35 -57
  57. package/dist/lib/engine/types.js +1 -1
  58. package/dist/lib/engine/walkSubflowSpec.js +172 -0
  59. package/dist/lib/recorder/CombinedRecorder.js +8 -1
  60. package/dist/lib/runner/FlowChartExecutor.js +23 -17
  61. package/dist/lib/runner/getSubtreeSnapshot.js +3 -2
  62. package/dist/trace.js +5 -2
  63. package/dist/types/advanced.d.ts +4 -4
  64. package/dist/types/index.d.ts +4 -2
  65. package/dist/types/lib/builder/FlowChartBuilder.d.ts +202 -17
  66. package/dist/types/lib/builder/index.d.ts +3 -2
  67. package/dist/types/lib/builder/structure/StructureRecorder.d.ts +349 -0
  68. package/dist/types/lib/builder/structure/StructureRecorderDispatcher.d.ts +77 -0
  69. package/dist/types/lib/builder/types.d.ts +39 -6
  70. package/dist/types/lib/engine/errors/errorInfo.d.ts +3 -2
  71. package/dist/types/lib/engine/handlers/ChildrenExecutor.d.ts +1 -1
  72. package/dist/types/lib/engine/handlers/DeciderHandler.d.ts +3 -3
  73. package/dist/types/lib/engine/handlers/RuntimeStructureManager.d.ts +1 -1
  74. package/dist/types/lib/engine/handlers/SelectorHandler.d.ts +2 -2
  75. package/dist/types/lib/engine/handlers/index.d.ts +1 -2
  76. package/dist/types/lib/engine/handlers/types.d.ts +2 -9
  77. package/dist/types/lib/engine/index.d.ts +1 -1
  78. package/dist/types/lib/engine/narrative/FlowRecorderDispatcher.d.ts +4 -2
  79. package/dist/types/lib/engine/narrative/NullControlFlowNarrativeGenerator.d.ts +1 -0
  80. package/dist/types/lib/engine/narrative/index.d.ts +1 -1
  81. package/dist/types/lib/engine/narrative/types.d.ts +59 -2
  82. package/dist/types/lib/engine/runtimeStageId.d.ts +57 -1
  83. package/dist/types/lib/engine/traversal/FlowchartTraverser.d.ts +1 -6
  84. package/dist/types/lib/engine/types.d.ts +19 -26
  85. package/dist/types/lib/engine/walkSubflowSpec.d.ts +95 -0
  86. package/dist/types/lib/runner/FlowChartExecutor.d.ts +4 -16
  87. package/dist/types/trace.d.ts +3 -1
  88. package/package.json +6 -4
  89. package/dist/esm/lib/engine/handlers/ExtractorRunner.js +0 -122
  90. package/dist/lib/engine/handlers/ExtractorRunner.js +0 -126
  91. package/dist/types/lib/engine/handlers/ExtractorRunner.d.ts +0 -41
@@ -0,0 +1,349 @@
1
+ /**
2
+ * StructureRecorder — passive observer of BUILD-TIME chart construction events.
3
+ *
4
+ * The build-time twin of `FlowRecorder`. Where `FlowRecorder` observes
5
+ * RUNTIME control-flow transitions (per-execution `runtimeStageId`,
6
+ * iteration counts, errors, pauses), `StructureRecorder` observes
7
+ * STATIC chart shape as it's assembled by the builder — every spec
8
+ * node added, every outgoing edge wired, every decider closed.
9
+ *
10
+ * Why two interfaces, not one
11
+ * ───────────────────────────
12
+ * Build and runtime events have different INVARIANTS, not just
13
+ * different timing:
14
+ *
15
+ * - Runtime events carry `runtimeStageId` (`[subflowPath/]stageId#N`),
16
+ * `iteration`, `traversalContext` — load-bearing for time-travel
17
+ * UIs, commit-log indices, and `SequenceRecorder<T>` consumers.
18
+ * NONE of these exist at build time.
19
+ *
20
+ * - Build events carry only `stageId` + structural fields. No
21
+ * execution index, no iteration, no snapshot.
22
+ *
23
+ * - Some events have no analog across phases: `onPause` /
24
+ * `onResume` / `onError` / `onRunStart` are runtime-only;
25
+ * `onDeciderComplete` / `onStageAdded` are build-only.
26
+ *
27
+ * A single `FlowRecorder` with `phase: 'static' | 'dynamic'`
28
+ * discriminator would force every consumer to write
29
+ * `if (e.phase === 'static') ...` branches and would erase the
30
+ * least-privilege boundary (a static-only consumer should NOT
31
+ * accidentally observe runtime data — e.g., a public docs site
32
+ * rendering chart topology must not see scope values).
33
+ *
34
+ * Two interfaces, same mental model, clean separation. Two
35
+ * registration sites ship today (pick whichever fits your call
36
+ * site):
37
+ *
38
+ * 1. **Options-bag** (preferred when the recorder set is known
39
+ * at construction time — registers BEFORE `start()` so even
40
+ * the seed event fires through the normal dispatcher fan-out):
41
+ * ```ts
42
+ * flowChart('seed', fn, 'seed', { structureRecorders: [rec] })
43
+ * .addFunction('a', fnA, 'a').build();
44
+ * ```
45
+ *
46
+ * 2. **Fluent** (preferred when attach is conditional, late, or
47
+ * in a chain builder — the just-attached recorder gets a
48
+ * one-time seed replay so it observes the root stage):
49
+ * ```ts
50
+ * flowChart('seed', fn, 'seed')
51
+ * .attachStructureRecorder(rec)
52
+ * .addFunction('a', fnA, 'a').build();
53
+ * ```
54
+ *
55
+ * Both deliver the same event stream to the same recorder. See the
56
+ * `@example` block below for a fully-fleshed-out implementation.
57
+ *
58
+ * Lifecycle + ordering invariants
59
+ * ───────────────────────────────
60
+ * - Fires SYNCHRONOUSLY during builder chain calls (and during
61
+ * `.build()` for the terminal node) at the natural moment each
62
+ * spec mutation completes.
63
+ * - `onStageAdded(A)` and `onStageAdded(B)` fire BEFORE any
64
+ * `onEdgeAdded({from: A, to: B})` — endpoint registration
65
+ * happens before edge wiring.
66
+ * - `onDeciderComplete(D)` fires at sub-builder `.end()` time, AFTER
67
+ * every child's `onStageAdded` and every `onEdgeAdded({from: D,
68
+ * to: child})`. Marks the decider as no-further-mutation.
69
+ * - `onSubflowMounted({subflowId})` fires once per mount.
70
+ * **MOUNT-ONLY**: parent recorders do NOT receive a replay of
71
+ * the subflow's own internal structure events (those fired
72
+ * during the subflow's own `.build()`). Matches the runtime
73
+ * `onSubflowEntry` semantics — composition over replay.
74
+ *
75
+ * Error isolation (matches FlowRecorder contract)
76
+ * ───────────────────────────────────────────────
77
+ * - A throwing handler is caught, logged via `isDevMode()` warning,
78
+ * and accumulated on `builder.getStructureBuildErrors()` for
79
+ * post-hoc inspection.
80
+ * - Subsequent recorders + subsequent events fire normally; one
81
+ * misbehaving recorder cannot THROW its way into blocking
82
+ * construction.
83
+ * - The errors[] accumulator is soft-capped (~100 entries) with a
84
+ * `__truncated__` sentinel record once the cap is hit, to prevent
85
+ * unbounded growth on chatty broken recorders.
86
+ * - Note: throw isolation does NOT defend against a handler that
87
+ * succeeds at runtime but mutates `event.spec` to corrupt the
88
+ * chart. See "Spec mutation" below.
89
+ *
90
+ * Spec mutation — readonly-by-contract (NOT runtime-enforced)
91
+ * ────────────────────────────────────────────────────────────
92
+ * - Event payloads pass spec references where useful (e.g., `spec`
93
+ * on `onStageAdded`). Every field on every event payload is
94
+ * marked `readonly` in TypeScript — the type system signals
95
+ * consumer intent at author time.
96
+ * - There is NO runtime freeze. The builder fires `onStageAdded`
97
+ * IMMEDIATELY when a spec node is added, BEFORE the builder
98
+ * wires that node's `.next`/`.children`/`.loopTarget` in the
99
+ * subsequent `addX` call. Freezing here would break the
100
+ * builder's own subsequent mutation.
101
+ * - Handler mutation of `event.spec` is DOCUMENTED UNDEFINED
102
+ * BEHAVIOR — it will succeed at runtime, but is not supported
103
+ * and downstream consumers may break. Specifically, mutating
104
+ * `event.spec.fn`, `event.spec.id`, or `event.spec.children`
105
+ * can corrupt subsequent runtime behavior of the chart.
106
+ * - **Trust model**: attach ONLY recorders from trusted code paths.
107
+ * A hostile or buggy recorder can effectively substitute stage
108
+ * functions or rewire the chart structure. The `readonly`
109
+ * markers cannot defend against intentional mutation.
110
+ *
111
+ * @example
112
+ * ```ts
113
+ * class TraceStructureRecorder implements StructureRecorder {
114
+ * readonly id = 'trace-static';
115
+ * nodes: XyflowNode[] = [];
116
+ * edges: XyflowEdge[] = [];
117
+ *
118
+ * onStageAdded(e) {
119
+ * // Node role: 'stage' is the initial type. If outgoing edges
120
+ * // arrive with kind 'fork-branch' / 'decision-branch' the node
121
+ * // is functioning as a fork/decider — derive the visual style
122
+ * // from those edges, not from `e.type` alone.
123
+ * this.nodes.push({ id: e.stageId, type: 'lensStage', data: { label: e.name } });
124
+ * }
125
+ * onEdgeAdded(e) {
126
+ * this.edges.push({ id: `${e.from}->${e.to}`, source: e.from, target: e.to });
127
+ * }
128
+ * onSubflowMounted(e) {
129
+ * this.nodes.push({ id: e.subflowId, type: 'subflow', data: { label: e.subflowName } });
130
+ * }
131
+ * onDeciderComplete(e) {
132
+ * // Optional: finalize node styling now that branch list is sealed.
133
+ * }
134
+ * }
135
+ *
136
+ * // Registration option A — options-bag (preferred when recorders
137
+ * // are known up front; attaches BEFORE the seed event fires):
138
+ * const rec = new TraceStructureRecorder();
139
+ * const builder = flowChart('seed', fn, 'seed', {
140
+ * structureRecorders: [rec],
141
+ * }).addFunction('a', fnA, 'a');
142
+ * const chart = builder.build();
143
+ *
144
+ * // Registration option B — fluent chain (preferred for conditional
145
+ * // or late attach; the just-attached recorder gets a seed replay):
146
+ * const rec2 = new TraceStructureRecorder();
147
+ * const builder2 = flowChart('seed', fn, 'seed')
148
+ * .attachStructureRecorder(rec2)
149
+ * .addFunction('a', fnA, 'a');
150
+ *
151
+ * // Inspect any recorder errors (call on the BUILDER, not the chart):
152
+ * const errors = builder.getStructureBuildErrors();
153
+ *
154
+ * <ReactFlow nodes={rec.nodes} edges={rec.edges} />
155
+ * ```
156
+ */
157
+ import type { FlowChartSpec, SerializedPipelineStructure } from '../types.js';
158
+ /**
159
+ * Event payload for `StructureRecorder.onStageAdded`. Fires for every
160
+ * spec node added to the chart, including the seed (added via
161
+ * `flowChart()` factory) and subflow mount nodes. Excludes synthetic
162
+ * loop-reference nodes (those carry `isLoopReference: true` on the
163
+ * spec; consumers see them via the separate `onLoopEdgeAdded` event
164
+ * instead of as fresh stages).
165
+ */
166
+ export interface StructureStageAddedEvent {
167
+ /**
168
+ * Stable identifier for this node. AT EVENT FIRE TIME this is the
169
+ * builder's LOCAL form (no subflow prefix — the prefix is applied
170
+ * later by `_prefixNodeTree` when this builder is mounted as a
171
+ * subflow into a parent).
172
+ *
173
+ * Note: `spec.id` is a LIVE reference. If you read it AFTER this
174
+ * chart has been mounted as a subflow, it may have been rewritten
175
+ * to the FULL prefixed form. Use `splitStageId(spec.id)` from
176
+ * `footprintjs/trace` to decompose it back into local + subflowPath.
177
+ *
178
+ * Correlating with runtime events:
179
+ * - Same builder (no mount) →
180
+ * `event.stageId === parseRuntimeStageId(runtime).stageId`
181
+ * - This builder mounted as subflow → use `splitStageId` on the
182
+ * prefixed form (or `parseRuntimeStageId` on the full
183
+ * runtimeStageId) before comparing.
184
+ */
185
+ readonly stageId: string;
186
+ /** Human-readable label for the node. */
187
+ readonly name: string;
188
+ /** Spec node `type`. Lets recorders branch on stage vs decider vs
189
+ * fork vs subflow without re-deriving from other fields. */
190
+ readonly type: NonNullable<FlowChartSpec['type']>;
191
+ /** True for nodes added via `addPausableFunction()` — useful for
192
+ * visualisers that want to mark stages that can pause execution. */
193
+ readonly isPausable?: boolean;
194
+ /** Live reference to the full spec node, for handlers that want
195
+ * details beyond the discriminator fields above. MUST NOT be
196
+ * mutated by the handler — readonly is type-level intent; no
197
+ * runtime freeze. See "Spec mutation" in the file header for the
198
+ * trust-model caveats. */
199
+ readonly spec: FlowChartSpec;
200
+ }
201
+ /** Edge kinds the builder produces at structural time. Matches the
202
+ * footprintjs control-flow vocabulary used at runtime.
203
+ *
204
+ * Why no `subflow-entry` here: subflow mounts use the lifecycle
205
+ * event `onSubflowMounted` (not an edge event). If a future use case
206
+ * needs the parent→subflow-root edge as a distinct kind, it'll be
207
+ * added then — YAGNI now (Panel 3, L7.2 review). */
208
+ export type StructureEdgeKind = 'next' | 'fork-branch' | 'decision-branch';
209
+ /**
210
+ * Event payload for `StructureRecorder.onEdgeAdded`. Fires whenever
211
+ * the builder wires an outgoing edge from one node to another. Both
212
+ * endpoints have already been announced via `onStageAdded` by the
213
+ * time this fires.
214
+ *
215
+ * Loop back-edges have their OWN event (`onLoopEdgeAdded`) so
216
+ * consumers can theme them distinctly and so build-time events stay
217
+ * free of the runtime-only `iteration` field.
218
+ */
219
+ export interface StructureEdgeAddedEvent {
220
+ readonly from: string;
221
+ readonly to: string;
222
+ readonly kind: StructureEdgeKind;
223
+ /** Optional human-readable label — used today for decider branch
224
+ * keys (e.g., `'low'`, `'high'`, `'high (default)'`) and parallel
225
+ * branch ids. */
226
+ readonly label?: string;
227
+ }
228
+ /**
229
+ * Event payload for `StructureRecorder.onLoopEdgeAdded`. Fires when
230
+ * `.loopTo(target)` installs a back-edge. Distinct from
231
+ * `onEdgeAdded` so consumers can theme back-edges differently and
232
+ * because runtime `onLoop` carries `iteration: number` which has no
233
+ * build-time meaning — keeping the events separated avoids forcing
234
+ * an `iteration?: number` lie into the structural shape.
235
+ */
236
+ export interface StructureLoopEdgeAddedEvent {
237
+ readonly from: string;
238
+ readonly to: string;
239
+ }
240
+ /**
241
+ * Event payload for `StructureRecorder.onDeciderComplete`. Fires
242
+ * when a decider / selector sub-builder closes via `.end()`. By the
243
+ * time this fires every branch has been announced and the
244
+ * `defaultBranch` (if set via `.setDefault()`) is final. Lets
245
+ * recorders treat the decider as no-further-mutation and snapshot
246
+ * its branch list as final.
247
+ */
248
+ export interface StructureDeciderCompleteEvent {
249
+ /** Decider / selector stage id. */
250
+ readonly decider: string;
251
+ /** Spec type (`'decider'` vs `'selector'`) — exposed so consumers
252
+ * don't need to look up the original `onStageAdded` event. */
253
+ readonly type: 'decider' | 'selector';
254
+ /** Branch member ids in declaration order. */
255
+ readonly branchIds: readonly string[];
256
+ /** Default branch id when set via `.setDefault()`; undefined when
257
+ * no default is configured (e.g., selectors). */
258
+ readonly defaultBranch?: string;
259
+ }
260
+ /**
261
+ * Event payload for `StructureRecorder.onSubflowMounted`. Fires once
262
+ * per subflow mount (`addSubFlowChart` / `addSubFlowChartNext` and
263
+ * their lazy variants).
264
+ *
265
+ * **MOUNT-ONLY semantics** — the subflow's OWN structure events
266
+ * (every `onStageAdded` / `onEdgeAdded` inside the subflow) fired
267
+ * during the subflow's own `.build()`, before the mount. Parent
268
+ * recorders do NOT receive a replay. Instead, this event delivers
269
+ * the subflow's full structure via `subflowSpec`, and consumers can
270
+ * walk it via `walkSubflowSpec` from `footprintjs/trace`.
271
+ *
272
+ * This matches `FlowRecorder.onSubflowEntry` semantics at runtime
273
+ * (parents see the boundary, not a replay of internals) and
274
+ * preserves the "one purpose per recorder" convention.
275
+ */
276
+ export interface StructureSubflowMountedEvent {
277
+ /** Subflow identifier the parent assigned at mount. */
278
+ readonly subflowId: string;
279
+ /** Human-readable subflow name. */
280
+ readonly subflowName: string;
281
+ /** Mount node id in the parent's chart. */
282
+ readonly rootStageId: string;
283
+ /** True when the mount uses lazy resolution (deferred subflow
284
+ * resolution until execution). */
285
+ readonly isLazy?: boolean;
286
+ /**
287
+ * The mounted subflow's complete spec — the SAME OBJECT
288
+ * (=== reference equality) reachable via `subflow.buildTimeStructure`
289
+ * for the FlowChart the consumer passed to `addSubFlowChartBranch` /
290
+ * `addSubFlowChart` / `addSubFlowChartNext`.
291
+ *
292
+ * UNDEFINED for lazy mounts at build time (`isLazy: true`) — the
293
+ * subflow's structure hasn't been resolved yet. Consumers waiting on
294
+ * lazy subflows handle the runtime `onSubflowEntry` event instead.
295
+ *
296
+ * Immutable post-build; consumers MUST NOT mutate.
297
+ *
298
+ * @internal `SerializedPipelineStructure` is library-internal.
299
+ * Use `walkSubflowSpec` from `footprintjs/trace` for the
300
+ * stable public contract.
301
+ */
302
+ readonly subflowSpec?: SerializedPipelineStructure;
303
+ /**
304
+ * Local mount id of this subflow within its parent — equal to
305
+ * `subflowId` for top-level mounts (e.g. `'auth'`), composed
306
+ * (`'auth/verify'`) for nested mounts whose recorder is attached
307
+ * to the grandparent. Matches runtime `traversalContext.subflowPath`
308
+ * semantics — NEVER prefixed with `__root__/`.
309
+ */
310
+ readonly subflowPath: string;
311
+ }
312
+ /**
313
+ * Pluggable observer for BUILD-TIME chart construction events.
314
+ *
315
+ * All methods are optional — implement only the hooks you need.
316
+ * Handlers fire SYNCHRONOUSLY during builder operations and `.build()`.
317
+ * A throwing handler is caught (see "Error isolation" in the file
318
+ * header); subsequent dispatch continues.
319
+ *
320
+ * Lifecycle: each `StructureRecorder` instance is registered via
321
+ * EITHER the `flowChart()` options bag (`{ structureRecorders: [rec] }`)
322
+ * OR the fluent `.attachStructureRecorder(rec)` chain — see the file
323
+ * header for the trade-offs. Registration after `.build()` is
324
+ * rejected (chart is sealed).
325
+ */
326
+ export interface StructureRecorder {
327
+ /**
328
+ * Stable identifier — used by the dispatcher to scope error
329
+ * messages and to support `detach(id)` lookup.
330
+ *
331
+ * Convention (matches `FlowRecorder.id` from CLAUDE.md):
332
+ * - domain-prefixed kebab-case (`'lens-structure'`,
333
+ * `'agentfootprint-composition'`, `'trace-explainable'`)
334
+ * - same id → multiple recorders coexist (NOT deduplicated by
335
+ * id; the dispatcher fires every attached recorder regardless)
336
+ * - `detach(id)` removes EVERY recorder with that id — be
337
+ * intentional about reuse if you rely on selective detach
338
+ *
339
+ * Choose a stable id at module load and don't randomise it across
340
+ * runs — it lands in `StructureBuildError.recorderId` so debug
341
+ * output is greppable.
342
+ */
343
+ readonly id: string;
344
+ onStageAdded?(event: StructureStageAddedEvent): void;
345
+ onEdgeAdded?(event: StructureEdgeAddedEvent): void;
346
+ onLoopEdgeAdded?(event: StructureLoopEdgeAddedEvent): void;
347
+ onDeciderComplete?(event: StructureDeciderCompleteEvent): void;
348
+ onSubflowMounted?(event: StructureSubflowMountedEvent): void;
349
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * StructureRecorderDispatcher — Fans build-time structure events out to N
3
+ * attached `StructureRecorder` instances.
4
+ *
5
+ * Mirrors `FlowRecorderDispatcher` (engine/narrative/) exactly:
6
+ *
7
+ * - `recorders: StructureRecorder[]` — attach order preserved
8
+ * - per-event fire methods early-return when no recorders attached
9
+ * (zero-allocation fast path)
10
+ * - per-recorder try/catch isolates errors; one bad recorder cannot
11
+ * cascade into the chain build or sibling recorders
12
+ * - errors route to BOTH the dev-mode console warning (matches
13
+ * FlowRecorderDispatcher) AND a structured `buildErrors`
14
+ * accumulator so consumers can inspect failures post-build
15
+ * - spec payloads are NOT frozen at dispatch time — handlers must
16
+ * respect the `readonly` markers on event payload types (the
17
+ * builder still needs to mutate `spec.next` after the immediate
18
+ * `onStageAdded` fires; see `fireStageAdded` for the full note,
19
+ * and `StructureRecorder.ts` header "Spec mutation" for the
20
+ * trust-model implications)
21
+ *
22
+ * The dispatcher itself owns NO chart state. The builder owns the
23
+ * dispatcher; events fire from the natural mutation points in
24
+ * FlowChartBuilder (L7.3).
25
+ */
26
+ import type { FlowChartSpec } from '../types.js';
27
+ import type { StructureDeciderCompleteEvent, StructureEdgeAddedEvent, StructureLoopEdgeAddedEvent, StructureRecorder, StructureStageAddedEvent, StructureSubflowMountedEvent } from './StructureRecorder.js';
28
+ /** Structured error captured when a recorder throws. Read post-build
29
+ * via `builder.getStructureBuildErrors()` — call on the BUILDER
30
+ * reference (NOT the chart returned by `.build()`). Capture the
31
+ * builder reference before `.build()` if you need post-build access. */
32
+ export interface StructureBuildError {
33
+ /** Which recorder's handler threw. */
34
+ readonly recorderId: string;
35
+ /** Which event method (`'onStageAdded'`, `'onEdgeAdded'`, ...). */
36
+ readonly method: string;
37
+ /** Error message extracted from the thrown value. */
38
+ readonly message: string;
39
+ /** The original thrown value — `Error` instance or whatever the
40
+ * recorder threw. Useful when diagnosis needs a stack trace. */
41
+ readonly error: unknown;
42
+ }
43
+ export declare class StructureRecorderDispatcher {
44
+ private recorders;
45
+ private readonly errors;
46
+ private _truncated;
47
+ /** Attach a `StructureRecorder`. Multiple recorders with the same
48
+ * id are allowed; the convention is one id per logical concern. */
49
+ attach(recorder: StructureRecorder): void;
50
+ /** Detach every recorder with the given id. */
51
+ detach(id: string): void;
52
+ /** Defensive copy of the attached recorders — used in tests + by
53
+ * tooling that wants to inspect what's registered. */
54
+ getRecorders(): StructureRecorder[];
55
+ /** Find one recorder by id. */
56
+ getRecorderById<T extends StructureRecorder = StructureRecorder>(id: string): T | undefined;
57
+ /** Read accumulated errors from this build. Returns a defensive copy. */
58
+ getErrors(): StructureBuildError[];
59
+ fireStageAdded(event: StructureStageAddedEvent): void;
60
+ fireEdgeAdded(event: StructureEdgeAddedEvent): void;
61
+ fireLoopEdgeAdded(event: StructureLoopEdgeAddedEvent): void;
62
+ fireDeciderComplete(event: StructureDeciderCompleteEvent): void;
63
+ fireSubflowMounted(event: StructureSubflowMountedEvent): void;
64
+ /**
65
+ * Externally-callable error capture for events the builder fires
66
+ * OUTSIDE the normal fire* fan-out path — specifically the seed
67
+ * replay in `FlowChartBuilder.attachStructureRecorder()`, which
68
+ * targets one specific recorder rather than every attached recorder.
69
+ *
70
+ * Same observability contract as the internal `recordError`:
71
+ * accumulates on `getErrors()` AND logs in dev mode.
72
+ */
73
+ recordErrorForReplay(recorderId: string, method: string, err: unknown): void;
74
+ private recordError;
75
+ }
76
+ /** Re-export the FlowChartSpec type for downstream type completeness. */
77
+ export type { FlowChartSpec };
@@ -9,8 +9,9 @@
9
9
  * The builder remains standalone at runtime.
10
10
  */
11
11
  import type { StageNode } from '../engine/graph/StageNode.js';
12
- import type { ILogger, ScopeFactory, StageFunction, TraversalExtractor } from '../engine/types.js';
12
+ import type { ILogger, ScopeFactory, StageFunction } from '../engine/types.js';
13
13
  import type { ScopeProtectionMode } from '../scope/protection/types.js';
14
+ import type { StructureRecorder } from './structure/StructureRecorder.js';
14
15
  export type { ResumeFn, StageNode } from '../engine/graph/StageNode.js';
15
16
  export type { ILogger, StageFunction, StreamCallback, StreamHandlers, StreamLifecycleHandler, StreamTokenHandler, SubflowMountOptions, } from '../engine/types.js';
16
17
  export { ArrayMergeMode } from '../engine/types.js';
@@ -52,6 +53,15 @@ export interface SerializedPipelineStructure {
52
53
  isLoopReference?: boolean;
53
54
  /** When true, this stage can pause execution (PausableHandler pattern). */
54
55
  isPausable?: boolean;
56
+ /**
57
+ * STRUCTURE-ONLY: for a fork/selector/decider branch, the downstream stage
58
+ * id its convergence `next` edge points to, instead of the shared next-stage
59
+ * its siblings converge at. Set from `SubflowMountOptions.convergeAt`; read by
60
+ * `_fireNextEdgeFromParent` to express an unequal-depth merge (e.g. tools →
61
+ * call-llm, bypassing message-api). Visualization-only — runtime convergence
62
+ * is unchanged.
63
+ */
64
+ convergeAt?: string;
55
65
  }
56
66
  export interface FlowChartSpec {
57
67
  name: string;
@@ -77,14 +87,37 @@ export interface FlowChartSpec {
77
87
  /** True when this node is a back-edge reference created by loopTo() — not an executable stage. */
78
88
  isLoopReference?: boolean;
79
89
  }
80
- /** Metadata provided to the build-time extractor for each node. */
81
- export type BuildTimeNodeMetadata = FlowChartSpec;
82
- export type BuildTimeExtractor<TResult = FlowChartSpec> = (metadata: BuildTimeNodeMetadata) => TResult;
83
- export type { TraversalExtractor } from '../engine/types.js';
90
+ /**
91
+ * Options-bag argument shape for the `flowChart()` factory.
92
+ */
93
+ export interface FlowChartOptions {
94
+ /**
95
+ * Build-time recorders to attach BEFORE `start()` fires. Equivalent
96
+ * to chaining `.attachStructureRecorder(rec)` immediately after the
97
+ * factory returns — but registered EARLIER, so even the seed event
98
+ * fires through the dispatcher without needing the seed-replay path.
99
+ *
100
+ * Multiple recorders attach in array order (same as
101
+ * `.attachStructureRecorder` repeated). See `StructureRecorder` JSDoc
102
+ * for event semantics, ordering invariants, and the trust model.
103
+ */
104
+ structureRecorders?: StructureRecorder[];
105
+ /** Free-form description shown on the root spec node. */
106
+ description?: string;
107
+ /**
108
+ * Only meaningful for `flowChartSelector` (a root selector). When the
109
+ * selector picks ≥2 branches they fan out in parallel: `failFast: true`
110
+ * uses `Promise.all` (first branch error rejects + aborts), the default
111
+ * uses `Promise.allSettled` (best-effort — all branches run even if some
112
+ * fail). Use fail-fast when all selected branches are REQUIRED. Ignored for
113
+ * non-selector charts. Same flag `addSelectorFunction` / `addListOfFunction`
114
+ * expose.
115
+ */
116
+ failFast?: boolean;
117
+ }
84
118
  export type FlowChart<TOut = any, TScope = any> = {
85
119
  root: StageNode<TOut, TScope>;
86
120
  stageMap: Map<string, StageFunction<TOut, TScope>>;
87
- extractor?: TraversalExtractor;
88
121
  subflows?: Record<string, {
89
122
  root: StageNode<TOut, TScope>;
90
123
  }>;
@@ -6,8 +6,9 @@
6
6
  * a single extraction point that preserves structured details while still
7
7
  * producing a human-readable message.
8
8
  *
9
- * Consumers (narrative recorders, extractors, diagnostic collectors) receive
10
- * StructuredErrorInfo instead of a flat string, and can decide how to render it.
9
+ * Consumers (narrative recorders, FlowRecorder.onError handlers, diagnostic
10
+ * collectors) receive StructuredErrorInfo instead of a flat string, and can
11
+ * decide how to render it.
11
12
  * String-ification happens only at the final rendering boundary.
12
13
  */
13
14
  import type { ValidationIssue } from '../../schema/errors.js';
@@ -28,5 +28,5 @@ export declare class ChildrenExecutor<TOut = any, TScope = any> {
28
28
  * Execute selected children based on selector result.
29
29
  * Validates IDs, records selection info, then delegates to executeNodeChildren.
30
30
  */
31
- executeSelectedChildren(selector: Selector, children: StageNode<TOut, TScope>[], input: any, context: StageContext, branchPath: string, traversalContext?: TraversalContext): Promise<Record<string, NodeResultType>>;
31
+ executeSelectedChildren(selector: Selector, children: StageNode<TOut, TScope>[], input: any, context: StageContext, branchPath: string, traversalContext?: TraversalContext, failFast?: boolean): Promise<Record<string, NodeResultType>>;
32
32
  }
@@ -8,8 +8,8 @@ import type { StageContext } from '../../memory/StageContext.js';
8
8
  import type { StageNode } from '../graph/StageNode.js';
9
9
  import type { TraversalContext } from '../narrative/types.js';
10
10
  import type { HandlerDeps, StageFunction } from '../types.js';
11
- import type { CallExtractorFn, ExecuteNodeFn, GetStagePathFn, RunStageFn } from './types.js';
12
- export type { CallExtractorFn, ExecuteNodeFn, GetStagePathFn, RunStageFn };
11
+ import type { ExecuteNodeFn, RunStageFn } from './types.js';
12
+ export type { ExecuteNodeFn, RunStageFn };
13
13
  export declare class DeciderHandler<TOut = any, TScope = any> {
14
14
  private readonly deps;
15
15
  constructor(deps: HandlerDeps<TOut, TScope>);
@@ -20,5 +20,5 @@ export declare class DeciderHandler<TOut = any, TScope = any> {
20
20
  */
21
21
  handleScopeBased(node: StageNode<TOut, TScope>, stageFunc: StageFunction<TOut, TScope>, context: StageContext, breakFlag: {
22
22
  shouldBreak: boolean;
23
- }, branchPath: string | undefined, runStage: RunStageFn<TOut, TScope>, executeNode: ExecuteNodeFn<TOut, TScope>, callExtractor: CallExtractorFn<TOut, TScope>, getStagePath: GetStagePathFn<TOut, TScope>, traversalContext?: TraversalContext): Promise<any>;
23
+ }, branchPath: string | undefined, runStage: RunStageFn<TOut, TScope>, executeNode: ExecuteNodeFn<TOut, TScope>, traversalContext?: TraversalContext): Promise<any>;
24
24
  }
@@ -11,7 +11,7 @@ import type { StageNode } from '../graph/StageNode.js';
11
11
  import type { SerializedPipelineStructure } from '../types.js';
12
12
  /**
13
13
  * Compute the node type from node properties.
14
- * Shared by RuntimeStructureManager (serialization) and ExtractorRunner (metadata).
14
+ * Used by RuntimeStructureManager for serialization.
15
15
  */
16
16
  export declare function computeNodeType(node: StageNode): 'stage' | 'decider' | 'selector' | 'fork' | 'streaming' | 'subflow' | 'loop';
17
17
  export declare class RuntimeStructureManager {
@@ -11,7 +11,7 @@ import type { StageNode } from '../graph/StageNode.js';
11
11
  import type { TraversalContext } from '../narrative/types.js';
12
12
  import type { HandlerDeps, NodeResultType, StageFunction } from '../types.js';
13
13
  import type { ChildrenExecutor } from './ChildrenExecutor.js';
14
- import type { CallExtractorFn, ExecuteNodeFn, GetStagePathFn, RunStageFn } from './types.js';
14
+ import type { ExecuteNodeFn, RunStageFn } from './types.js';
15
15
  export declare class SelectorHandler<TOut = any, TScope = any> {
16
16
  private readonly deps;
17
17
  private readonly childrenExecutor;
@@ -23,5 +23,5 @@ export declare class SelectorHandler<TOut = any, TScope = any> {
23
23
  */
24
24
  handleScopeBased(node: StageNode<TOut, TScope>, stageFunc: StageFunction<TOut, TScope>, context: StageContext, breakFlag: {
25
25
  shouldBreak: boolean;
26
- }, branchPath: string | undefined, runStage: RunStageFn<TOut, TScope>, executeNode: ExecuteNodeFn<TOut, TScope>, callExtractor: CallExtractorFn<TOut, TScope>, getStagePath: GetStagePathFn<TOut, TScope>, traversalContext?: TraversalContext): Promise<Record<string, NodeResultType>>;
26
+ }, branchPath: string | undefined, runStage: RunStageFn<TOut, TScope>, executeNode: ExecuteNodeFn<TOut, TScope>, traversalContext?: TraversalContext): Promise<Record<string, NodeResultType>>;
27
27
  }
@@ -3,12 +3,11 @@
3
3
  */
4
4
  export { StageRunner } from './StageRunner.js';
5
5
  export { NodeResolver } from './NodeResolver.js';
6
- export type { CallExtractorFn, ExecuteNodeFn, GetStagePathFn, RunStageFn } from './types.js';
6
+ export type { ExecuteNodeFn, RunStageFn } from './types.js';
7
7
  export { ChildrenExecutor } from './ChildrenExecutor.js';
8
8
  export { DeciderHandler } from './DeciderHandler.js';
9
9
  export { SelectorHandler } from './SelectorHandler.js';
10
10
  export { ContinuationResolver, DEFAULT_MAX_ITERATIONS } from './ContinuationResolver.js';
11
11
  export { SubflowExecutor } from './SubflowExecutor.js';
12
12
  export { applyOutputMapping, createSubflowHandlerDeps, extractParentScopeValues, getInitialScopeValues, seedSubflowGlobalStore, } from './SubflowInputMapper.js';
13
- export { ExtractorRunner } from './ExtractorRunner.js';
14
13
  export { computeNodeType, RuntimeStructureManager } from './RuntimeStructureManager.js';
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * handlers/types.ts — Shared callback types for all handler modules.
3
3
  *
4
- * Avoids duplicate definitions of ExecuteNodeFn / CallExtractorFn / etc.
4
+ * Avoids duplicate definitions of ExecuteNodeFn / RunStageFn / etc.
5
5
  * across ChildrenExecutor, DeciderHandler, ContinuationResolver, SubflowExecutor.
6
6
  * All types are callbacks that break circular dependencies with FlowchartTraverser.
7
7
  */
@@ -28,12 +28,5 @@ export interface BreakFlag {
28
28
  }
29
29
  /** Recursive node execution — avoids circular dep with FlowchartTraverser. */
30
30
  export type ExecuteNodeFn<TOut = any, TScope = any> = (node: StageNode<TOut, TScope>, context: StageContext, breakFlag: BreakFlag, branchPath?: string) => Promise<any>;
31
- /** Run a stage function with commit + extractor. */
31
+ /** Run a stage function with commit. */
32
32
  export type RunStageFn<TOut = any, TScope = any> = (node: StageNode<TOut, TScope>, stageFunc: StageFunction<TOut, TScope>, context: StageContext, breakFn: () => void) => Promise<TOut>;
33
- /** Call the traversal extractor after stage execution. */
34
- export type CallExtractorFn<TOut = any, TScope = any> = (node: StageNode<TOut, TScope>, context: StageContext, stagePath: string, stageOutput?: unknown, errorInfo?: {
35
- type: string;
36
- message: string;
37
- }) => void;
38
- /** Compute the stage path string for extractor and narrative. */
39
- export type GetStagePathFn<TOut = any, TScope = any> = (node: StageNode<TOut, TScope>, branchPath?: string, contextStageName?: string) => string;
@@ -14,7 +14,7 @@ export { NullControlFlowNarrativeGenerator } from './narrative/NullControlFlowNa
14
14
  export type { IControlFlowNarrative } from './narrative/types.js';
15
15
  export { FlowRecorderDispatcher } from './narrative/FlowRecorderDispatcher.js';
16
16
  export { NarrativeFlowRecorder } from './narrative/NarrativeFlowRecorder.js';
17
- export type { FlowBreakEvent, FlowDecisionEvent, FlowErrorEvent, FlowForkEvent, FlowLoopEvent, FlowNextEvent, FlowRecorder, FlowSelectedEvent, FlowStageEvent, FlowSubflowEvent, FlowSubflowRegisteredEvent, TraversalContext, } from './narrative/types.js';
17
+ export type { FlowBreakEvent, FlowDecisionEvent, FlowErrorEvent, FlowForkEvent, FlowLoopEvent, FlowNextEvent, FlowRecorder, FlowRunEvent, FlowRunFailedEvent, FlowSelectedEvent, FlowStageEvent, FlowSubflowEvent, FlowSubflowRegisteredEvent, TraversalContext, } from './narrative/types.js';
18
18
  export type { StructuredErrorInfo } from './errors/errorInfo.js';
19
19
  export { extractErrorInfo, formatErrorInfo } from './errors/errorInfo.js';
20
20
  export { AdaptiveNarrativeFlowRecorder } from './narrative/recorders/AdaptiveNarrativeFlowRecorder.js';
@@ -10,7 +10,8 @@
10
10
  * When no recorders are attached, every method is a fast no-op (empty array check).
11
11
  */
12
12
  import type { DecisionEvidence, SelectionEvidence } from '../../decide/types.js';
13
- import type { FlowRecorder, IControlFlowNarrative, TraversalContext } from './types.js';
13
+ import type { StructuredErrorInfo } from '../errors/errorInfo.js';
14
+ import type { FlowRecorder, IControlFlowNarrative, StageType, TraversalContext } from './types.js';
14
15
  export declare class FlowRecorderDispatcher implements IControlFlowNarrative {
15
16
  private recorders;
16
17
  /** Attach a FlowRecorder. Duplicate IDs are allowed (same as scope ScopeRecorder). */
@@ -21,7 +22,7 @@ export declare class FlowRecorderDispatcher implements IControlFlowNarrative {
21
22
  getScopeRecorders(): FlowRecorder[];
22
23
  /** Find a recorder by ID. Useful for retrieving built-in recorders like NarrativeFlowRecorder. */
23
24
  getRecorderById<T extends FlowRecorder = FlowRecorder>(id: string): T | undefined;
24
- onStageExecuted(stageName: string, description?: string, traversalContext?: TraversalContext): void;
25
+ onStageExecuted(stageName: string, description: string | undefined, traversalContext: TraversalContext | undefined, stageType: StageType): void;
25
26
  onNext(fromStage: string, toStage: string, description?: string, traversalContext?: TraversalContext): void;
26
27
  onDecision(deciderName: string, chosenBranch: string, rationale?: string, deciderDescription?: string, traversalContext?: TraversalContext, evidence?: DecisionEvidence): void;
27
28
  onFork(parentStage: string, childNames: string[], traversalContext?: TraversalContext): void;
@@ -36,6 +37,7 @@ export declare class FlowRecorderDispatcher implements IControlFlowNarrative {
36
37
  onResume(stageName: string, stageId: string, hasInput: boolean, traversalContext?: TraversalContext): void;
37
38
  onRunStart(input: unknown, traversalContext?: TraversalContext): void;
38
39
  onRunEnd(output: unknown, traversalContext?: TraversalContext): void;
40
+ onRunFailed(error: StructuredErrorInfo, traversalContext?: TraversalContext): void;
39
41
  /**
40
42
  * Returns sentences from an attached NarrativeFlowRecorder (looked up by ID).
41
43
  * Callers that need sentences should attach a NarrativeFlowRecorder with id 'narrative'
@@ -22,5 +22,6 @@ export declare class NullControlFlowNarrativeGenerator implements IControlFlowNa
22
22
  onResume(): void;
23
23
  onRunStart(): void;
24
24
  onRunEnd(): void;
25
+ onRunFailed(): void;
25
26
  getSentences(): string[];
26
27
  }