footprintjs 9.45.0 → 9.46.1

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 (64) hide show
  1. package/CLAUDE.md +10 -10
  2. package/dist/esm/lib/decide/evaluator.js +2 -2
  3. package/dist/esm/lib/decide/evidence.js +2 -2
  4. package/dist/esm/lib/engine/handlers/servedPause.js +2 -2
  5. package/dist/esm/lib/memory/RecordFrame.d.ts +232 -0
  6. package/dist/esm/lib/memory/RecordFrame.js +296 -0
  7. package/dist/esm/lib/memory/SharedMemory.d.ts +1 -1
  8. package/dist/esm/lib/memory/SharedMemory.js +2 -2
  9. package/dist/esm/lib/memory/StageContext.d.ts +71 -155
  10. package/dist/esm/lib/memory/StageContext.js +130 -285
  11. package/dist/esm/lib/memory/TransactionBuffer.d.ts +2 -2
  12. package/dist/esm/lib/memory/TransactionBuffer.js +3 -3
  13. package/dist/esm/lib/memory/borrowedMutation.d.ts +1 -1
  14. package/dist/esm/lib/memory/borrowedMutation.js +2 -2
  15. package/dist/esm/lib/memory/honesty.d.ts +4 -3
  16. package/dist/esm/lib/memory/honesty.js +5 -4
  17. package/dist/esm/lib/memory/index.d.ts +4 -2
  18. package/dist/esm/lib/memory/index.js +6 -4
  19. package/dist/esm/lib/memory/placeholders.d.ts +23 -28
  20. package/dist/esm/lib/memory/placeholders.js +24 -29
  21. package/dist/esm/lib/memory/recordCommit.d.ts +5 -3
  22. package/dist/esm/lib/memory/recordCommit.js +7 -5
  23. package/dist/esm/lib/memory/redaction.d.ts +78 -40
  24. package/dist/esm/lib/memory/redaction.js +130 -64
  25. package/dist/esm/lib/memory/runAddress.d.ts +20 -0
  26. package/dist/esm/lib/memory/runAddress.js +25 -0
  27. package/dist/esm/lib/memory/scrub.d.ts +51 -0
  28. package/dist/esm/lib/memory/scrub.js +72 -0
  29. package/dist/esm/lib/memory/utils.d.ts +3 -3
  30. package/dist/esm/lib/memory/utils.js +4 -4
  31. package/dist/esm/lib/reactive/structuralWrite.d.ts +1 -1
  32. package/dist/esm/lib/reactive/structuralWrite.js +2 -2
  33. package/dist/lib/decide/evaluator.js +3 -3
  34. package/dist/lib/decide/evidence.js +3 -3
  35. package/dist/lib/engine/handlers/servedPause.js +3 -3
  36. package/dist/lib/memory/RecordFrame.js +300 -0
  37. package/dist/lib/memory/SharedMemory.js +2 -2
  38. package/dist/lib/memory/StageContext.js +134 -289
  39. package/dist/lib/memory/TransactionBuffer.js +3 -3
  40. package/dist/lib/memory/borrowedMutation.js +2 -2
  41. package/dist/lib/memory/honesty.js +5 -4
  42. package/dist/lib/memory/index.js +6 -4
  43. package/dist/lib/memory/placeholders.js +25 -30
  44. package/dist/lib/memory/recordCommit.js +9 -7
  45. package/dist/lib/memory/redaction.js +144 -75
  46. package/dist/lib/memory/runAddress.js +29 -0
  47. package/dist/lib/memory/scrub.js +77 -0
  48. package/dist/lib/memory/utils.js +4 -4
  49. package/dist/lib/reactive/structuralWrite.js +2 -2
  50. package/dist/types/lib/memory/RecordFrame.d.ts +232 -0
  51. package/dist/types/lib/memory/SharedMemory.d.ts +1 -1
  52. package/dist/types/lib/memory/StageContext.d.ts +71 -155
  53. package/dist/types/lib/memory/TransactionBuffer.d.ts +2 -2
  54. package/dist/types/lib/memory/borrowedMutation.d.ts +1 -1
  55. package/dist/types/lib/memory/honesty.d.ts +4 -3
  56. package/dist/types/lib/memory/index.d.ts +4 -2
  57. package/dist/types/lib/memory/placeholders.d.ts +23 -28
  58. package/dist/types/lib/memory/recordCommit.d.ts +5 -3
  59. package/dist/types/lib/memory/redaction.d.ts +78 -40
  60. package/dist/types/lib/memory/runAddress.d.ts +20 -0
  61. package/dist/types/lib/memory/scrub.d.ts +51 -0
  62. package/dist/types/lib/memory/utils.d.ts +3 -3
  63. package/dist/types/lib/reactive/structuralWrite.d.ts +1 -1
  64. package/package.json +1 -1
@@ -2,35 +2,35 @@
2
2
  * StageContext — Execution context for a single stage in a flowchart run
3
3
  *
4
4
  * Like a stack frame in a compiler/runtime:
5
- * - Reference to SharedMemory (accessing heap memory)
6
- * - TransactionBuffer for staging mutations (transaction buffer)
5
+ * - A RecordFrame — the record half (C3): the heap, the address, the
6
+ * first-touch base, the lazy transaction buffer and every write's scrub
7
+ * (C4), the readKeys list
7
8
  * - Links to parent/child/next contexts (call stack frames)
9
+ * - What belongs to a stage inside a run: retention of reads and writes, the
10
+ * redaction decision per write (`redaction.ts · decideWrite`) and where the
11
+ * frame writes (`runAddress.ts`), the dev-mode warnings, the commit observer
8
12
  * - DiagnosticCollector for logs, errors, metrics
9
13
  */
10
14
  import { type DiagnosticChannel, DiagnosticCollector } from './DiagnosticCollector.js';
11
- import { EventLog } from './EventLog.js';
15
+ import type { EventLog } from './EventLog.js';
12
16
  import type { EmitSourcePosition } from './eventPosition.js';
13
17
  import type { RedactionVerdict } from './redaction.js';
14
18
  import { RedactionRule } from './redaction.js';
15
19
  import type { RunPolicy } from './runPolicy.js';
16
- import { SharedMemory } from './SharedMemory.js';
17
- import { TransactionBuffer } from './TransactionBuffer.js';
20
+ import type { SharedMemory } from './SharedMemory.js';
18
21
  import type { CommitPhase, FlowControlType, StageSnapshot, UntrackedSource } from './types.js';
19
22
  export declare class StageContext {
20
- private sharedMemory;
21
23
  /**
22
- * Parallel redacted mirror of `sharedMemory`. Populated in `commit()` with
23
- * the already-computed redacted patches (the same ones fed to `eventLog`).
24
- * Present **only** when the executor has been told to maintain a redacted
25
- * view — i.e. when a `RedactionPolicy` is configured. Otherwise undefined,
26
- * zero extra work per commit.
27
- *
28
- * The mirror is read via `FlowChartExecutor.getSnapshot({ redact: true })`
29
- * and is the foundation for the "export trace" / paste-into-viewer feature
30
- * — consumers share the redacted view externally without leaking raw PII
31
- * through `sharedState`.
24
+ * The record half of this frame (`RecordFrame.ts`, L3, C3): the heap and,
25
+ * when the run keeps them, the redacted mirror and the log; the address;
26
+ * the first-touch base; the lazy transaction buffer; the readKeys list.
27
+ * Every state read of this frame goes through it, and so does every
28
+ * commit. The mirror is present **only** when the run's policy keeps a
29
+ * redacted view (a `RedactionPolicy` is configured) — read via
30
+ * `FlowChartExecutor.getSnapshot({ redact: true })`, the foundation for
31
+ * sharing a trace without leaking raw PII through `sharedState`.
32
32
  */
33
- private redactedSharedMemory?;
33
+ private readonly record;
34
34
  /**
35
35
  * The run's policy — the four dials, the redaction rule, the mirror flag
36
36
  * (`runPolicy.ts`, F5). Held BY REFERENCE: one frozen object per run (and
@@ -40,15 +40,6 @@ export declare class StageContext {
40
40
  * ({@link usePolicy}). Never edited; a change is a new object.
41
41
  */
42
42
  private policy;
43
- private buffer?;
44
- /**
45
- * Committed-state view captured at this stage's FIRST touch (first read OR
46
- * first write) — held by REFERENCE, never cloned. See
47
- * {@link firstTouchState} for the algorithm and the immutability invariant
48
- * that makes a bare reference safe.
49
- */
50
- private stateView?;
51
- private eventLog?;
52
43
  /** The emitting frame's logical leg, never the mutable executor's current leg. */
53
44
  private emitRunId?;
54
45
  stageName: string;
@@ -65,7 +56,13 @@ export declare class StageContext {
65
56
  * none — the same once-per-execution law `_untrackedSources` keeps.
66
57
  */
67
58
  tags?: readonly string[];
68
- runId: string;
59
+ /**
60
+ * The run this frame belongs to — its identity on every event, and the run
61
+ * namespace it reads and writes in ({@link runAddress}), read once, when the
62
+ * frame is built (C3). Only {@link useAddressOf} moves the address after
63
+ * that, and only before the first write.
64
+ */
65
+ readonly runId: string;
69
66
  /**
70
67
  * The run namespace this frame writes and reads in when that is not its own
71
68
  * `runId` — set once, before the frame's first write, by
@@ -128,11 +125,6 @@ export declare class StageContext {
128
125
  * tier (`ScopeFacade`, one per stage execution), not here.
129
126
  */
130
127
  private _committed;
131
- /** Lazily-allocated ordered registry of keys tracked-read in THIS stage —
132
- * the source of the per-write prefix. Only allocated under the
133
- * `'reads-prefix'` dial; insertion-ordered (a Set) and monotonic, which
134
- * is what makes "last write's prefix == union" hold in delta mode. */
135
- private _provenanceReads?;
136
128
  /** Tracked reads of a selected value ({@link selectedReads}). */
137
129
  private _selectedReads;
138
130
  /** Each object this frame read under a selected name → that read's verdict (`stageWrite` hands it on). */
@@ -159,7 +151,7 @@ export declare class StageContext {
159
151
  /**
160
152
  * Install a parallel redacted mirror. Subsequent `commit()` calls apply
161
153
  * the already-computed redacted patches — the ones the log records — to
162
- * this mirror, beside the raw patches on `sharedMemory` (`recordCommit`).
154
+ * this mirror, beside the raw patches on the heap (`recordCommit`).
163
155
  * Child / next contexts inherit the mirror via `createNext` / `createChild`.
164
156
  *
165
157
  * Called on a runtime's root frame by `ExecutionRuntime` when the run's policy keeps a mirror.
@@ -198,6 +190,12 @@ export declare class StageContext {
198
190
  * {@link createChild} both call it, and it names no dial.
199
191
  */
200
192
  private inheritRun;
193
+ /**
194
+ * The ONE place this frame's policy changes: the frame holds it, and its
195
+ * record frame encodes under the same reference (`commitValues`,
196
+ * `writeProvenance` — `RecordFrame · useEncoding`), so the two never differ.
197
+ */
198
+ private adoptPolicy;
201
199
  /**
202
200
  * Record a tracked user-level write, policy-gated (#13c-A) — the single
203
201
  * bookkeeping path for {@link setObject} and {@link updateObject}. Holds
@@ -210,10 +208,10 @@ export declare class StageContext {
210
208
  * and stores nothing under `'off'` (entry skipped entirely — nothing to
211
209
  * leak). A field-level verdict scrubs a clone BEFORE the dial sees it, so
212
210
  * a summary preview can never show the secret either. The verdict is the
213
- * one `stageWrite` asked the rule for BEFORE staging, carried to commit
214
- * with the value. The staged write itself is unaffected — redaction of
215
- * the committed payload is handled by the transaction buffer's
216
- * `redactedPaths`.
211
+ * one `stageWrite` decided BEFORE staging, carried to commit with the
212
+ * value. The staged write itself is unaffected — redaction of the
213
+ * committed payload is the record frame's scrub of that write
214
+ * (`RecordFrame · write`, then `scrub.ts` at commit).
217
215
  */
218
216
  private trackWrite;
219
217
  /**
@@ -247,80 +245,21 @@ export declare class StageContext {
247
245
  /**
248
246
  * THE ONE FUNNEL every staged write passes through — facade writes AND
249
247
  * the paths that bypass the facade (subflow seed, `outputMapper`
250
- * merge-back, resume re-seed). The ONE decision is made here: an explicit
251
- * per-call flag makes the value secret outright (and marks the key for the
252
- * rest of the run, the declare-once contract); otherwise the installed
253
- * rule decides from the user-level path. It stages the write, registers
254
- * the redacted paths the commit log and mirror will scrub (whole key, or
255
- * the fields inside the value), and keeps the run's marked-keys set in
256
- * step (a whole verdict marks its key; a delete clears the mark). Returns
257
- * the verdict so the caller retains and reports under the same decision.
248
+ * merge-back, resume re-seed). Two owners (C4): the run's rule DECIDES,
249
+ * the record frame WRITES the verdict's bytes, in four steps
250
+ * (`redaction.ts`, "The write decision"):
251
+ * 1. the verdict — `decideWrite`, under the rule active as the write begins;
252
+ * 2. identity inheritance — `inheritByIdentity`, against this frame's
253
+ * selected reads as they stand after step 1;
254
+ * 3. the bytes — `RecordFrame · write`, the op and its scrub (the whole
255
+ * value, or the fields inside it);
256
+ * 4. the marks — `markStagedWrite`, only once the write staged, so a
257
+ * write that fails to stage marks nothing.
258
+ * Each step reads the rule (and the selected reads) when it acts, as this
259
+ * funnel always did. Returns the verdict so the caller retains and reports
260
+ * under the same decision.
258
261
  */
259
262
  private stageWrite;
260
- /**
261
- * ── The first-touch state view (#13) ────────────────────────────────────
262
- *
263
- * WHAT: returns the committed shared state as it was at this stage's FIRST
264
- * touch (first read or first write), capturing the reference on first call.
265
- * Serves two consumers: reads before the first write ({@link readState})
266
- * and the transaction buffer's diff base ({@link getTransactionBuffer}).
267
- *
268
- * WHY A BARE REFERENCE IS SAFE — the invariant this rests on: committed
269
- * state is immutable-after-swap. Every write to `SharedMemory` builds the
270
- * NEXT generation and swaps it in (copy-on-write, 9.29.0: `applyPatch` via
271
- * `nextGeneration`, and `setValue`/`updateValue` too) — it copies the root
272
- * and the containers on each written path, shares the rest, and never
273
- * edits a container of the generation a stage captured here. Holding the
274
- * reference therefore gives this stage a stable snapshot at zero cost — no
275
- * clone, which is the entire point of #13. The transaction buffer's
276
- * net-change diff base rests on the same guarantee: it IS this view.
277
- *
278
- * WHY FIRST TOUCH, not first write: the pre-#13 eager engine cloned the
279
- * state into the buffer at the stage's first ACCESS, anchoring both its
280
- * snapshot reads and its commit baseline (the net-change diff base) there.
281
- * #13's first cut anchored the lazy buffer at first WRITE — observably
282
- * different when something else commits in the gap between this stage's
283
- * first read and its first write. That gap is REACHABLE: fork siblings are
284
- * namespace-isolated for run-scoped keys (each child writes under
285
- * `runs/<childId>/`), but ROOT-level keys are shared — written via
286
- * `setGlobal` from consumer scope code and, critically, by
287
- * `SubflowInputMapper`'s output mapping (`parentContext.setGlobal`), which
288
- * is exactly what runs when a subflow is a fork branch. A sibling's
289
- * root-key commit landing in the gap would shift this stage's diff base,
290
- * making its CommitBundle record a phantom change (or swallow a real one)
291
- * relative to the eager engine. Anchoring the view at first touch restores
292
- * the EXACT eager semantics — sequential AND parallel — at zero clone cost.
293
- *
294
- * Read visibility is two-tier, matching eager byte-for-byte: keys present
295
- * in the view at first touch read repeatably from it; keys ABSENT from it
296
- * fall back to LIVE state (the eager engine's exact fallback — a
297
- * mid-flight sibling root-key write was always visible to reads, and
298
- * stays visible; only the DIFF BASE is pinned).
299
- */
300
- private firstTouchState;
301
- /** Lazily creates the transaction buffer on the stage's FIRST WRITE (#13).
302
- *
303
- * Reads NEVER construct it: read-your-writes only matters once a staged
304
- * write exists, so before that {@link getValue}/{@link getValueDirect}
305
- * serve from the first-touch state view and {@link commit} records an
306
- * empty bundle — all with ZERO `structuredClone`s of the shared state.
307
- *
308
- * The buffer's base is the FIRST-TOUCH view, NOT the live state at write
309
- * time: under parallel forks a sibling may have committed between this
310
- * stage's first read and this write, and the net-change diff base must
311
- * stay anchored at first touch to match the eager engine — see
312
- * {@link firstTouchState}. */
313
- getTransactionBuffer(): TransactionBuffer;
314
- /**
315
- * Where this frame writes and reads: `['runs', <id>]`, or `[]` (the root) with
316
- * no run id. Handed to the record layer as data (C2): the buffer's address,
317
- * and where `SharedMemory · getValue` looks before the root.
318
- */
319
- private get address();
320
- /** Builds an absolute path inside the shared memory: this frame's {@link address}, then the path. */
321
- private withNamespace;
322
- /** The run namespace writes and reads go to: {@link addressRunId}, else `runId`. */
323
- private get namespaceId();
324
263
  /**
325
264
  * Write and read at ANOTHER frame's address (its run namespace) while this
326
265
  * frame keeps its own identity on the record — `stage`, `stageId`,
@@ -328,9 +267,13 @@ export declare class StageContext {
328
267
  * (`SubflowExecutor · executeSubflow`, R13): the mount frame of a branch or
329
268
  * fork child lands its values where its parent writes, as before, and its
330
269
  * bundle now names the mount instead of the stage before it. Refused once
331
- * the frame has staged anything — its buffer's address is fixed then.
270
+ * the frame has staged anything — its buffer's address is fixed then. The
271
+ * decision is the engine's (namespace ids, as since R13); the record frame
272
+ * takes the address it comes to (`RecordFrame · useAddress`).
332
273
  */
333
274
  useAddressOf(frame: StageContext): void;
275
+ /** The run namespace writes and reads go to: {@link addressRunId}, else `runId`. */
276
+ private get namespaceId();
334
277
  patch(path: string[], key: string, value: unknown, shouldRedact?: boolean): RedactionVerdict;
335
278
  set(path: string[], key: string, value: unknown): void;
336
279
  merge(path: string[], key: string, value: unknown, shouldRedact?: boolean): RedactionVerdict;
@@ -342,26 +285,6 @@ export declare class StageContext {
342
285
  updateGlobalContext(key: string, value: unknown): void;
343
286
  appendToArray(path: string[], key: string, items: unknown[], description?: string): void;
344
287
  mergeObject(path: string[], key: string, obj: Record<string, unknown>, description?: string): void;
345
- /** Buffer-aware read, mirroring the eager engine's read order byte-for-byte:
346
- *
347
- * 1. staged writes + first-touch snapshot — `buffer.get` over its
348
- * workingCopy when the buffer exists, else `nativeGet` over the
349
- * zero-clone state view (the buffer's base IS that view, so the two
350
- * tiers agree on content);
351
- * 2. LIVE state via `sharedMemory.getValue` for keys absent from the
352
- * snapshot — including its run→global namespace fallback. The eager
353
- * engine had this exact live fallback for snapshot-missing keys;
354
- * byte-identity over purity. After the stage's first write such a
355
- * value can be the very container the buffer's diff base holds at
356
- * the path (the stage deleted or unset it, or replaced a container
357
- * above it), so the base is detached there first
358
- * (`TransactionBuffer · detachBase`): an in-place edit of the value
359
- * (out of contract) written back is recorded, as on 9.28.0, whose
360
- * base was a clone taken at the first write.
361
- *
362
- * Reads never construct the buffer (#13): a stage that never writes
363
- * performs zero clones of the shared state. */
364
- private readState;
365
288
  /**
366
289
  * Tracked read. The returned value is BORROWED — see the contract on
367
290
  * `ScopeFacade.getValue`. Read-tracking cost is policy-gated (#14):
@@ -420,8 +343,8 @@ export declare class StageContext {
420
343
  * those are engine reads (the subflow merge-back), not user reads.
421
344
  *
422
345
  * It also only looks at keys served from the PINNED source — the first-touch
423
- * view or this stage's own buffer. A key absent from both was served by
424
- * `readState`'s live fallback, which a parallel sibling's root-key commit can
346
+ * view or this stage's own buffer. A key absent from both was served by the
347
+ * read's live fallback (`RecordFrame · read`, tier 2), which a parallel sibling's root-key commit can
425
348
  * legitimately move; accusing the stage there would be a false alarm. For
426
349
  * the same reason it runs on the frame's FIRST commit only: the reads belong
427
350
  * to that round, and by the engine's second round (a fork double-commit, a
@@ -453,21 +376,16 @@ export declare class StageContext {
453
376
  * `outputMapper` (a lazy mount, every `parallelForEach` branch), whose exit
454
377
  * is its only bundle, records that bundle as its own, tags and all.
455
378
  *
456
- * Commit is the stage's lifecycle end: `buffer` (its working copy and
457
- * whatever private copies its reads took) and `stateView` (a reference that
458
- * pins one committed-state GENERATION) are only needed DURING execution,
459
- * as the read snapshot + net-change diff base. The execution tree retains
460
- * every StageContext for the lifetime of the run, so WITHOUT the release a
461
- * long loop retains one state generation per executed stage — measured
462
- * O(N²) before copy-on-write (9.29.0), when each generation and each
463
- * buffer was a whole-state clone: 563.8MB at N=200 on an agent-style
464
- * chart; a 500-iteration agent OOMed a default Node heap (backlog #18).
465
- * Generations now share every unchanged subtree, but a pinned one still
466
- * keeps the containers later commits replaced alive.
379
+ * Commit is the stage's lifecycle end: the record frame's buffer and
380
+ * first-touch base are only needed DURING execution, as the read snapshot +
381
+ * net-change diff base, so the record frame releases them once the commit
382
+ * has landed and the observer has seen it (`RecordFrame · release` — why a
383
+ * frame that kept them would retain one state generation per executed
384
+ * stage, and why a re-used frame re-anchors on current state, is there).
467
385
  *
468
- * RE-USE AFTER COMMIT stays correct because both fields re-create lazily:
469
- * - a later READ re-anchors via {@link firstTouchState} on the CURRENT
470
- * committed state (which includes this stage's own flushed writes);
386
+ * RE-USE AFTER COMMIT stays correct because both re-create lazily:
387
+ * - a later READ re-anchors on the CURRENT committed state (which includes
388
+ * this stage's own flushed writes);
471
389
  * - a later WRITE constructs a fresh buffer on that re-anchored view, so a
472
390
  * second commit diffs against post-first-commit state. The pre-release
473
391
  * buffer behaved the same for VALUES (its `workingCopy` was reset on
@@ -498,20 +416,18 @@ export declare class StageContext {
498
416
  * (M1: the transaction buffer holds writes until `commit()` flushes them).
499
417
  *
500
418
  * What is released, and why each one matters for the next attempt:
501
- * - `buffer` — the staged writes themselves; the next attempt starts
502
- * with nothing staged;
503
- * - `stateView` — the first-touch anchor; the next attempt re-anchors on
504
- * committed state as it stands NOW (a sibling fork branch
505
- * may legitimately have committed in between);
419
+ * - the record frame's staged state (`RecordFrame · discard`) — the buffer
420
+ * (the next attempt starts with nothing staged), the
421
+ * first-touch base (it re-anchors on committed state as
422
+ * it stands NOW — a sibling fork branch may legitimately
423
+ * have committed in between) and the readKeys list (#P1:
424
+ * a discarded attempt's reads must never reach the next
425
+ * attempt's `TraceEntry.readKeys`);
506
426
  * - `_stageWrites` / `_pendingWrites` / `_stageReads` — the snapshot
507
427
  * payload; without the reset the execution tree would
508
428
  * report writes that were discarded, which is the exact
509
429
  * lie this feature exists to prevent (the pending map
510
430
  * holds bare references — dropping it un-clones nothing);
511
- * - `_provenanceReads` — the per-write read prefix (#P1); a discarded
512
- * attempt's reads must never appear in the next attempt's
513
- * `TraceEntry.readKeys`, or a backward slice would follow
514
- * an edge that no committed write ever had;
515
431
  * - `_untrackedSources` — the D2 honesty markers, released with the rest.
516
432
  *
517
433
  * What is deliberately KEPT: `debug` (logs, metrics, errors, flow messages).