<!-- analyzed-at: 22953d9 @ 2026-07-02 | model: fable-5 -->
# footprintjs — feature-work map

Self-explaining flowchart engine: a fluent builder emits a static chart; one DFS pass executes it while observer channels + a commit log capture everything ("collect during traversal, never post-process"). This file maps seams and blast radius; for API description read the code/README — **trust the code** where any doc disagrees.

## Module map
Entry points: `footprintjs` (public API) · `/recorders` (factories) · `/trace` (the record's shapes `CommitBundle`/`TraceEntry`/`MemoryPatch`, commitLog queries, `applySmartMerge`, stores, causalChain) · `/write` (C5, 9.47.0: the record layer as the public writer — `RecordFrame`, `SharedMemory`, `EventLog` + option types; engine-free, `test/architecture/write-door.test.ts`) · `/advanced` (engine internals; until 10.0.0 also every record name it handed out before 9.47.0, as `keptUntil` second doors) · `/detach` · `/zod` (opt-in; zod never imported by core). New public symbols are wired through the barrel that OWNS them (canonical path) — do not assume `export *` chaining. A minor never drops a published name: a move keeps the old door until the next major (`test/architecture/published-doors.test.ts`).

| src/lib/ | one job |
|---|---|
| capture/ | retention policies + bounded payload summarization (leaf; shared by tracking dials AND deferred-observer capture) + `valueKinds` (the ONE classifier of value kinds: `deepEqual`'s arms and what freezing seals, 9.44.2; `valueKinds` and `freeze` are record files — `RECORD_FILES`, C6) + the shared leaves `invokeHook` (the one recorder-hook invoker both delivery tiers use), `circular` (dev-mode cycle probe), `summarizeValue` (the narrative one-liner); `lib/devMode.ts` — the dev-mode flag — is its sibling leaf (all four moved out of scope/ and recorder/ in F0: they closed the one memory ⇄ scope ⇄ recorder module cycle) |
| memory/ | transactional state: SharedMemory heap, StageContext frames, `RecordFrame` (the record half of a frame, L3, C3: the address, the first-touch base, the two-tier read, the lazy buffer (private since C4: staged only through `write(path, value, verb, scrub?)`, which carries a redaction verdict's bytes), the readKeys list, and the commit through `recordCommit`; StageContext composes it; with `SharedMemory` and `EventLog` it is the public writer on `/write`, C5 — the engine writes through the same classes), the log's scrub (`scrub.ts`, L2: `scrubPatch` / `redactPatch`, C4), TransactionBuffer staging (+ admission.ts, the admitted record's check; deltaEncoding.ts, the delta encoder's helpers), EventLog commit log — PLUS the ONE verb law (`verbs.ts`, L1: `applyVerb`, `foldRows`, `foldKey`; leaves `paths.ts` / `equality.ts` / `merge.ts`; `utils.ts` re-exports them) and trace analysis (backtrack.ts causalChain, commitLogUtils.ts). The record's types are in `types.ts`, which imports nothing; `ExecutionTree` there is the tree as the readers read it (C6). The frame's types are in `frameTypes.ts` (L4: `StageSnapshot`, `FlowMessage`, the retention dials, C6) |
| schema/ | duck-typed validation (`.safeParse/.parse` ⇒ 'parseable') |
| builder/ | fluent DSL → FlowChartSpec → StageNode graph; build-time StructureRecorder channel. NOT standalone: imports runner's makeRunnable (FlowChartBuilder.ts:18) |
| scope/ | ScopeFacade per stage (tracked get/set, $emit, frozen args/env); built-in ScopeRecorders in scope/recorders/ |
| reactive/ | TypedScope deep Proxy over the facade — four write laws + the honest boundary (what `find`/`for…of` hands back is raw and WARNED ABOUT in dev mode, not intercepted; a proxy held past its stage IS refused — `ScopeFacade` · `assertLive`); handles.ts = the registry of every proxy the factories built → the value behind it, asked wherever an app value enters the record (the set traps since 9.24.0 — no JSON round-trip any more; fan-out `resolveItems`, subflow in/out mappers, `$setValue`/`$update`/`$batchArray` since 9.23.3, after a fan-out over object items seeded the handle and lost every child); see src/lib/reactive/README.md |
| decide/ | decide()/select() evidence capture |
| recorder/ | stores (KeyedStore/SequenceStore/BoundaryStateStore), CombinedRecorder routing, built-ins (Topology/InOut/ControlDep/Quality); `CommitRangeIndex` is a record file, ranked L3 (C6) |
| slice/ | variable-first backward slicing (triage query layer): sliceForKey (anchor at last writer → causalChain) + arrayProvenance/elementProvenance (append-fold element births — the agent mega-key fix) + KeysReadSource strategies. Pure post-hoc queries; imports memory/ ONLY (record files; its tree is `ExecutionTree`, C6) (see src/lib/slice/README.md) |
| time-travel/ | the READER'S cursor over a finished trace (9.17.0): `timeTravel()` stops/moves/marks/`drill()`, `stateAt()` fold + honesty basis, `TimeTravelStrategy<TMeta>` seam (one shipped: `commitStops`). 9.18.0 composer's side: axis.ts (`splitAxis` = the ONE guard of the `[start,…stages,end]` contract; `filterStops` = filter + re-partition + `Stop.meta` vocabulary + `Stop.prologue` on an absorbing start — a FLAG, deliberately not a 5th StopKind), bundles.ts (per-bundle narrowing of `unknown[]` rows → index-holding GAPS in `FoldedState.skipped`), chain.ts (`timeTravel([paused, resumed])`: commit indices stay RUN-LOCAL + `Stop.sourceIdx`; refused unless execution indices are monotonic across legs and no id repeats). Read-time only — never a second live cursor. Record files only (`RECORD_FILES`, C6): memory/ + `ids/runtimeStageId` + `capture/freeze` + `capture/valueKinds`; the tree it reads is the record's `ExecutionTree`, a supertype of `StageSnapshot` (9.48.0) (see src/lib/time-travel/README.md) |
| pause/ | PauseSignal / FlowchartCheckpoint types + `interrupt(scope, payload)` (throws InterruptSignal; converted to PauseSignal at the StageRunner boundary, `pausedBy:'interrupt'`) |
| engine/ | FlowchartTraverser + handlers/; narrative/ = the whole FlowRecorder channel (dispatcher + 9 strategies), not just text; `lib/ids/branchSegment.ts` (moved to the `ids/` leaf in 9.37.0) = the ONE owner of the generated-segment grammar (`<stageId>~<index>`, marker `~` RESERVED); `executeStage` (:720) = the ONE funnel every stage fn passes through — and therefore the ONE owner of the declarative `retry` loop; `handlers/ResumeEntry.ts` = the ONE owner of a resume's ONE-SHOT re-entry (M2, 9.28.0: start + one hop per subflow on the pause path, each level's dispatcher continuation read from the chart, waiting sibling pauses queued at their fan-out) |
| runner/ | FlowChartExecutor wiring — composes `attach.ts` (RunObservers) · `resume.ts` · `checkpoint.ts` · `snapshot.ts` · `options.ts` (F9, 9.41.0); (runtime, recorders, pause/resume, snapshot, DeferredObserverTier). RunContext.ts is a fluent run builder, NOT the threaded per-run context |
| observer-queue/ | pure deferred-delivery pipeline (engine imports IT, never reverse) |
| contract/, detach/ | I/O schema normalization (OpenAPI generation belongs to runner/RunnableChart); fire-and-forget child charts (drivers passed explicitly) |

**The fence (F0):** a file imports only its own layer or BELOW — L0 values/leaves · L1 verbs · L2 staging+commit · L3 the log as a read model + the record half of a frame (`RecordFrame`) · L4 frame+run policy+the redaction verdict (`redaction.ts`, C4) · L5 scope/recorders/hooks · L6 engine · L7 builder+executor · L8 the `src/*.ts` entry barrels. The table is DATA in `scripts/layering.config.cjs` (most specific pattern wins; an unplaced file is an error, and `memory/` has no directory default because it spans L0–L4) and has three readers, none with a copy: `.eslintrc.js` (generated `import/no-restricted-paths` zones + `import/no-cycle`, src only), `npm run check:layering` (value-level cycles at module AND file level + upward RUNTIME edges; type-only imports excluded — the set equals the compiled CJS `require` graph), `test/architecture/layering.test.ts`. Three edges are named on purpose — builder → `runner/RunnableChart.ts` (`makeRunnable`), engine → `reactive/handles.ts`, scope → `detach/spawn.ts` — plus a short `TYPE_ONLY_ALLOWANCES` list (upward import-type declarations tsc erases, which the lint rule cannot tell apart; none starts below L4 since C6); both lists are live-checked and only shrink. **The second rule (C6, 9.48.0): the record is closed.** `RECORD_FILES` (same file; 44 files, all L0–L3) is the record: a record file imports only record files, by value OR by type — `check:layering` fails on an import out of it (`recordEscapes`), on a stale entry, or on a record file above L3, and compiles the list on its own (`recordAlone`: that program must load nothing else and report no diagnostic, which catches `import()` type references and package imports); ESLint has the import zone. A new file that is the record's goes into `RECORD_FILES` as well as LAYERS. R1 = R2 = 0. A new src file under an unsplit directory inherits its layer; one under `memory/` (or an exact-path entry) must be added to LAYERS — an unplaced file fails lint and the script. Public symbols have ONE canonical door each — `test/architecture/exports.test.ts` fails on any symbol two of the seven `package.json` doors both hand out unless it is in `SECOND_DOORS` (19 standing in three reasoned groups, plus 15 `keptUntil: '10.0.0'` old `/advanced` doors of C5's moves; an alias counts as a door).

## Core state & flow
- `SharedMemory` (memory/SharedMemory.ts) — address-scoped heap: the frame's record frame hands it the address (`RecordFrame · address`; the engine decides it in `runAddress.ts · runAddress` / `StageContext · useAddressOf` from the ONE L4 constant `runAddress.ts · RUN_NAMESPACE`, C2–C4); committed state is **immutable-after-swap**, COPY-ON-WRITE since 9.29.0: every write (`applyPatch` → `verbs · nextGeneration`, `setValue`/`updateValue`) copies the root + the containers on each written path and SHARES every other subtree with the previous generation, then swaps. A generation is never edited; the seed is detached once at construction. Design: docs/design/2026-10-copy-on-write-commit.md.
- `StageContext` (memory/StageContext.ts:29) — per-stage frame inside a run; composes `RecordFrame` (memory/RecordFrame.ts, L3, C3: the lazy TransactionBuffer, the first-touch base, the two-tier read, readKeys, the address); `commit()` → `RecordFrame · commit` (payload, then the names) → `memory/recordCommit.ts · recordCommit` (live apply → scrub → mirror → `EventLog.record`) → onCommit → `RecordFrame · release`. `runId` is readonly (the address is decided once, 9.46.0). A staged write is `StageContext · stageWrite`: `redaction.ts · decideWrite` (the verdict) → `inheritByIdentity` (an object read under a selected name keeps its rule) → `RecordFrame · write(…, scrubOf(verdict))` (its bytes) → `markStagedWrite` (the run's marks, only once the write staged; C4, 9.46.1). Each step reads the rule when it acts.
- `CommitBundle`/`EventLog` — one bundle per executed stage, `bundle.idx` == array position; encoding governed by `commitValues: 'full'|'delta'`. Canonical doors: `CommitBundle` on `/trace`, `EventLog` on `/write` (C5; also `/advanced` until 10.0.0).
- `TraversalContext` (engine/narrative/types.ts:172) — THE per-event context (runId, runtimeStageId, parentRuntimeStageId, subflowPath, loopIteration); stamped on every FlowRecorder event.
- `runtimeStageId` = `[subflowPath/]stageId#executionIndex`; counter shared by reference into subflow traversers → globally unique; NOT reset on resume.
- Subflows get a fresh isolated ExecutionRuntime (SubflowExecutor.ts:119); inputMapper seeds, outputMapper merges back (arrays CONCAT unless `arrayMerge: Replace`).

**Retry (9.15.0):** `StageNode.retry` is a POLICY on a stage, not a kind — no new node type, no `computeNodeType` edit. The loop lives in `FlowchartTraverser.executeStage` (:720), so linear/streaming/pausable/decider/selector/fork-child/subflow-inner all get it from one implementation. A failed non-final attempt calls `StageContext.discardStaged()` (`RecordFrame · discard`, which drops the buffer, the first-touch base and readKeys, plus _stageWrites/_stageReads + _untrackedSources); the final attempt is today's law untouched. PauseSignal and abort are NEVER retried. One runtimeStageId + one CommitBundle per stage regardless of attempts; retries do not consume `maxIterations`.

**Declared tags (9.21.0):** `StageNode.tags?: readonly string[]` is a NAME on a stage, declared at build time — POLICY-like (the retry precedent): no node type, no `computeNodeType` edit, no type-union site, and the one prefixer (`engine/graph/prefixNodeTree.ts`, 9.37.0) spreads `{ ...node }` so it rides subflow prefixing untouched. ONE landing site `FlowChartBuilder.ts · applyTags` (twin of `applyRetryPolicy`) serves the `.tag(...names)` cursor modifier and every `options.tags` site; refuses empty/non-string/`~`/duplicate/double-declare at build. Stamp: `FlowchartTraverser.executeNodeStep` sets `context.tags = node.tags` beside `runtimeStageId` (unconditionally — a re-used context must not keep a prior node's names); `memory/recordCommit.ts · recordCommit` (C1, 9.44.3; was `StageContext · bundleFor`) stamps `tags` beside `untrackedSources` on BOTH commit paths and `StageContext · commit` RELEASES them, so a double-commit path (fork child fan-out repeat, mount exit) records it once — absent when empty (`CommitBundle.tags?`; pinned byte-identical to 9.20.0 by test/lib/engine/scenario/declared-tags-byte-identity.test.ts). NOT inherited by createNext/createChild; the one synthetic node that could lose it is `FlowChartExecutor.resume`'s stand-in for the paused stage — built by `runner/resume.ts · standInFor` from the paused node ITSELF (fn swapped), so it keeps the tags on both re-entries; it runs ONCE — a later visit of the paused id runs the chart's real node, which carries its own tags. The spec carries `tags` as-is (Map advertises the vocabulary; `RuntimeStructureManager.stageNodeToStructure` copies it for run-time-resolved nodes). Reader: `time-travel/tagStops.ts` = `filterStops(commitStops(...), any-of over log[stop.commitIdx].tags, meta = the array)`, exported from `/trace`. Never a runtime `$tag()` — a value would ride past every redaction point. Design: docs/design/2026-09-declared-tags.md.

**Event order (code-verified; older docs had 3/4 swapped):** onStageStart → onRead/onWrite (live, PRE-commit) → **onStageEnd (StageRunner.ts:81) → onCommit (`StageContext · commit`, after `RecordFrame · commit`)** → FlowRecorder onDecision/onFork/onSelected/onSubflowEntry → onStageExecuted (uniform, with stageType). Error path: commit STILL happens (FlowchartTraverser.ts:1088) before onError+rethrow — a failing stage's writes land. For ANY thrown value (null, null-prototype, hostile Proxy) catch blocks describe it with `errors/errorInfo · thrownText` (never throws), onError fires, and run() rejects with the ORIGINAL value; a failed commit (e.g. an uncloneable write) also fires onError (`handlers/commitStage.ts`, 9.40.0). `interrupt()` rides the ERROR path's shape, not the pausable one: the stage function threw, so onStageEnd does NOT fire (the `addPausableFunction` pause DOES fire it — there the function returned normally).

## Extension points
- **New stage kind**: boolean flag on `StageNode` (engine/graph/StageNode.ts:39 — kinds are flags, not an enum) + builder method on FlowChartBuilder (pattern: addPausableFunction :1264; fire structure events endpoint-before-edge) + phase in the hard-coded chain `executeNodeStep` (FlowchartTraverser.ts:801 — no handler registry; new handler class takes HandlerDeps, engine/types.ts:344, instantiated in traverser ctor :402-413). Must also edit 5 type-union sites: engine/types.ts:477, builder/types.ts:50, builder/types.ts:104, engine/types.ts:427, and computeNodeType's flag→type mapping (RuntimeStructureManager.ts:19-21 — the only one the compiler won't flag; miss it and the new kind silently serializes as 'stage'). Zero-engine alternative: pure sugar over addFunction (pattern: addDetachAndForget :1343). WORKED EXAMPLE (9.14.0 addParallelForEach): flag `isDynamicParallel` + ParallelForEachHandler + Phase 0b (BEFORE Phase 1 VALIDATE — the node has no fn/children/decider by design); it added NO new node type — the spec carries a BOOLEAN and computeNodeType returns 'fork' (precedent: isPausable/isLazy/isStreaming are flags on a 'stage'), which is the cheaper choice whenever the new kind is a variant of an existing shape.
- **Decider/selector**: addDeciderFunction :1433 → DeciderList (:53); addSelectorFunction :1478 → SelectorFnList (:428); runtime match on `child.branchId ?? child.id` (DeciderHandler.ts:133). Evidence via decide()/select() (decide/decide.ts:169/218). `decide()`'s 3rd param is `DefaultBranch = string | { branch, label? }` (9.16.0) — the default is chosen by NO rule, so `rules[].label` can never name it; the object form puts that label on `DecisionEvidence.defaultLabel`, spread into BOTH evidence exits (matched AND fallthrough) so a harvest doesn't gain/lose the meaning with the data. Bare string ⇒ key absent, byte-identical to pre-9.16. `select()` has no default and is deliberately untouched.
- **Recorder**: channels = ScopeRecorder (scope/types.ts:137), FlowRecorder (engine/narrative/types.ts:425), EmitRecorder (recorder/EmitRecorder.ts:121 — attachEmitRecorder just delegates to the scope channel), CombinedRecorder (recorder/CombinedRecorder.ts:98, routed by method-shape), StructureRecorder (builder/structure/StructureRecorder.ts:347, build-time; its hooks are NOT in `HOOKS` (build-time, not a run channel) — the `StructureRecorderDispatcher` fires each by name, so a new structure hook = interface + `fire*` + the builder's `_fire*` helper; 9.24.0 `onStageTagged` is the worked example: names that land AFTER `onStageAdded` through the `.tag()` door go out as their own event, `StructureStageAddedEvent.tags` carries the ones that landed before). Attach via executor.attach*Recorder — idempotent by id. **One registry since 9.36.0 (F6): `recorder/hooks.ts · HOOKS`** — every hook declared once (channels, payload, executor-made); `hooksOn(channel)` drives channel routing and the deferred tier's taps, `CompositeRecorder`'s fan-out is generated from `HOOK_NAMES`, and one `fire` isolates every recorder error (scope → `onError`, flow → dev warning; R10: a throwing `onResume` no longer rejects `resume()`). A hook on an interface but not in `HOOKS` does not compile (`test/architecture/hook-registry-compile.test.ts`). Detection accepts class-prototype methods (blocks only Object.prototype — CombinedRecorder.ts:266). New built-in = compose a store as a field (no base classes) + factory in src/recorders.ts. `toSnapshot()` bundles are REBUILT field by field into `RecorderSnapshot` (`recorder/snapshot.ts · copyBundle`, called by `runner/snapshot.ts · collectRecorderSnapshots` — so a recorder can't spoof its own `id`); anything not on that copy list is dropped silently — the free-form slot is `meta` (facts about the bundle: which projection, what it omits — what a consumer BRANCHES on; prose goes in `description`). A new bundle field means editing the copy list AND the three `toSnapshot?()` signature sites (scope/types.ts:159, engine/narrative/types.ts:462, recorder/CombinedRecorder.ts:121).
- **Scope $-method (4-file checklist)**: ScopeFacade capability + ReactiveTarget (reactive/types.ts:20); ScopeMethods type (:74); METHOD_ROUTES (createTypedScope.ts:46); SCOPE_METHOD_NAMES (reactive/types.ts:262) — miss #4 and the name becomes a state key.
- **Contract**: builder.contract() (:1056) → makeRunnable toOpenAPI/toMCPTool (runner/RunnableChart.ts:83/153). BYO schema is duck-typed (schema/detect.ts:24).
- **Scope provider**: ProviderResolver via registerScopeResolver (exported from /advanced), or a custom scopeFactory. Runtime ownership is explicit in `scope/runtime.ts`: `registerScopeRuntime(scope, { target, handlesAssignments, setBreak? })` maps the user object to its engine port without probing its fields. ScopeFacade, TypedScope, attachScopeMethods and canonical Zod factories auto-register. Other executor/decide/select scopes must register; no duck-typed fallback. One facade per stage owns recording, redaction, argument ownership and commit sealing; adapters use its path-aware accessors instead of writing directly to StageContext.
- **Detach driver**: `detach/types.ts · DetachDriver`; no driver registry — passed explicitly. The three batching adapters delegate to `detach/drivers/batch.ts · createBatchSchedule`, the one owner of queued work, dispatch, settlement and handle-registry cleanup. A scheduling failure returns a failed handle, never a stuck queue or a parent exception. `validate()` is explicit caller preflight, not an automatic entry-point hook; do not use it instead of containing failures in the driver. No implicit fallback or retry.
- **Observability dial** (pattern): capture/ primitive + FlowChartExecutorOptions option + snapshot discriminant — FOUR dials now: readTracking/writeTracking/commitValues/writeProvenance (#P1: `'reads-prefix'` stamps `TraceEntry.readKeys` = keys tracked-read before each write; consumed by causalChain `edgeAttribution: 'per-write'` and sliceForKey's default). Propagation is the same 6-site pattern for all four.
- **Worked example of a new FLOW hook (9.15.0 `onStageRetry`)**: `FlowStageRetryEvent` + `FlowRecorder.onStageRetry?` + `IControlFlowNarrative.onStageRetry` (engine/narrative/types.ts) → `FlowRecorderDispatcher` impl → `NullControlFlowNarrativeGenerator` no-op (interface is REQUIRED there) → **its `HOOKS` entry (recorder/hooks.ts)** → `NarrativeFlowRecorder` sentence (the 8 strategies extend it, so they inherit free) → `CombinedNarrativeRecorder` + `CombinedNarrativeEntry.type` + `NarrativeFormatter.renderRetry` → barrels (the composite fan-out is generated). Retry is the only flow event that fires DURING a stage, so the narrative BUFFERS it via `bufferOp` (the `onEmit` precedent) — pushing it straight to the store would place the line before its own stage header.
- **Closed seams that look open**: ExecutionEnv (fixed type); decide() operators (evaluator.ts:19 module-private); METHOD_ROUTES; the executeNodeStep phase chain; emit dispatcher (rides scope channel).

## Change-impact map
- **Nested diagnostic writes** → `memory/utils.ts · isDeniedWritePath` checks the entire address/path/field before either nested writer creates containers; `SharedMemory` asks it before copying a generation. It uses the existing `pathOps · isDeniedSegment` rule. A denied selector is a no-op; allowed payloads may still contain those names as data. Missing properties are created as own data properties without invoking inherited setters, and updates never merge inherited final values. Diagnostic updates keep their duplicate-preserving array concatenation and shallow object merge, not the record's union-merge law. The planned extraction keeps this one implementation on `foottrace/paths`, not `/write`.
- **Trace verbs** (`set|merge|append|delete`, `memory/types.ts · TraceEntry.verb`) → ONE law, `memory/verbs.ts` (L1) — F2 deleted the three replicas. `applyVerb` is the ONLY function with an arm per verb (a fifth verb in the union stops compiling there; `TRAITS` — `isTotal` / `recordsTail` — says what a verb IS, pinned against the step), `placeVerb` puts its value or removes the key (`ABSENT`), and two loops fold it: `foldRows` folds ONE bundle's rows into a state with the clone DISCIPLINE as a parameter (`'private'` = the PUBLIC `applySmartMerge`, which keeps its 9.28.0 contract: base deep-cloned, fully detached result — 9.29.0; `'pathCopy'` = the engine's `nextGeneration` — live commit + redacted mirror — and the folds' `applySmartMergeInto` — `stateAt`, `EventLog.materialise`; `'byReference'` = `dryFold`, the admitted record's comparison fold — 9.30.0), and `foldKey` folds ONE path's rows across a log — `commitLogUtils · commitValueAt` (anchored at the last total row) and `slice/elementProvenance · arrayProvenance` (an OBSERVER of that fold: it keeps the births and has no verb switch); causalChain/findLastWriter only match paths. A bundle's merge delta is detached ONCE per bundle (`RecordedPayload`): `deepSmartMerge` dedups an array union BY REFERENCE, so the merge rows of one bundle must see the same objects (a per-row clone made `commitValueAt` double the elements — the 9.29.0 reader bug, fixed here); `foldKey` narrows that copy to its one path. **An unknown verb is REFUSED** (`UnknownVerbError`, public on `/trace`; R2) by `foldRows` (so `applySmartMerge`), `commitValueAt`, `arrayProvenance` — they used to fold it as a `merge`; `time-travel/bundles · bundleRefusal` asks the same `isVerb` and makes it a GAP instead. Pinned: the three old replicas live on as the CONTROL in test/lib/memory/property/verb-law-differential.property.test.ts (kept for the packet's lifetime). The delta encoder keeps VOCABULARY only since 9.30.0 (it picks a verb; its values come from `dryFold` of the stage's 'full' rows — `TransactionBuffer · fullRowsValue`; `replayPathVerbs`/`replayFamilyVerbs` deleted). NOT a second loop (9.22.1): `verbs.ts · supersededByNextSet` — the ONE repeated-path skip (CONSECUTIVE same-path `set` rows: only the last is applied), asked by `foldRows` before its step; its commit-side twin is the last-materialised-path memo in `toChangeOnlyPayload`. Rows are never dropped from the log; a non-consecutive skip is WRONG (a descendant op in between coerces/shells the container) — pinned by test/lib/memory/scenario/repeated-path-byte-identity.test.ts. Since 9.23.0 the clone in that memo loop (and every `structuredClone` in `toDeltaPayload`) is THE clone — the patch trees hold references until commit; no verb switch was touched.
- **Write address** → ONE constant, `runAddress.ts · RUN_NAMESPACE` (an L4 leaf, C4), read by `runAddress` / `useAddressOf` and by `RedactionRule · verdictOfRead` / `retainState`; the frame's `RecordFrame` holds the address (C3). `SharedMemory` and the nested writers take the address as data and refuse a non-array; `TransactionBuffer` takes it too (C2, 9.45.0). `test/architecture/write-address.test.ts` refuses the namespace below L4, as a literal or a property — no exceptions since C4.
- **StageContext.commit / dials** → ONE owner since 9.35.0 (F5): `memory/runPolicy.ts` — a frozen `RunPolicy` (the four dials + the `RedactionRule` + the mirror flag) built ONCE by `FlowChartExecutor · createTraverser` (run AND resume), handed to `ExecutionRuntime`, held BY REFERENCE by every `StageContext` (`inheritRun` in createNext/createChild) and by its `RecordFrame` as its encoding (`useEncoding`, through `StageContext · adoptPolicy`, the one place a frame's policy changes) and passed by `SubflowExecutor · executeSubflow` to the nested runtime BEFORE the seed. A fifth dial = the policy type + its consumer. The old per-dial `use*`/`get*` and `enableRedactedMirror` were removed in 9.36.0 (no compat shims — owner ruling). `commit()`'s FIRST act (9.23.0) is `materialiseWrites` — the `writeTracking` retained form is taken there, once per key, from `_pendingWrites` (bare references + the write-time verdict); anything that reads `_stageWrites` goes through `retainedWrites()` (a non-consuming fold, so a mid-run snapshot is honest), and `discardStaged` drops the pending map with the rest.
- **runtimeStageId format** (`#`/`/` delimiters) → ONE grammar owner since 9.37.0 (F7): `src/lib/ids/` (L0 leaves: `runtimeStageId.ts` — the record's half, imports nothing: build/parse/read — `parseRuntimeStageId`, `isExecutionKey`, `stageIdOf`, `subflowPathOf`; `reservedIds.ts` — the engine's id doors' ONE refusal, `refuseReservedId` (C6, 9.48.0); `branchSegment.ts` — the `~` grammar). Every parser (ScopeFacade subflow path, subflowResults dual-keying, narrative buffering, store keys) calls it; none re-derives the grammar. R5: every id door (22 at build, plus the run-time dynamic-StageNode door) refuses `#` and `/`; stage ids may contain `~`. `TraversalContext` has ONE constructor (`engine/traversalContext.ts`); `depth` = subflow nesting read off the runtimeStageId on every stamp. Brands `RuntimeStageId`/`ExecutionIndex`/`CommitIdx` on `/trace` (types only).
- **Subflow id prefixing** → ONE prefixer since 9.37.0: `engine/graph/prefixNodeTree.ts`, used by the builder AND the traverser (eager, lazy and `parallelForEach` mounts — lazy/branch roots were unprefixed before 9.37.0, so branch ids collided); change it + stageMap key composition + resume drilling; the builder's eager mounts all go through `FlowChartBuilder · _mountSubflow` (F10). test/lib/engine/branch-segment-prefixer-equivalence.test.ts stays as a guard.
- **Checkpoint shape** (format 2 since 9.44.0, LEAN: only what resume reads + the pause's record — no `executionTree`/`subflowResults`; size independent of run length, pinned by test/lib/pause/checkpoint-size.test.ts; 9.39–9.43 refuse format 2, so upgrade readers before writers) → `runner/checkpoint.ts · buildPauseCheckpoint` ↔ `pause/record.ts · decodeCheckpoint` (one upcaster drops v0/v1 fields) ↔ `runner/resume.ts · planResume` (validation via `pause/record.ts · decodeCheckpoint`) ↔ `ResumeEntry.plan` (the path's mounts + captures + dispatcher continuations, the `pendingPauses` queue; refused up front when unwalkable, ambiguous, or a pending record is not a sibling) ↔ `SubflowExecutor · executeSubflow` resume hop (seed from the capture in place of the mapper's values — the mapper still runs, for the args — FIRST entry only) ↔ `ChildrenExecutor · raisePauses` (fills `pendingPauses`). 9.28.0 adds ONE optional field, `pendingPauses` (present only when parallel siblings wait); the redaction law adds `redactionMarks` (names only, present only when the run marked something); every stored checkpoint enters through `pause/record.ts · decodeCheckpoint` (`checkpointVersion: 1`, one upcaster from unversioned v0 which drops the legacy `continuationStageId`, one validator, one message set; 9.39.0). Pinned: a 9.27.0 checkpoint resumes, test/lib/pause/resume-real-chart-9.27.0-checkpoints.test.ts.
- **Committed-state writers** (copy-on-write, 9.29.0) → anything that writes into `SharedMemory`'s state, a fold's working copy or the buffer's working copy must copy before it writes: `pathOps · ownSpine` for the containers on the path, `ownedRootOf` for the root, `own`/`adopt` for what it created. An in-place edit of a container it did not create edits every generation sharing it — the counted guard stays green while the generations property fails. A new READ path through `TransactionBuffer` goes through `get` (private after the first write) unless it only compares (`peek`); a read the working copy cannot answer is served from LIVE state (`RecordFrame · read`, as on 9.28.0) after `TransactionBuffer · detachBase` gives the diff base a private copy at that path — a private copy of the value handed out instead would change live state wherever 9.28.0 let an in-place edit in (a fork child's read of a parent key). `TransactionBuffer · merge` privatises its base first: `deepSmartMerge` dedups arrays BY REFERENCE, so the identity of the base is part of the bytes.
- **Redaction** (9.19.0; the law restored to owner ruling (a)) has ONE owner of the DECISION, `memory/redaction.ts` (L4 since C4): `RedactionRule` (verdict per user-level path: `clear | whole | fields`) and, per staged write, `decideWrite` → `inheritByIdentity` → (the record writes) → `markStagedWrite` (the marks, only once the write staged — a failed `$update` leaves no mark in `redactionMarks` or the report); the BYTES are the record's (`RecordFrame · write` registers `scrubOf(verdict)`, `memory/scrub.ts · scrubPatch` (L2) writes the log's placeholder at commit; no record file imports `redaction.ts`), carried on the run's `RunPolicy` with the four dials (F5). THE LAW: a policy covers EVERYTHING the library retains or serves — log (both encodings), mirror, stageReads/stageWrites, every recorder event inline and deferred (and the rows built from them), narrative, snapshots, diagnostics, pause payloads, boundary records, logger lines — and NEVER the live heap or the resume checkpoint. The two true exceptions are the caller's own values: the `run()` rejection and the live fork result. It selects by NAME, never by content: state by path (`verdict`); a record handed out whole (root input/output — `FlowchartTraverser.execute`; subflow seed/exit — `SubflowExecutor`; pause payload — `engine/handlers/servedPause.ts`; thrown value — `retainStageError`) by its keys at every depth plus declared `fields` under a key of that name (`retainBoundary` — one decision per PATH via `servedByPath`: copy-on-write per path, the visited set only the current path's ancestor stack, a cycle edge landing on the served copy; a path rule is exact under shared references, a key-NAME rule masks a shared node everywhere); a diagnostic entry as the record `{ [name]: value }` (`retainDiagnostic`; `policy.diagnostics` ADDS diagnostic-only selectors, the only ones that reach flow-message text); a subflow mapper's copy by TAINT (`MapperTaint`, asked by `SubflowInputMapper` for both mappers; the `parallelForEach` items selector counts selected reads via `StageContext · selectedReads` and marks `item`/`into`): a value passed under its own name keeps that name's verdict, an object by reference (or the whole handed record, as fields) keeps its source's, anything computed after a selected read is marked whole (conservative); never value equality across names. A mark is a NAME, run-wide, until the key is deleted — and a pause carries the marks (names only) to the resumed run (`FlowchartCheckpoint.redactionMarks`, checked by `pause/record.ts`). A thrown value is walked with its `cause`/`errors`; an object a mapper passes on by reference is scanned for the selected values it read (planted → fields). Name-only patterns (`needsPath` false — no `.`, lookaround, or escape/class that can match `.`) are decided per object, so the walk shares decisions per (object, targets) — linear on a DAG or a linked history; only a path-dependent pattern walks paths, and past 1,000,000 visits it serves the unvisited remainder as the placeholder (one dev-mode warning), never fails the run. Inside a stage, an OBJECT read under a selected name and written under another keeps its rule (`redaction.ts · inheritByIdentity`, by identity — decided before the write); a primitive or a new object copied across names is selected by its own name only. `StageContext.stageWrite` is the ONE write funnel (facade writes, subflow seed, outputMapper merge-back, resume re-seed) and `getValue` retention asks the same rule; the facade asks it for read/write recorder payloads. An inert rule never enumerates a payload. `emitPatterns` is the one separate scrub (`$emit` payloads, by event name). Two placeholders, historical, one per owner: log/mirror `'REDACTED'` (`placeholders.ts · LOG_PLACEHOLDER`, a record byte), scope-tier `'[REDACTED]'` (`redaction.ts · SCOPE_PLACEHOLDER`). See `docs/guides/scope.md` → "The one law". Capture the retained form at runtime; no second tree walk or recorder-local policy.
- **Diagnostic retention** → `DiagnosticCollector` asks the current run rule before merging/storing: a named entry maps to the state key of its name (the run's keys/patterns/marks/fields, at every depth of the value), then the opt-in `RedactionPolicy.diagnostics` selectors (same path algorithm, rooted at the channel). Its internal retained-write bridge supplies legacy facade emits. Existing writers stay void. Only flow-message description/rationale are payloads (no name — diagnostic selectors only); keys/topology/timing stay present. Dynamic capture uses a per-visit engine flag, not a diagnostic bag. A redacted `logs.deciderRationale` also reaches decision explanations. Checkpoints keep retained diagnostic bags but preserve operational state; no retroactive scrub. See the scope guide's diagnostic contract.
- **Traverser phases** → node-shape reads must use eff* overlay accessors (:646-703) or dynamic StageNode returns break; tail continuations must return ContinuationHops or stacks regrow; decider flat-dispatch must keep InvokerStamp or pause loses its invoker.

## End-to-end trace (seed → decider → loop branch → finish)
build: flowChart() → FlowChartBuilder.start → addDeciderFunction → branch {loopTo} plants stub `next={id,isLoopRef:true}` (:346) → build() → makeRunnable.
run: executor.run (FlowChartExecutor.ts:1455): re-entrancy guard → fresh runId → createTraverser (scope factory composed by `runner/attach.ts · RunObservers · composeScopeFactory`: TypedScope + recorders + redaction) → new ExecutionRuntime (SharedMemory+EventLog+root StageContext) → traverser.execute (:501) fires onRunStart → trampoline executeNode (:738; flat hops, recursion only for forks/subflows/decider-with-next, depth cap 500 counts NESTING per call path — `FlowchartTraverser · nestingDepthOf`, siblings share a level; a reached cap fires onError and rejects run() with `TraversalDepthError`, ChildrenExecutor rethrows it in either mode, 9.40.0).
per stage: executeNodeStep stamps runtimeStageId (:818) → StageRunner.run: scopeFactory → onStageStart → stage fn (writes stage into TransactionBuffer via StageContext.setObject; onWrite fires live) → onStageEnd → traverser calls context.commit() (:1094) → onCommit → onStageExecuted (narrative flushes buffered ops here).
decider: DeciderHandler.prepareDispatch (:89) — runs stage, **commits BEFORE branch resolution** (:124), matches branchId, fires onDecision(+evidence) → onStageExecuted('decider') → flat hop with InvokerStamp.
loop: Phase 6 sees isLoopRef → ContinuationResolver.resolveTarget (:107): id-map lookup, iteration guard (max 1000), onLoop → hop; zero stack, state carries forward (never rewound). A decider WITH its own next runs its branch as a BRANCH FRAME (9.40.0): a `loop`-flagged hop leaves the frame only when an ENCLOSING driver already ran the target — the decider follows it flat and skips its next, which runs once after the non-looping pass; any other jump stays in the frame. A selector's own loopTo resolves the same way and fires onLoop.
end: onRunEnd (throw → onRunFailed; pause → neither) → deferred terminalFlush → getSnapshot() = {sharedState (LIVE view; dev-mode frozen clone), initialState (the fold base), commitLog (detached+frozen; a bundle or base holding what freezing cannot seal — a Date, Map, buffer … — is served as a copy of its open paths, `runner/snapshot.ts · servedSnapshot` → `capture/freeze.ts · serveRecord`, 9.44.2), executionTree, subflowResults (dual-keyed; `globalContext` = the subflow's LIVE heap, or under `redact: true` its own nested mirror — 9.20.0, `servedSubflowResults`), recorders}.

## Backtracking
Seven mechanisms, no state rollback anywhere: **M1** TransactionBuffer staging + net-change commit — commit-on-error by design, explicitly NOT rollback (TransactionBuffer.ts:13-18; error path commits then rethrows). **M2** Pause/Resume checkpointing — the only resume-from-prior-point; TWO raise shapes, ONE checkpoint: `addPausableFunction` (resume runs `resumeFn`) and `interrupt(scope, payload)` (resume RE-RUNS THE STAGE FUNCTION FROM ITS TOP — stages are atomic; the answer returns out of the `interrupt()` call via a WeakMap keyed by the scope), discriminated by `checkpoint.pausedBy`; checkpoint is one detached structuredClone; resume rebuilds the cursor from pausedStageId+subflowPath and re-enters ONCE (9.28.0, `ResumeEntry`): the paused stage's stand-in (its own node, fn swapped — so a dispatcher that interrupted still dispatches) is only where the resumed traversal STARTS — never in a node map — and each subflow on the path takes its capture + entry point on its FIRST entry (a 2+-deep pause re-enters each outer subflow AT the next mount; no pre-mount stage re-runs); every level's entry carries its DISPATCHER's continuation, read from the chart (a branch/fork-child mount hands back to its parent's `next`/join at the parent's level); parallel siblings that paused together wait in `checkpoint.pendingPauses` and are asked in turn, the join after the last; the rest of the run walks the REAL chart (a loop back to the paused stage reaches it and it pauses again; later entries run the inputMapper). Not rebuilt (pinned in test/lib/pause/resume-known-limitations.test.ts): lazy-subflow / parallelForEach-branch pauses (refused), a parallel-branch STAGE's namespace on resume, fan-out error containment for the resumed child. **M3** commit-log replay (EventLog.materialise, commitValueAt — required under delta mode). **M4** loopTo re-entry — forward execution over accumulated state, bounded by maxIterations + dynamicNextHops. **M5** causalChain backward slicing (read-only analysis; honesty flags for untracked reads; `edgeAttribution: 'per-write'` refines edges via `TraceEntry.readKeys` when the writeProvenance dial recorded them — worklist, subset-of-ceiling safe). **M6 (query layer)** slice/ — variable-first triage: sliceForKey + append-fold element provenance + the ONLY safe serializations (sliceToJSON/formatSlice — never JSON.stringify a slice root). **M7 (read-time cursor)** time-travel/ — `timeTravel(snapshot)` walks stops over the finished log with `stateAt()` folding M3's replay at each one (base = `RuntimeSnapshot.initialState`, which now travels with the log); it is a READER, never a second live cursor, and `drill(mount)` opens a separate cursor over a subflow's own isolated log. 9.18.0: `timeTravel([paused, resumed])` chains a cross-executor M2 resume into ONE axis — the resumed leg's `initialState` (= the state at the pause) restarts the fold, commit indices stay run-local (`Stop.sourceIdx` names the leg), and the chain is refused unless the never-reset execution counter is monotonic across legs. Deep dive + step tables: [.claude/rules/backtracking.md](.claude/rules/backtracking.md).

## Invariants (assumed, not stated)
- **The admitted record (9.30.0)** — a commit's bundle, replayed onto the state its stage began from, gives back the stage's read-your-writes view (its working copy, by `deepEqual`, as a record can hold it — through `structuredClone`, so an `Error`'s dropped own fields never count) at every path it touched and at every container and array slot its writes made on the way there, below the stage's address (`runs/<id>` is where a namespaced stage writes, not a value it reads). VERIFIED for every commit that staged a `merge` or a nested op (`TransactionBuffer · admit` → `admission.ts · lossyFamilies`: one `dryFold`, no clone, families of TOUCHED paths compared at their roots); a family that does not fold back is re-encoded as `set` rows of the read-back at its last touch (`emitReadBack`). BY CONSTRUCTION for a stage of `set`/`delete` only, directly under its address (no fold). Every commit that already folded back keeps its bytes. NOT covered: an in-place edit of a value the stage read but never wrote at or above (M2 — still lost), and readers (`commitValueAt`) that disagree with the fold. Pinned by test/lib/memory/property/record-equals-read-back.property.test.ts (independent oracle) and, end to end on both engines, by the witness in property/copy-on-write-fixture.ts.
- Committed state immutable-after-swap — a generation is NEVER edited, and since 9.29.0 generations SHARE every untouched subtree (copy-on-write). First-touch views AND the transaction buffer's net-change diff base hold bare references on that guarantee — an in-place mutation of a committed container corrupts every generation that shares it and blinds the diff. Every writer copies before it writes (`pathOps · ownSpine` / `ownedRootOf`); pinned by the generations property in test/lib/memory/scenario/copy-on-write-commit.test.ts and the counted guard in test/lib/memory/boundary/commit-cost-independent-of-state.test.ts.
- Reads are borrowed live references (recorders get them un-cloned) — never mutate.
- The commit log is never the reader's to edit (9.44.2, `capture/freeze.ts`): `freezeRecord` freezes each bundle (`EventLog · record`) and the fold base, flags one holding a Date/Map/Set/RegExp/Error/buffer/view (what freezing cannot seal) and stores a view over a slice of a bigger buffer as its own bytes; `serveRecord` serves a sealed record as itself and an open one as a copy of its OPEN PATHS only (the containers on the way to each such value, mapped at the first serve — no spread, a throw leaves the record flagged; every other part shared; an own `"__proto__"` stays DATA via `putOwn`). Doors: `getSnapshot().commitLog` / `.initialState` (`runner/snapshot.ts · servedSnapshot`) and `EventLog.list()` (a new array per call). The engine's own snapshot (`ExecutionRuntime · getSnapshot` via `EventLog · recorded`, taken once per subflow mount) is NOT served — so the run path serves nothing; every walk iterative (pinned 20,000 deep). NOT yet covered — known open doors, follow-up: the served-surface law, designed with the record-frame clean-up (C3/C4): the engine's own snapshot on `/advanced` (its bundles and fold base; `executor.getRuntime()`, `EventLog.recorded()`), subflow results (`history`/`initialState`/`stageContexts`), the execution tree's `stageReads`/`stageWrites`/diagnostics/flow messages, recorder rows, `getCheckpoint()`, `getNarrativeEntries()`. Pinned: test/lib/memory/scenario/served-record-doors.test.ts + the property beside it.
- executionIndex globally monotonic per run; continues across resume. The FIRST CommitBundle per runtimeStageId is the stage's own: no `phase`, carries its tags. Only a later bundle of the same execution is a continuation, stamped by the WRITER (`phase: 'exit'` = a mount's exit after its merge-back; `phase: 'repeat'` = a fork child's fan-out settle), so a mount with no merge-back (lazy, `parallelForEach` branch, no `outputMapper`) is one bundle. Readers group by runtimeStageId and read `phase`, never the log's shape (9.39.0, F8); a pre-9.39.0 log is inferred only by `inferLegacyPhases` (on `/trace`), only without a tree; tree-less, a `subflowResults` entry marks a mount. Empty commits are deliberate cursor stops.
- runtimeStageId stamped BEFORE the stage runs — the entire event-correlation model rests on scope+flow events sharing it.
- One executor = one run at a time (_isExecuting guard). State values must survive structuredClone.
- A resume re-enters ONCE (`ResumeEntry`, 9.28.0): the stand-in carries the paused stage's id but is only a START node — every graph lookup (node map, loop targets, subflow roots) must go through the real chart, never through it; a pause-path capture seeds its subflow's first entry only; what runs after an entry is read from the chart, never from the checkpoint. A paused-and-resumed run equals the never-paused run — same trace, same final state — for linear, decider-branch, selector-branch and fork-child placements at every depth and with parallel siblings pausing together (fast-check property, test/lib/pause/resume-real-chart.property.test.ts). Qualified: the RECORD differs by design (the stand-in is one more execution of the paused stage; the loop budget is per leg), and the three pinned limitations above hold.
- Loop-ref stubs deliberately VIOLATE node-id uniqueness — every graph search must skip `isLoopRef` first.
- runId regenerates per run() AND per resume(); recorders detect new runs via traversalContext.runId (engine never resets them).
- Recorder errors never abort traversal (isolated by `recorder/hooks.ts · fire`; every recorder `ErrorEvent.error` is a `StructuredErrorInfo` built once by `recorderFailureEvent`, inline and deferred alike; `errorInfo` lives in the L0 leaf `src/lib/errors/`).
- Deferred-observer capture default at the executor tier is **'clone'** (DeferredObserverTier.ts:167), not 'summary'.
- `getSnapshot().recorders` = ONE row per recorder **id**, across both channels and both delivery tiers (`runner/snapshot.ts · collectRecorderSnapshots`) — a shared-name hook (onError/onPause/onResume) puts one recorder on two lists by design, and rows must survive structuredClone (the narrative row drops `rawValue`, a live reference).

## Landmines
1. **NOT a landmine any more (9.24.0), recorded because it was one for eight releases:** TypedScope's set trap used to JSON-round-trip the ASSIGNED VALUE of every object write (Date→string, Map→{}, own undefined dropped) while `$setValue` did not — two write paths, two byte shapes. The round-trip existed only to keep the scope's own Proxies out of the buffer; `reactive/handles.ts` recognises them now (O(1)), so `writeTraps.ts · sinkSetTrap`, `arrayTraps.ts` (index set, `MUTATING_METHODS` arguments, element-proxy leaf) and `createTypedScope · assignStateKey` hand the value to the sink AS ASSIGNED and the buffer's `structuredClone` at commit is the one detaching step — both doors store the same bytes (pinned: test/lib/reactive/scenario/assigned-value-only.test.ts, a fast-check property over `fc.jsonValue()`). Consequences to keep in mind: (a) a value nothing can clone — a function, a foreign Proxy such as a framework's reactive object — now fails the stage at COMMIT through the trap too (it always did through `$setValue`; the trap used to JSON-copy it silently, dropping the function); (b) `s.k = o; o.x = 1` commits `x: 1` — what the stage reads back — the same law 9.23.0 gave `$setValue`, and since 9.30.0 (owner ruling R1, the admitted record) so does `$update`: an in-place edit of a merged value within its stage is recorded; (c) the shape work the round-trip did for free stays explicit: `delete arr[i]`, a write past the end and `length` growth spell holes as `null` (`arrayTraps` · `fillHoles`). The surrounding state reaches the buffer by reference and is detached with `structuredClone` at commit (9.22.0 / 9.23.0).
2. Fork "parent breaks when ALL children broke" is implemented (ChildrenExecutor.ts:53) but **unwired** — every live call site passes parentBreakFlag=undefined; fork breaks do NOT propagate in real runs.
3. **First bite CLOSED (9.23.0 — clone once at commit):** `TransactionBuffer` · `set` now stores the REFERENCE in `overwritePatch` too (it always did in `workingCopy`) and the record's copy is taken ONCE per surviving path when the payload leaves the buffer (`toChangeOnlyPayload` / `toDeltaPayload`); `StageContext` · `trackWrite` holds the reference + verdict in `_pendingWrites` and `materialiseWrites` takes the retained form once per key at `commit()`. So `$setValue(k, o); o.x = 1` commits `x: 1` — what the stage read back — instead of a stale write-time snapshot (pinned: test/lib/memory/scenario/clone-once-at-commit.test.ts). The law "the record never aliases a caller's object" holds at the commit boundary; a mutation AFTER commit reaches nothing. Two things to keep in mind: (a) the engine's OWN nested ops (`set a` then `merge a.b` — the buffer driven directly; the proxy writes root keys) must not edit through a shared container, because only `workingCopy` receives a nested merge's result — `detachHeldAncestors` clones the held ancestor in `overwritePatch` first (the design page missed this; the `set-merge-interleaved` 9.22.0 reference pins it); (b) an UNCLONEABLE value (function, foreign Proxy) now fails at COMMIT, not at the write — the run still fails loudly with the `DataCloneError`, the stage cannot catch it, and none of that stage's writes land. The scope's OWN proxies are not uncloneable values at the boundaries the library owns (9.23.3, `reactive/handles.ts`): a handle handed to the fan-out selector, a subflow mapper or `$setValue` is swapped for the value behind it first — a handle buried inside a hand-built value elsewhere (an `interrupt` payload, a `$emit` payload) still fails at the clone. The element-write loop's other O(N)-per-write cost, the array commit callbacks' whole-array JSON round-trip, went in the same release (landmine 1, second moved behaviour); what remains per element write is the array proxy's own shallow copy-on-write (`arrayTraps` · `replaceInElement` / the traps' `[...getCurrent()]`, ~9 µs at N = 10k) — bench 1k 5.7 ms, 10k 104 ms. **Second bite — a raw element held past its stage:** (within ONE stage, 9.29.0: a raw read BEFORE the stage's first write is the committed object itself — edited in place and written back it records NO change, because the diff base is that object (M7; dev mode warns, `StageContext · warnOnCommittedMutation`); a raw read AFTER the first write is the stage's private copy, `TransactionBuffer · privatise` — option D, 9.28.0's behaviour; one the working copy cannot answer (a key the stage deleted) is LIVE state, as on 9.28.0, with the diff base detached there first, `TransactionBuffer · detachBase`, so a write-back is recorded as it was.) A value obtained through `find`/`filter`/`for…of`/`$getValue`/`$read`/`$toRaw` in stage A and mutated in stage B is the committed object itself, so the mutation edits committed state with no row, and the dev-mode guard (`StageContext` · `warnOnBorrowedMutation`) sees it only if B also READS that key. Cannot be trapped; documented in src/lib/reactive/README.md ("A handle is bound to its stage"). A PROXY held that long is refused at the write (`ScopeFacade` · `assertLive`, sealed by the commit observer — the refusal lives on the facade, not `stageWrite`, because the frame's post-commit re-use is what the engine's double-commit paths need).
4. **NOT a landmine any more (9.22.0), recorded because it was one:** an indexed array read (`arr[i]`) used to hand back the RAW element, and a nested array write committed with the `merge` verb whose array arm is a set UNION. Both are fixed at the root — `reactive/arrayTraps.ts` wraps elements, and a nested array commits as a `set` of its ROOT KEY (`createTypedScope` · nested array commit callback). Consequences to keep in mind: an array ASSIGNMENT now REPLACES at every depth (`$update` is still the append), and a nested array write appears in `bundle.overwrite`/verb `set`, not `bundle.updates`/verb `merge`. What the proxy still cannot see — an element reached via `find`/`filter`/`for…of`/`forEach` — is REPORTED loudly instead: `StageContext` · `warnOnBorrowedMutation` (a dev-mode WARNING at the frame's first commit, under `readTracking: 'full'` only — a report after the fact, not a refusal). `memory/equality.ts` · `equalPairs` has one arm per value KIND (`capture/valueKinds.ts · kindOf`, the one classifier — every kind `structuredClone` keeps since 9.44.2; before it, a replaced RegExp/Error/boxed primitive/buffer/view/Blob was a no-op) so a `$setValue` of a new Date/Set/RegExp is a change, not a no-op; an opaque value (Blob) compares by identity in the replacement check and as equal between two COPIES of one record (`OpaqueRule`: the writer rule, element provenance, the borrowed-mutation guard, admission). Every proxy (nested/terminal/element) READS LIVE through `reactive/liveView.ts` — a held handle sees its own writes; it answers from the captured object only when the path is gone. See src/lib/reactive/README.md. **Named, not fixed (out of contract):** a cyclic self-reference reached through a terminal proxy commits array/element writes to the WRONG place (row recorded, fold agrees, value lands beside the cycle edge); a top-level key containing U+001F (the path separator) throws at the seed; an expando on an array (`arr.foo = 1`) is dropped by the array proxy's set trap (indices and `length` only).

## Pointers
- [.claude/rules/backtracking.md](.claude/rules/backtracking.md) — the 5 mechanisms with step tables + pseudocode
- [docs/guides/](docs/guides/) (error-handling, observers-deferred) · [examples/](examples/) — mandatory integration tests (Convention 2); 7 test types per feature (Convention 3)
- Build/test: `npm run build` (CJS+ESM+types), `npm test`
