footprintjs 9.3.0 → 9.5.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 (61) hide show
  1. package/AGENTS.md +2 -1
  2. package/CLAUDE.md +2 -1
  3. package/dist/advanced.js +1 -1
  4. package/dist/esm/advanced.js +1 -1
  5. package/dist/esm/index.js +1 -1
  6. package/dist/esm/lib/builder/FlowChartBuilder.js +15 -2
  7. package/dist/esm/lib/capture/envelope.js +187 -0
  8. package/dist/esm/lib/capture/index.js +16 -0
  9. package/dist/esm/lib/capture/policies.js +29 -0
  10. package/dist/esm/lib/capture/summarize.js +58 -0
  11. package/dist/esm/lib/engine/handlers/ContinuationResolver.js +23 -4
  12. package/dist/esm/lib/engine/handlers/SubflowExecutor.js +9 -1
  13. package/dist/esm/lib/engine/types.js +1 -1
  14. package/dist/esm/lib/memory/StageContext.js +62 -44
  15. package/dist/esm/lib/memory/index.js +2 -2
  16. package/dist/esm/lib/memory/types.js +7 -4
  17. package/dist/esm/lib/observer-queue/deferredDispatcher.js +226 -0
  18. package/dist/esm/lib/observer-queue/flushDriver.js +163 -0
  19. package/dist/esm/lib/observer-queue/index.js +22 -0
  20. package/dist/esm/lib/observer-queue/mergedQueue.js +91 -0
  21. package/dist/esm/lib/observer-queue/ring.js +122 -0
  22. package/dist/esm/lib/runner/ExecutionRuntime.js +13 -1
  23. package/dist/esm/lib/runner/FlowChartExecutor.js +20 -1
  24. package/dist/index.js +1 -1
  25. package/dist/lib/builder/FlowChartBuilder.js +15 -2
  26. package/dist/lib/capture/envelope.js +192 -0
  27. package/dist/lib/capture/index.js +23 -0
  28. package/dist/lib/capture/policies.js +30 -0
  29. package/dist/lib/capture/summarize.js +63 -0
  30. package/dist/lib/engine/handlers/ContinuationResolver.js +23 -4
  31. package/dist/lib/engine/handlers/SubflowExecutor.js +9 -1
  32. package/dist/lib/engine/types.js +1 -1
  33. package/dist/lib/memory/StageContext.js +63 -45
  34. package/dist/lib/memory/index.js +3 -2
  35. package/dist/lib/memory/types.js +10 -5
  36. package/dist/lib/observer-queue/deferredDispatcher.js +230 -0
  37. package/dist/lib/observer-queue/flushDriver.js +167 -0
  38. package/dist/lib/observer-queue/index.js +36 -0
  39. package/dist/lib/observer-queue/mergedQueue.js +95 -0
  40. package/dist/lib/observer-queue/ring.js +126 -0
  41. package/dist/lib/runner/ExecutionRuntime.js +13 -1
  42. package/dist/lib/runner/FlowChartExecutor.js +20 -1
  43. package/dist/types/advanced.d.ts +1 -1
  44. package/dist/types/index.d.ts +13 -0
  45. package/dist/types/lib/capture/envelope.d.ts +169 -0
  46. package/dist/types/lib/capture/index.d.ts +16 -0
  47. package/dist/types/lib/capture/policies.d.ts +42 -0
  48. package/dist/types/lib/capture/summarize.d.ts +65 -0
  49. package/dist/types/lib/engine/handlers/ContinuationResolver.d.ts +15 -2
  50. package/dist/types/lib/engine/types.d.ts +3 -0
  51. package/dist/types/lib/memory/StageContext.d.ts +38 -1
  52. package/dist/types/lib/memory/index.d.ts +2 -2
  53. package/dist/types/lib/memory/types.d.ts +33 -22
  54. package/dist/types/lib/observer-queue/deferredDispatcher.d.ts +169 -0
  55. package/dist/types/lib/observer-queue/flushDriver.d.ts +124 -0
  56. package/dist/types/lib/observer-queue/index.d.ts +25 -0
  57. package/dist/types/lib/observer-queue/mergedQueue.d.ts +85 -0
  58. package/dist/types/lib/observer-queue/ring.d.ts +99 -0
  59. package/dist/types/lib/runner/ExecutionRuntime.d.ts +11 -1
  60. package/dist/types/lib/runner/FlowChartExecutor.d.ts +43 -1
  61. package/package.json +1 -1
@@ -19,7 +19,7 @@
19
19
  *
20
20
  * Import via: import { ... } from 'footprint/advanced'
21
21
  */
22
- export type { CommitBundle, FlowControlType, FlowMessage, MemoryPatch, ReadSummaryMarker, ReadTrackingMode, StageSnapshot, TraceEntry, } from './lib/memory/index.js';
22
+ export type { CommitBundle, FlowControlType, FlowMessage, MemoryPatch, ReadSummaryMarker, ReadTrackingMode, RetentionPolicy, StageSnapshot, TraceEntry, WriteSummaryMarker, WriteTrackingMode, } from './lib/memory/index.js';
23
23
  export { SharedMemory } from './lib/memory/index.js';
24
24
  export { StageContext } from './lib/memory/index.js';
25
25
  export { EventLog } from './lib/memory/index.js';
@@ -123,6 +123,19 @@ export type { ScopeFactory } from './lib/engine/index.js';
123
123
  * or call `executor.setReadTracking(mode)` before `run()`.
124
124
  */
125
125
  export type { ReadSummaryMarker, ReadTrackingMode } from './lib/memory/index.js';
126
+ /**
127
+ * @category Configuration
128
+ *
129
+ * Write-tracking policy for `StageSnapshot.stageWrites` (#13c-A) — the
130
+ * sibling of `ReadTrackingMode`; both alias the shared `RetentionPolicy`
131
+ * family from `lib/capture`. `'full'` (default — per-write value clone,
132
+ * historical behavior) / `'summary'` (cheap `WriteSummaryMarker` per write)
133
+ * / `'off'` (no tracking; `stageWrites` absent and the `onCommit` mutations
134
+ * payload is empty — writes themselves still commit, and the commit log is
135
+ * unaffected). Pass as `new FlowChartExecutor(chart, { writeTracking })` or
136
+ * call `executor.setWriteTracking(mode)` before `run()`.
137
+ */
138
+ export type { RetentionPolicy, WriteSummaryMarker, WriteTrackingMode } from './lib/memory/index.js';
126
139
  /** @category Contract & Validation */
127
140
  export type { SchemaKind, ValidationIssue, ValidationResult } from './lib/schema/index.js';
128
141
  /** @category Contract & Validation */
@@ -0,0 +1,169 @@
1
+ /**
2
+ * capture/envelope.ts — RFC-001 Block 1: capture envelopes + payload summarizer.
3
+ *
4
+ * Pattern: Point-in-time capture. An observer event is snapshotted into a
5
+ * self-contained, immutable envelope at the moment it happens, so
6
+ * DELIVERY can be deferred (RFC-001 "one beat behind") without the
7
+ * payload drifting under later engine mutations.
8
+ * Role: The capture tier of the deferred-observer pipeline
9
+ * (`src/lib/observer-queue/`). Pure module — ZERO engine imports
10
+ * (only `capture/` internals); the engine wiring is RFC-001
11
+ * Blocks 6–10.
12
+ *
13
+ * Capture policies (RFC-001 §5):
14
+ * - `'summary'` — bounded, reference-free summarization via
15
+ * {@link summarizePayload}. Built on the SAME classification path as the
16
+ * retention markers in `summarize.ts` (#14 / #13c-A), extended with
17
+ * bounded structural descent. Structured-clone-safe by construction.
18
+ * - `'clone'` — `structuredClone` at capture time (the capture-tier
19
+ * spelling of retention `'full'` — see the mapping notes in
20
+ * `policies.ts`). If the payload is not clonable (functions, symbols,
21
+ * live handles), capture DEGRADES to `'summary'` and reports through the
22
+ * `warn` hook — a capture must never throw into the producer.
23
+ * - `'ref'` — pass-through of the live reference. The CALLER asserts
24
+ * immutability for the delivery window (safe e.g. for committed-state
25
+ * reads, proven immutable-after-swap in #13/#13b). Exempt from the
26
+ * clone-safety guarantee — see the dev-warn seam below.
27
+ *
28
+ * Dev-warn seam (resolves the isDevMode-would-be-an-engine-import problem):
29
+ * This module must not import `scope/detectCircular` (engine territory).
30
+ * Instead, `capture()` accepts {@link CaptureHooks} with an optional
31
+ * `warn` callback and invokes it on every `'ref'` capture and every
32
+ * `'clone'` degradation. The WIRING layer (Block 6) binds `warn` to an
33
+ * `isDevMode()`-gated, deduplicated console warner; the pure module stays
34
+ * engine-free and silent by default (no hooks ⇒ no warning, zero cost).
35
+ *
36
+ * Summarizer bounds (documented contract, exported as constants):
37
+ * - depth ≤ {@link PAYLOAD_SUMMARY_MAX_DEPTH} (3) — deeper structure
38
+ * collapses to a classified leaf with `depthClipped: true`.
39
+ * - breadth ≤ {@link PAYLOAD_SUMMARY_MAX_ENTRIES} (16) per object/array —
40
+ * the remainder is dropped and flagged `truncated: true` (the honest
41
+ * `size` still reports the real count).
42
+ * - total ≤ {@link PAYLOAD_SUMMARY_MAX_NODES} (128) summary nodes per
43
+ * payload — a global budget so wide×deep payloads stay O(1)-bounded.
44
+ * - string previews ≤ `SUMMARY_PREVIEW_LENGTH` (80) chars.
45
+ * Cycles are detected (ancestor set) and flagged `circular: true`;
46
+ * throwing getters yield a `'unreadable'` leaf; symbol-keyed properties
47
+ * are ignored (same as JSON / `Object.keys`); `Map`/`Set` are leaves with
48
+ * their REAL entry count (no descent — matches `summarize.ts`).
49
+ */
50
+ import { type SummaryValueType } from './summarize.js';
51
+ /** Which observer channel produced a captured event (RFC-001 §5). */
52
+ export type CaptureChannel = 'scope' | 'flow' | 'emit';
53
+ /**
54
+ * How an event payload is materialized into the envelope (RFC-001 §5).
55
+ * See the module header for the full per-policy contract.
56
+ */
57
+ export type CapturePolicy = 'summary' | 'clone' | 'ref';
58
+ /**
59
+ * A captured observer event — self-contained and immutable (shallow-frozen
60
+ * at creation). `seq` is the arrival stamp assigned at capture under the
61
+ * single JS thread: it totally orders events ACROSS all three channels, is
62
+ * monotonic, and is gap-detectable (a dropped event leaves a visible hole
63
+ * in the delivered `seq` sequence — honest loss accounting).
64
+ */
65
+ export interface CaptureEnvelope {
66
+ /** Arrival stamp — total order across channels; gaps reveal drops. */
67
+ readonly seq: number;
68
+ /** Producing observer channel. */
69
+ readonly channel: CaptureChannel;
70
+ /** Producing hook name — `'onWrite'`, `'onStageExecuted'`, `'onEmit'`, ... */
71
+ readonly method: string;
72
+ /** The execution step that produced the event (`stageId#executionIndex`). */
73
+ readonly runtimeStageId: string;
74
+ /** The run that produced the event (Convention 4 per-run scoping). */
75
+ readonly runId: string;
76
+ /**
77
+ * Per {@link CapturePolicy} — NEVER a live engine reference under
78
+ * `'summary'` / `'clone'`; under `'ref'` the caller asserted immutability.
79
+ */
80
+ readonly payload: unknown;
81
+ /** Capture wall-clock (ms epoch by default; injectable via hooks.now). */
82
+ readonly capturedAt: number;
83
+ }
84
+ /** The raw event description handed to {@link capture}. */
85
+ export interface CaptureRequest {
86
+ /** Arrival stamp — assigned by the merged queue's counter (Block 3). */
87
+ readonly seq: number;
88
+ readonly channel: CaptureChannel;
89
+ readonly method: string;
90
+ readonly runtimeStageId: string;
91
+ readonly runId: string;
92
+ /** The LIVE payload — `capture()` materializes it per policy. */
93
+ readonly payload: unknown;
94
+ }
95
+ /**
96
+ * Engine-free seams injected by the wiring layer (Block 6). The pure module
97
+ * never imports dev-mode or clock infrastructure.
98
+ */
99
+ export interface CaptureHooks {
100
+ /**
101
+ * Diagnostic sink — invoked on every `'ref'` capture (caller-asserted
102
+ * immutability) and every `'clone'` → `'summary'` degradation. Block 6
103
+ * binds this to an `isDevMode()`-gated, deduplicated warner.
104
+ */
105
+ readonly warn?: (message: string) => void;
106
+ /** Clock for `capturedAt` — defaults to `Date.now`. Injectable for tests. */
107
+ readonly now?: () => number;
108
+ }
109
+ /** Max nesting depth a payload summary descends before clipping. */
110
+ export declare const PAYLOAD_SUMMARY_MAX_DEPTH = 3;
111
+ /** Max entries summarized per object/array level before truncating. */
112
+ export declare const PAYLOAD_SUMMARY_MAX_ENTRIES = 16;
113
+ /** Global per-payload budget of summary nodes (wide×deep hard bound). */
114
+ export declare const PAYLOAD_SUMMARY_MAX_NODES = 128;
115
+ /**
116
+ * Leaf classification for a summary node — the `summarize.ts` family plus
117
+ * `'unreadable'` for properties whose getter threw during capture.
118
+ */
119
+ export type PayloadSummaryType = SummaryValueType | 'unreadable';
120
+ /**
121
+ * One node of a payload summary tree. Every field is a primitive, a plain
122
+ * object, or a plain array — structured-clone-safe by construction.
123
+ */
124
+ export interface PayloadSummaryNode {
125
+ /** Classification — same rules as the retention markers (one code path). */
126
+ readonly type: PayloadSummaryType;
127
+ /** Honest size proxy: string length, array length, or key/entry count. */
128
+ readonly size?: number;
129
+ /** First `SUMMARY_PREVIEW_LENGTH` chars — primitives/strings only. */
130
+ readonly preview?: string;
131
+ /** Summarized own enumerable string-keyed properties (objects). */
132
+ readonly fields?: Readonly<Record<string, PayloadSummaryNode>>;
133
+ /** Summarized leading items (arrays). */
134
+ readonly items?: readonly PayloadSummaryNode[];
135
+ /** Entries were omitted here (breadth cap or node budget). */
136
+ readonly truncated?: boolean;
137
+ /** This value is an ancestor of itself — descent stopped. */
138
+ readonly circular?: true;
139
+ /** {@link PAYLOAD_SUMMARY_MAX_DEPTH} reached — children not descended. */
140
+ readonly depthClipped?: true;
141
+ }
142
+ /**
143
+ * Root of a payload summary — branded so consumers (and tests) can detect
144
+ * that a payload was summarized rather than cloned. Sibling of the
145
+ * `__readSummary` / `__writeSummary` retention markers.
146
+ */
147
+ export interface PayloadSummary extends PayloadSummaryNode {
148
+ /** Discriminant — `'summary'`-policy envelope payloads carry this. */
149
+ readonly __payloadSummary: true;
150
+ }
151
+ /**
152
+ * Produce a bounded, reference-free, structured-clone-safe summary of an
153
+ * arbitrary payload. Never throws; never holds a reference into the source
154
+ * value (every node is a fresh object whose fields are primitives). Bounds
155
+ * are documented in the module header.
156
+ */
157
+ export declare function summarizePayload(payload: unknown): PayloadSummary;
158
+ /**
159
+ * Capture one observer event into an immutable {@link CaptureEnvelope}.
160
+ *
161
+ * Guarantees:
162
+ * - Never throws into the producer (`'clone'` degradation, summarizer
163
+ * never-throws contract).
164
+ * - The returned envelope is shallow-frozen — `seq`/`channel`/... cannot
165
+ * be reassigned. Under `'summary'`/`'clone'` the payload holds no live
166
+ * reference into the source; under `'ref'` it intentionally does.
167
+ * - Default policy is `'summary'` (cheapest safe tier).
168
+ */
169
+ export declare function capture(request: CaptureRequest, policy?: CapturePolicy, hooks?: CaptureHooks): CaptureEnvelope;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * capture/ — Shared value-capture / retention primitives (zero deps, leaf
3
+ * module — `memory/` imports it, never the reverse).
4
+ *
5
+ * One home for "what do we keep about a tracked operation's value?":
6
+ * - `RetentionPolicy` — the `'full' | 'summary' | 'off'` family behind the
7
+ * #14 `readTracking` and #13c-A `writeTracking` dials.
8
+ * - `summarizeReadValue` / `summarizeWriteValue` — the parameterized
9
+ * summary-marker builders sharing one classification path.
10
+ *
11
+ * RFC-001 (deferred observer delivery) builds its capture tier on this
12
+ * module — see the mapping notes in `policies.ts`.
13
+ */
14
+ export type { RetentionPolicy } from './policies.js';
15
+ export type { ReadSummaryMarker, SummaryValueType, WriteSummaryMarker } from './summarize.js';
16
+ export { READ_PREVIEW_LENGTH, SUMMARY_PREVIEW_LENGTH, summarizeReadValue, summarizeWriteValue } from './summarize.js';
@@ -0,0 +1,42 @@
1
+ /**
2
+ * policies.ts — The retention policy family shared by every snapshot-tracking
3
+ * dial (#14 `readTracking`, #13c-A `writeTracking`).
4
+ *
5
+ * RETENTION answers "what does the engine KEEP in its own snapshot state
6
+ * (`StageSnapshot.stageReads` / `stageWrites`, and therefore the commit
7
+ * observer's mutations payload) after the moment of the operation?" It is
8
+ * distinct from DELIVERY — what an observer receives at event time
9
+ * (`ScopeRecorder.onRead`/`onWrite` always deliver the live value, in every
10
+ * retention mode).
11
+ *
12
+ * ── RFC-001 (deferred observer delivery) mapping ──────────────────────────
13
+ * RFC-001's capture tier uses the vocabulary `'clone' | 'summary' | 'ref'`.
14
+ * When its Block 1 lands it builds on THIS module:
15
+ *
16
+ * - RFC capture `'clone'` ≈ retention `'full'` (alias at the module
17
+ * boundary — same semantics: structuredClone at capture time).
18
+ * - RFC capture `'summary'` ≈ retention `'summary'` (same marker shapes —
19
+ * see `summarize.ts`).
20
+ * - RFC capture `'ref'` is DELIVERY-tier only and is NOT implemented here
21
+ * — reserved. Retention must never hold live references into engine
22
+ * state: retained entries outlive the stage (the execution tree keeps
23
+ * them for the whole run), while a `'ref'` is only safe within the
24
+ * immutability window of the captured value. Holding one in retention
25
+ * would either pin state generations (the #18 leak) or expose
26
+ * later-mutated values as if they were point-in-time captures.
27
+ */
28
+ /**
29
+ * How a tracked operation's value is retained in the per-stage snapshot view.
30
+ *
31
+ * - `'full'` — `structuredClone` the value at the moment of the operation
32
+ * (the historical default for both dials; point-in-time, detached copy).
33
+ * - `'summary'` — retain a cheap marker (type + size proxy + short preview)
34
+ * instead of the value. O(1)-ish per operation; no value clone.
35
+ * - `'off'` — retain nothing. The operation itself is unaffected (reads
36
+ * still return values, writes still commit); only the snapshot bookkeeping
37
+ * is skipped.
38
+ *
39
+ * `ReadTrackingMode` (#14) and `WriteTrackingMode` (#13c-A) are public
40
+ * aliases of this type — see `memory/types.ts`.
41
+ */
42
+ export type RetentionPolicy = 'full' | 'summary' | 'off';
@@ -0,0 +1,65 @@
1
+ /**
2
+ * summarize.ts — Cheap value summarization for `'summary'` retention.
3
+ *
4
+ * Extracted from `StageContext` (#14's `summarizeReadValue`) into a shared,
5
+ * brand-parameterized helper so both snapshot-tracking dials (#14
6
+ * `readTracking`, #13c-A `writeTracking`) — and later RFC-001's deferred
7
+ * observer capture tier — produce identical summaries from ONE code path.
8
+ *
9
+ * Deliberately avoids every O(value) operation: no clone, no serialization.
10
+ * See {@link ReadSummaryMarker} for the honest-cost contract.
11
+ */
12
+ /** Max characters captured in a summary marker's `preview`. */
13
+ export declare const SUMMARY_PREVIEW_LENGTH = 80;
14
+ /**
15
+ * Compat alias for {@link SUMMARY_PREVIEW_LENGTH} — the name shipped with
16
+ * #14, kept so existing import paths (`memory/types`, the memory barrel)
17
+ * stay valid. Same constant; reads and writes share one preview cap.
18
+ */
19
+ export declare const READ_PREVIEW_LENGTH = 80;
20
+ /** `typeof` result, refined to 'array' / 'null' for objects. */
21
+ export type SummaryValueType = 'string' | 'number' | 'boolean' | 'bigint' | 'symbol' | 'function' | 'object' | 'array' | 'null';
22
+ /** Brand-free summary fields shared by both marker shapes. */
23
+ interface ValueSummary {
24
+ /** `typeof` result, refined to 'array' / 'null' for objects. */
25
+ type: SummaryValueType;
26
+ /** Size proxy: string length, array length, or object key count. */
27
+ size?: number;
28
+ /** First {@link SUMMARY_PREVIEW_LENGTH} chars — primitives and strings only. */
29
+ preview?: string;
30
+ }
31
+ /**
32
+ * Marker recorded in `StageSnapshot.stageReads` under `readTracking: 'summary'`.
33
+ *
34
+ * Honest cost note: `size` is a cheap proxy (string length / array length /
35
+ * object key count), NOT a serialized byte count — computing real byte size
36
+ * would require an O(value) serialization, which is exactly the cost the
37
+ * summary mode removes. `preview` is only produced for primitives and strings
38
+ * (first {@link SUMMARY_PREVIEW_LENGTH} characters); objects and arrays carry
39
+ * no preview for the same reason.
40
+ */
41
+ export interface ReadSummaryMarker extends ValueSummary {
42
+ /** Discriminant — lets snapshot consumers detect marker entries. */
43
+ __readSummary: true;
44
+ }
45
+ /**
46
+ * Marker recorded in `StageSnapshot.stageWrites` (and the commit observer's
47
+ * mutations payload) under `writeTracking: 'summary'` (#13c-A). Same fields
48
+ * and cost contract as {@link ReadSummaryMarker}; distinct brand so consumers
49
+ * can tell which dial produced an entry.
50
+ */
51
+ export interface WriteSummaryMarker extends ValueSummary {
52
+ /** Discriminant — lets snapshot consumers detect marker entries. */
53
+ __writeSummary: true;
54
+ }
55
+ /**
56
+ * Summarize a tracked READ for `readTracking: 'summary'` (#14). Byte-identical
57
+ * output to the pre-extraction `StageContext`-local implementation.
58
+ */
59
+ export declare function summarizeReadValue(value: unknown): ReadSummaryMarker;
60
+ /**
61
+ * Summarize a tracked WRITE for `writeTracking: 'summary'` (#13c-A). Sibling
62
+ * of {@link summarizeReadValue} — same classification, distinct brand.
63
+ */
64
+ export declare function summarizeWriteValue(value: unknown): WriteSummaryMarker;
65
+ export {};
@@ -43,6 +43,18 @@ export declare class ContinuationResolver<TOut = any, TScope = any> {
43
43
  * Key: node.id, Value: visit count (0 = first visit).
44
44
  */
45
45
  private iterationCounters;
46
+ /**
47
+ * Total fn-bearing dynamic-next hops this traverser has followed.
48
+ *
49
+ * Fresh fn-bearing nodes bypass the per-node-id iteration counter (they
50
+ * are new nodes, often without stable ids — there is no back-edge to
51
+ * count). Without a bound, a stage that keeps returning a function-bearing
52
+ * dynamic `next` runs FOREVER on the flat trampoline (no stack overflow
53
+ * brakes it either). This run-total counter puts such chains under the
54
+ * same `maxIterations` budget (default 1000, tuned via
55
+ * `RunOptions.maxIterations`) that bounds loop edges.
56
+ */
57
+ private dynamicNextHops;
46
58
  private readonly onIterationUpdate?;
47
59
  private readonly maxIterations;
48
60
  constructor(deps: HandlerDeps<TOut, TScope>, nodeResolver: NodeResolver<TOut, TScope>, onIterationUpdate?: (nodeId: string, count: number) => void, maxIterations?: number);
@@ -62,8 +74,9 @@ export declare class ContinuationResolver<TOut = any, TScope = any> {
62
74
  * `onLoop` narrative), in the same order.
63
75
  *
64
76
  * Three dynamicNext patterns:
65
- * - StageNode with fn → truly dynamic node, returned as-is (no iteration
66
- * tracking — it is a fresh node, not a back-edge).
77
+ * - StageNode with fn → truly dynamic node, returned as-is (no per-node
78
+ * iteration tracking — it is a fresh node, not a back-edge — but the
79
+ * run-total dynamic-hop budget applies; see `dynamicNextHops`).
67
80
  * - String ID → reference to an existing node, resolved via NodeResolver.
68
81
  * - StageNode without fn → reference by ID, resolved via NodeResolver.
69
82
  */
@@ -337,6 +337,9 @@ export interface RunOptions {
337
337
  * This is the binding constraint for loop-heavy pipelines — raise it for
338
338
  * legitimately long loops (`loopTo` chains run with a flat stack, so high
339
339
  * values are safe; memory for state/narrative still grows per iteration).
340
+ * Also bounds the run-total chain of function-bearing dynamic `next`
341
+ * continuations (fresh nodes returned from stages) — a runaway dynamic
342
+ * chain errors instead of running forever.
340
343
  * Propagates to subflows. Must be >= 1.
341
344
  */
342
345
  maxIterations?: number;
@@ -11,7 +11,7 @@ import { DiagnosticCollector } from './DiagnosticCollector.js';
11
11
  import { EventLog } from './EventLog.js';
12
12
  import { SharedMemory } from './SharedMemory.js';
13
13
  import { TransactionBuffer } from './TransactionBuffer.js';
14
- import type { FlowControlType, ReadTrackingMode, StageSnapshot } from './types.js';
14
+ import type { FlowControlType, ReadTrackingMode, StageSnapshot, WriteTrackingMode } from './types.js';
15
15
  export declare class StageContext {
16
16
  private sharedMemory;
17
17
  /**
@@ -68,6 +68,19 @@ export declare class StageContext {
68
68
  * every mode.
69
69
  */
70
70
  private readTracking;
71
+ /**
72
+ * How tracked writes are recorded into `_stageWrites` (#13c-A) — the
73
+ * sibling of {@link readTracking}, with the same propagation pattern
74
+ * (inherited via {@link createNext}/{@link createChild}, pushed into
75
+ * subflow root contexts by `SubflowExecutor`). Governs the per-write
76
+ * `structuredClone` in {@link setObject}/{@link updateObject}. Affects the
77
+ * snapshot's `stageWrites` payload AND the commit observer's mutations
78
+ * payload (which is a spread of `_stageWrites`) — but NOT the write
79
+ * itself: the transaction buffer, the commit log, and shared state are
80
+ * identical in every mode, and `ScopeRecorder.onWrite` always fires with
81
+ * the live value.
82
+ */
83
+ private writeTracking;
71
84
  /** Observer called after commit() — used by ScopeFacade to fire ScopeRecorder.onCommit. */
72
85
  private _commitObserver?;
73
86
  constructor(runId: string, name: string, stageId: string, sharedMemory: SharedMemory, branchId?: string, eventLog?: EventLog, isDecider?: boolean);
@@ -94,6 +107,30 @@ export declare class StageContext {
94
107
  useReadTracking(mode: ReadTrackingMode): void;
95
108
  /** Returns the active read-tracking policy (used for subflow propagation). */
96
109
  getReadTracking(): ReadTrackingMode;
110
+ /**
111
+ * Set the write-tracking policy for this context (#13c-A). Same plumbing
112
+ * as {@link useReadTracking}: called at the root by
113
+ * `ExecutionRuntime.useWriteTracking()` (plumbed from `FlowChartExecutor`);
114
+ * descendants inherit via `createNext`/`createChild`, and `SubflowExecutor`
115
+ * pushes the parent context's mode into each subflow root.
116
+ */
117
+ useWriteTracking(mode: WriteTrackingMode): void;
118
+ /** Returns the active write-tracking policy (used for subflow propagation). */
119
+ getWriteTracking(): WriteTrackingMode;
120
+ /**
121
+ * Record a tracked user-level write into `_stageWrites`, policy-gated
122
+ * (#13c-A) — the single bookkeeping path for {@link setObject} and
123
+ * {@link updateObject}.
124
+ *
125
+ * Redaction takes precedence over the dial in EVERY mode: a redacted
126
+ * write stores the `'[REDACTED]'` placeholder under `'full'` AND
127
+ * `'summary'` (a summary marker would leak the value's preview/size),
128
+ * and stores nothing under `'off'` (entry skipped entirely — nothing to
129
+ * leak). The staged write itself is unaffected — redaction of the
130
+ * committed payload is handled by the transaction buffer's
131
+ * `redactedPaths`.
132
+ */
133
+ private trackWrite;
97
134
  /**
98
135
  * ── The first-touch state view (#13) ────────────────────────────────────
99
136
  *
@@ -9,6 +9,6 @@ export { EventLog } from './EventLog.js';
9
9
  export { SharedMemory } from './SharedMemory.js';
10
10
  export { StageContext } from './StageContext.js';
11
11
  export { TransactionBuffer } from './TransactionBuffer.js';
12
- export type { CommitBundle, FlowControlType, FlowMessage, MemoryPatch, ReadSummaryMarker, ReadTrackingMode, ScopeFactory, StageSnapshot, TraceEntry, } from './types.js';
13
- export { READ_PREVIEW_LENGTH } from './types.js';
12
+ export type { CommitBundle, FlowControlType, FlowMessage, MemoryPatch, ReadSummaryMarker, ReadTrackingMode, RetentionPolicy, ScopeFactory, StageSnapshot, TraceEntry, WriteSummaryMarker, WriteTrackingMode, } from './types.js';
13
+ export { READ_PREVIEW_LENGTH, SUMMARY_PREVIEW_LENGTH } from './types.js';
14
14
  export { applySmartMerge, deepSmartMerge, DELIM, getNestedValue, getRunAndGlobalPaths, normalisePath, redactPatch, setNestedValue, updateNestedValue, updateValue, } from './utils.js';
@@ -1,8 +1,13 @@
1
1
  /**
2
2
  * types.ts — Core type definitions for the memory library
3
3
  *
4
- * Zero dependencies on old code or other libraries in this package.
4
+ * Zero dependencies on old code or other libraries in this package, with one
5
+ * deliberate exception: `capture/` — the standalone leaf module holding the
6
+ * shared retention-policy family and summary-marker builders (extracted from
7
+ * here in #13c-A so the read and write dials, and later RFC-001, share one
8
+ * implementation).
5
9
  */
10
+ import type { RetentionPolicy } from '../capture/policies.js';
6
11
  /** A flat key-value bag representing a state patch (overwrite or merge). */
7
12
  export interface MemoryPatch {
8
13
  [key: string]: any;
@@ -45,6 +50,9 @@ export interface FlowMessage {
45
50
  iteration?: number;
46
51
  timestamp?: number;
47
52
  }
53
+ export type { RetentionPolicy } from '../capture/policies.js';
54
+ export type { ReadSummaryMarker, WriteSummaryMarker } from '../capture/summarize.js';
55
+ export { READ_PREVIEW_LENGTH, SUMMARY_PREVIEW_LENGTH } from '../capture/summarize.js';
48
56
  /**
49
57
  * Policy for how tracked reads are recorded into `StageSnapshot.stageReads`.
50
58
  *
@@ -62,30 +70,30 @@ export interface FlowMessage {
62
70
  *
63
71
  * Set via `new FlowChartExecutor(chart, { readTracking })` or
64
72
  * `executor.setReadTracking(mode)` (before `run()`).
73
+ *
74
+ * Alias of the shared {@link RetentionPolicy} family (#13c-A) — kept as the
75
+ * shipped public name for the read dial.
65
76
  */
66
- export type ReadTrackingMode = 'full' | 'summary' | 'off';
77
+ export type ReadTrackingMode = RetentionPolicy;
67
78
  /**
68
- * Marker recorded in `StageSnapshot.stageReads` under `readTracking: 'summary'`.
79
+ * Policy for how tracked writes are recorded into `StageSnapshot.stageWrites`
80
+ * (#13c-A) — the sibling of {@link ReadTrackingMode}.
81
+ *
82
+ * - `'full'` (default) — every tracked write `structuredClone`s the value into
83
+ * the stage's write view. Byte-identical to the historical behavior.
84
+ * - `'summary'` — writes record a cheap {@link WriteSummaryMarker} instead of
85
+ * the cloned value.
86
+ * - `'off'` — writes are not recorded at all; `stageWrites` is absent from the
87
+ * snapshot. The writes themselves still commit to shared state and still
88
+ * appear in the commit log — only the per-stage snapshot bookkeeping (and
89
+ * therefore the commit observer's mutations payload) is affected.
69
90
  *
70
- * Honest cost note: `size` is a cheap proxy (string length / array length /
71
- * object key count), NOT a serialized byte count — computing real byte size
72
- * would require an O(value) serialization, which is exactly the cost the
73
- * summary mode removes. `preview` is only produced for primitives and strings
74
- * (first {@link READ_PREVIEW_LENGTH} characters); objects and arrays carry no
75
- * preview for the same reason.
91
+ * Set via `new FlowChartExecutor(chart, { writeTracking })` or
92
+ * `executor.setWriteTracking(mode)` (before `run()`). See
93
+ * `FlowChartExecutorOptions.writeTracking` for the full observable-consequence
94
+ * contract (onCommit payload, redaction precedence, what is OUT of scope).
76
95
  */
77
- export interface ReadSummaryMarker {
78
- /** Discriminant — lets snapshot consumers detect marker entries. */
79
- __readSummary: true;
80
- /** `typeof` result, refined to 'array' / 'null' for objects. */
81
- type: 'string' | 'number' | 'boolean' | 'bigint' | 'symbol' | 'function' | 'object' | 'array' | 'null';
82
- /** Size proxy: string length, array length, or object key count. */
83
- size?: number;
84
- /** First {@link READ_PREVIEW_LENGTH} chars — primitives and strings only. */
85
- preview?: string;
86
- }
87
- /** Max characters captured in {@link ReadSummaryMarker.preview}. */
88
- export declare const READ_PREVIEW_LENGTH = 80;
96
+ export type WriteTrackingMode = RetentionPolicy;
89
97
  /** Serialisable representation of a stage's state (for debugging / visualisation). */
90
98
  export type StageSnapshot = {
91
99
  id: string;
@@ -98,7 +106,10 @@ export type StageSnapshot = {
98
106
  subflowId?: string;
99
107
  isDecider?: boolean;
100
108
  isFork?: boolean;
101
- /** User-level writes made by this stage (pre-namespace keys → values). */
109
+ /** User-level writes made by this stage (pre-namespace keys → values).
110
+ * Shape depends on {@link WriteTrackingMode}: cloned values under `'full'`
111
+ * (default), {@link WriteSummaryMarker}s under `'summary'`, absent under
112
+ * `'off'`. Redacted writes show `'[REDACTED]'` regardless of mode. */
102
113
  stageWrites?: Record<string, unknown>;
103
114
  /** User-level reads made by this stage (pre-namespace keys → values at read
104
115
  * time). Shape depends on {@link ReadTrackingMode}: cloned values under