Liking cljdoc? Tell your friends :D

Level 3 (full owned-tree elimination) — CONVERGED execution plan (2026-06-08)

Owner GO'd "Level 3: full no-residual". This is the multi-week, data-loss-critical, IRREVERSIBLE campaign to eliminate the OWNED in-memory trie representation. Authoritative reference; supersedes slice3-f5-f7-{execution-plan,revalidated-2026-06-08}.md for ordering. Baseline a46d9c1 / e44a877 (2713 green).

Process (owner directive): plan → red-team → refine until convergence → +1 confirming red-team → implement BY HAND, each step green + committable. The 8 data-loss-critical steps (flagged ⚠️RT) each get a dedicated adversarial red-team BEFORE implementation. Implementation is by hand (plan/explore/red-team agents allowed; no work delegation).

REFINEMENT R1 (confirming red-team af13fe7 — 2026-06-08): L0.3 RETRACTED; OR-lock collapse → L3.3

The +1 confirming red-team on the converged L0 found a BLOCKER the prior passes missed: the owned &self WRITE mutators were never enumerated. L0.3 (collapse OR RwLock → bare field) CANNOT happen at Level 0 — RETRACTED:

  • byte insert_impl_core/remove_impl_core + dirty_tracking clear_dirty_flags_recursive/propagate_dirty_to_root, and char try_insert_impl_no_wal/_with_value/try_remove_impl_no_wal/preflight_existing_terminal_is_final all take self.root.WRITE() through &self (the OR lock's raison d'être). They are LOAD-BEARING (compaction staging via insert_impl_no_wal until L2.1; reestablish/recovery/staging until L3.x); the plan deletes them only at L2.2/L3.3.
  • A bare field cannot express &self writes (won't compile); removing the lock is a data race — commit_document is now &self (e44a877), so an Arc embedder can kill_switch_to_owned + concurrently commit_document[owned write] + contains[owned read] + enable_eviction[owned evict], serialized today ONLY by OR. persistent_lockfree_f4_lock_ hierarchy_loom.rs EMPIRICALLY proves OR serializes a concurrent insert_impl_core.

Resolution: the OR RwLock + owned root field persist UNTIL L3.3 deletes the field OUTRIGHT (after every &self owned writer/reader is gone via L2.2 + L3.3). NO intermediate bare-field state — "collapse" == "delete at L3.3". L3.3 additionally deletes clear_dirty_flags_recursive/propagate_dirty_to_root and must RETIRE/REWRITE the loom suite (its OR-lock premise evaporates). Level 0 is now ONLY L0.1 (L0.2 MOVED to L3.2 — see below), with these corrections:

  • L0.1 commit must ALSO, in the same commit, migrate the owned-eviction tests: src/persistent_artrie_char/ eviction_registry_tests.rs; the persist.rs ~2725 region; the owned-arm assertions in tests/persistent_char_eviction_ {correspondence,proptest,registry_correspondence}.rs (re-assert vs the overlay registry / drop the kill-switched- owned-evict scenario); confirm tests/persistent_artrie_loom_correspondence.rs:1748 is comment-only. DONE+COMMITTED (5aa6fc3).
  • L0.2 ROLLED BACK + MOVED to L3.2 (2026-06-08). The original L0.2 (delete owned READ tails) was UNSOUND: it assumed the owned read arm was reachable only via kill_switch_to_owned, but in-memory ::new() tries (a PUBLIC API) are permanently route_overlay()==false and use the owned read path — collapsing the reads broke 76 tests (44 basic ::new() char_integration tests, ZERO kill_switch). The read-tail deletion is causally DOWNSTREAM of the L3.2 ::new()→overlay flip + struct-zipper re-point, so it is now FUSED into L3.2. Full retrospective + the rejected "flip ::new() early" Strategy-B viability analysis: docs/design/slice3-l02-rollback-2026-06-08.md.
  • L0.1 is RT-CLEARED (mechanical). The OR-lock collapse + the former L0.2 read-tail deletion both live at L3 now (L3.3 / L3.2 respectively) and inherit L3's ⚠️RT.

5 ground-truth corrections (each load-bearing for ordering)

  1. Normal reopen is ALREADY overlay-drained. open_inner production arms use convert_owned_to_overlay_on_reopen (Owned regime) / reconcile_and_drain_overlay (Overlay regime) — neither touches self.root. The owned replay_records_lww + reestablish_overlay_from_owned survive in open_inner ONLY on the open_with_legacy_loader (force_f5==false) oracle branch. So L1 redirects the CORRUPTION/ARCHIVE-rebuild ctors, not "reopen".
  2. An overlay→dense serializer EXISTS but is NOT path-compressing. serialize_overlay_node_to_disk (byte overlay_checkpoint.rs / char persist.rs serialize_char_node_to_disk) is the shipping production overlay-arm checkpoint — one owned Node/CharNode record per overlay node, UN-compressed. owned serialize_root emits path-COMPRESSED images. C-opt-1 (L2) is a NEW path-compressing serializer; density is compact()'s whole purpose.
  3. 4th load-bearing owned consumer: the struct zipper. PersistentARTrieZipper / …CharZipper (zipper.rs has_path/is_final_at_path/get_children_at_path walk inner.root.read(), log::warn! stub). Passes the formal gate today only because zipper_language_correspondence exercises it on a ::new() in-memory trie (route_overlay()==false). L3 must re-point it onto the overlay before deleting the owned root.
  4. kill_switch_to_owned has exactly ONE production caller (compaction_impl.rs:209) + ~80 TEST callers (owned-white-box). So OverlayWriteMode deletion (L2) is entangled with retiring that owned-white-box test corpus.
  5. Owned NODE record types are NOT deletable, even at L3 — Node/Node4/16/48/256/StringBucket (nodes/mod.rs:576, bucket.rs:287) + CharNode/CharNode4/16/48 (nodes/mod.rs:237) ARE the on-disk record format (serialize_overlay_node_to_disk + load_overlay_node_from_disk consume them). Only the in-memory root HOLDERS — TrieRoot (dict_impl.rs:484), CharTrieRoot (types.rs:753), CharTrieNodeInner (types.rs:468) — and the owned methods over them are deletable. L3's codec drives the Node-record format DIRECTLY from overlay nodes, never materializing a TrieRoot/CharTrieNodeInner.

OPTIMAL ORDERING (the central decision): L0 → L1 → CX → L2 → L3

Reorder vs naive 0→1→2→3: build the codec primitives as a DORMANT sub-phase (CX) AFTER L1 and BEFORE the L2/L3 flips, because (a) C-opt-1/C-opt-2 are the real new format work + prerequisites for BOTH L2 (owned-staging removal) and L3 (owned-root deletion); landing them dormant lets the byte-identity proof land in isolation before load-bearing; (b) L1 is codec-independent (reuses the shipping apply_recovered_operation_overlay/drain). Throughout: #41 checkpoint_lsn=committed-watermark capture ordering (core/overlay/checkpoint.rs overlay arm) is UNTOUCHED.

Per-step gate (every commit): full suite (feature-on default) + --no-default-features (feature-off) + doctests + scripts/verify-formal-correspondence.sh exit 0 + scripts/verify-unsafe-boundary-inventory.sh set-equality + fmt + cross-repo READ-ONLY cargo check (liblevenshtein-rust; and at L3.3 libgrammstein/lling-llang/pgmcp). Keep verification LOGS to summaries (don't commit 25K-line test logs).


LEVEL 0 — runtime deletion (computed route_overlay; OR-lock collapse; eviction owned arms)

route_overlay() STAYS a computed fn (red-team #2: const true breaks compaction staging). A-decision: KEEP OverlayWriteMode at L0 (deleted at L2). Hand-delete every route_overlay-false owned arm (the compiler will NOT flag them — if route_overlay(){return X} <tail> borrow-checks the tail), then collapse the OR lock.

  • L0.1 — delete eviction owned else-arms (mechanical, RT:no — already dead under production route_overlay==true). Byte shared_trait_impl.rs start_eviction async cb (~291-317) + force_eviction (~392-401) → keep only the overlay arm; char mod.rs start_char (~2141-2145) + force_eviction (~2231-2235) → drop the evict_char_nodes else + unused quiescence locals. Now-dead → delete: byte evict_node_at_path (+find_parent_in_root), char evict_char_nodes + evict_node_at_path (+inline relink). KEEP vocab evict_node_at_path (persistent_vocab_artrie/mod.rs:767/869 — distinct type).
  • L0.2 — ❌ ROLLED BACK + MOVED to L3.2 (2026-06-08; was "delete route_overlay-false owned READ tails", RT:no). UNSOUND at L0: in-memory ::new() tries are permanently route_overlay()==false (public API) and use the owned read path, so deleting the read tails before the L3.2 ::new()→overlay flip broke 76 tests. The read-tail deletion (char try_contains/get_value/try_get + iter_prefix*; byte iter_prefix_from_cursor + collect_terms_from_cursor) is now FUSED into L3.2. KEPT-as-was (D1 seams): byte contains_impl/get_value_impl + unrouted_; char owned_try_/owned_root_guard/ navigate_to_prefix_from. Retrospective: docs/design/slice3-l02-rollback-2026-06-08.md.
  • L0.3 — collapse OR RwLock→bare field ⚠️RT (data-loss-critical: lock-collapse soundness). root: RwLock<TrieRoot> → TrieRoot (dict_impl.rs:280); char RwLock<CharTrieRoot> → CharTrieRoot (mod.rs:429). Surviving self.root accesses after L0.1/L0.2: (i) ctor/reopen scratch (f5_loader get_mut, &mut); (ii) D1 converter seam readers; (iii) byte compaction staging (insert_impl_no_wal/serialize_root/capture_owned_snapshot); (iv) recovery appliers; (v) struct zipper; (vi) reestablish_overlay_from_owned. Rewrite each: &mut self → &mut self.root; &self read (byte contains_impl/get_value_impl, char owned_root_guard) → direct &self.root borrow (owned_root_guard return type MappedRwLockReadGuard → Option<&CharTrieNodeInner>). Soundness: every surviving access is &mut self (single-writer) OR a &self read on a rep only reached by single-threaded reopen/recovery/compaction (production never writes owned). Lock order after OR removed: CK > merge_lock > EC (no cycle); Send/Sync OK (SwizzledPtr = AtomicU64+AtomicPtr). Gate adds the loom suites. Red-team focus: prove NO surviving &self self.root read can race a &mut self owned writer in ANY config (Shared wrappers, kill-switched owned, in-memory zipper).*

LEVEL 1 — recovery redirect (S5′) ⚠️RT

Point the corruption/archive-rebuild ctors at the overlay drain, eliminating the apply_*_recovered_operation_no_wal + reestablish_overlay_from_owned dependency. Reuse the SHIPPING apply_recovered_operation_overlay (flip.rs:1031) — red-team #1 proved it subsumes counter semantics incl. u64>i64::MAX (counter_leaf_to_i128) + delta ACCUMULATE.

  • L1.1 byte open_with_recovery_config (mmap_ctor.rs:935-1027): apply closures 938/977 → apply_recovered_operation_overlay; DELETE reestablish_overlay_from_owned (1021-1027). Verify watermark base-seed covers drained LSNs.
  • L1.2 char open_with_recovery_config (mmap_ctor.rs:1082-1343, apply :1166 + reestablish :1337), recover_from_archives (:1531-1601, :1573 + :1592), open_with_full_recovery (:1403). Same transform + delete reestablish calls.
  • L1.3 retire the legacy-loader oracle's owned dependency (migrate open_with_legacy_loader to overlay-built correspondence or delete + re-point the both-loaders/owned-to-overlay suites to production reopen + BTreeMap oracle), THEN delete owned replay_records_lww + apply_*_recovered_operation_no_wal + recompute_recovered_increment + value_from_recovered_i64 (prove unreachable first). KEEP *_impl_no_wal (staging/reestablish; die L2/L3). UNSAFE rows 23-24 live in KEPT *_impl_no_wal → NO prune at L1 (re-run set-equality to confirm 0 delta). Red-team focus: recover-via-drain $\equiv$ recover-via-owned-then-convert across V $\times$ archive-layout $\times$ crash-point; the recover-family uses rebuild_from_wal_segments_regime_aware (its own tx-resolution) — verify SAME tx-filter the owned path applied (else aborted-tx records resurrect). Perf: bulk overlay path-copy rebuild vs owned dense. New TLA RecoveryRebuildOverlay (archive-rebuild-into-overlay sink + tx-filter parity).

SUB-PHASE CX — build + PROVE the path-compressing overlay↔dense codec (DORMANT, reversible) ⚠️RT

  • CX.1 byte serializer serialize_overlay_snapshot_compressed: walk the immutable overlay root, emit the SAME dense Node-record format serialize_root produces (collapse single-child chains → compressed prefixes; leaf runs → StringBucket; ROOT_TYPE_BUCKET vs ART_NODE per the owned heuristics) via serialize_node_to_disk_with_value_len. Do NOT touch serialize_overlay_node_to_disk (the un-compressed production checkpoint — STAYS). Proof: byte-identity (or reopen-equivalence incl. compacted_bytes density bound) vs owned serialize_root over V $\times${valued,term-only,""}$\times$deep key.
  • CX.2 byte loader load_overlay_root_compressed: read the dense path-compressed format, EXPAND multi-unit prefixes + buckets directly into Arc<OverlayNode<ByteKey,V>>, never materializing TrieRoot. Reuse load_overlay_node_from_disk (single-node). Proof: load(serialize(overlay))$\equiv$overlay AND load_compressed(legacy_owned_image) $\equiv$ build_overlay_root_from_owned(load_root_from_disk(legacy_owned_image)) — back-compat vs the owned loader (the "B2 brick-risk" mitigation: prove BEFORE it's the only path).
  • CX.3 char twins (serialize_char_*_compressed / load_overlay_char_root_compressed). Same proofs.
  • Prefer ZERO new unsafe (build via Arc/OverlayNode::with_child); any new unsafe → new inventory row + contract. Optional TLA OverlayDenseCodecRoundTrip; correspondence + back-compat tests are the empirical gate. Fully reversible. Red-team focus: back-compat (every legacy on-disk format — bucket/ArtNode roots, all 4 node sizes, compressed prefixes, value blobs incl. "" — read byte-equivalently to the owned loader); deep-term iterative expand; byte-identity/density.

LEVEL 2 — compaction onto the codec; delete OverlayWriteMode / owned checkpoint arm / owned staging

  • L2.1 flip compact() (byte-only; char has no compact()) ⚠️RT. compaction_impl.rs:110-370: replace create-staging
    • kill_switch_to_owned (209) + insert_impl_no_wal loop (231) + checkpoint (264) with serialize_overlay_snapshot_compressed (CX.1) of the source overlay snapshot into the temp file. compaction_snapshot enumeration (already overlay) stays the verify oracle. No owned staging trie / kill_switch / owned insert; density preserved. Red-team: new staging image byte-equivalent + complete; atomic-rename + WAL-sidecar dance unchanged; &mut self exclusivity → no past-snapshot WAL loss.
  • L2.2 delete OverlayWriteMode + kill_switch_to_owned + field + owned checkpoint arm ⚠️RT. route_overlay() body → self.lockfree_root().is_some() (overlay installed iff routing — true for all eligible V; ineligible V never installs overlay). Delete OverlayWriteMode enum/field/seams, kill_switch_to_owned, wal_stamp_owned_regime, the owned checkpoint else-arm in checkpoint_route_split (incl. RES-4 assert), capture_owned_snapshot/publish_owned_and_reclaim + trait decls + byte serialize_root (superseded by CX.1). KEEP overlay arm + capture_overlay_snapshot (#41 untouched). Delete byte staging mutators (insert_impl_no_wal/insert_impl_core/...); char twins survive to L3. Retire the ~80 owned-white-box kill_switch_to_owned test callers as a GROUP (this commit). Prune any byte owned-walk UNSAFE rows + UNSAFE_CONTRACTS entries (set-equality). Red-team: with the owned arm gone, prove by construction lockfree_root().is_some() at every reachable checkpoint (the only counterexample was the kill-switched staging trie, now deleted).
    • ⚠️ FINDING (2026-06-08, Plan + red-team — L2.2 IS NOT INDEPENDENTLY DOABLE; its substantive deletions are FORCED to L3.3). L2.1 is DONE (73fc8eb). For L2.2: eligibility IS universal (overlay_eligible_v()==true ∀V, byte+char), so a created trie is always overlay-routed and the L2.1 owned-staging compaction fallback is dead-for-created-tries. BUT OverlayWriteMode + kill_switch_to_owned + the route_overlay() body + the shared checkpoint_route_split else-arm are SHARED byte/char surfaces: kill_switch_to_owned is the shared LockFreeOverlay default (flip.rs:577) that char's ~20 #[cfg(test)] callers + char's owned-checkpoint coverage NEED until char flips. A "byte-local SHAPE-1" (delete byte owned sinks — compaction owned arm, owned checkpoint arm — while keeping the kill-switch) is BROKEN: kill_switch_to_owned() only flips the mode (NOT lockfree_root), so a kill-switched byte trie writes to self.root then checkpoints/compacts via the owned arm (green guards m4b_old_owned_file_stays_owned_on_reopen, empty_string_valued_owned_regime_reopen). Deleting the sinks while the kill-switch lives = compaction reads the EMPTY lockfree_root (data loss) + checkpoint() Err regresses durability. So byte owned-staging removal MUST land WITH the global kill_switch_to_owned removal — which requires char to flip first.L2.2 is ABSORBED into L3.3 (mirrors L0.2→L3.2, L0.3→L3.3). Corrections to the above L2.2 line for when it executes at L3.3: do NOT delete insert_impl_core/remove_impl_core/upsert_impl_no_wal (LIVE byte writers — die at L3.3 anyway, but as owned-root deletion not "staging"); NO UNSAFE delta at L2.2-scope (rows 23-24 are CHAR types.rs, die at L3.3); serialize_root sole caller is capture_owned_snapshot (NOT persist_to_disk); KEEP serialize_root_value_bytes (used by the live iterative overlay serializer). L2 effectively = L2.1 only; NEXT real step = L3.1.

LEVEL 3 — keystone: reopen-scratch onto codec; re-point zipper; delete owned root field + holder types

  • L3.1 flip F5 reopen scratch onto CX.2/CX.3 ⚠️RT. load_root_immutable_seam (flip.rs:1557) + the F5 Overlay arm → load_overlay_root_compressed (no TrieRoot scratch). Keep corrupt-image→empty+WAL fallback. Red-team: every normal reopen now reads via the new codec with NO owned fallback (the brick-risk; CX back-compat proof is the precondition).
  • L3.2 re-point the struct zipper onto the overlay + ::new()→empty overlay + ABSORB the former L0.2 read-tail collapse ⚠️RT (4th consumer). zipper.rs has_path/is_final_at_path/get_children_at_path (+ char twin): replace inner.root.read() navigation with the overlay-backed root()/DictionaryNode (NodeInner::Overlay; fault OnDisk via fault_overlay_slot). Delete the log::warn! stub. ::new() MUST install an empty overlay so route_overlay() is universally true (this is the PRECONDITION the rolled-back L0.2 lacked — see slice3-l02-rollback-2026-06-08.md + the durability spike below). In the SAME fused commit, collapse the public read-method owned arms (the former L0.2: char try_contains/get_value/try_get + iter_prefix*; byte iter_prefix_from_cursor) — once ::new() is overlay-backed these arms are dead. Update zipper_language_correspondence to not rely on owned. Durability spike (do FIRST, reversible): ::new()→overlay must resolve the WAL-less-write question (flip_to_overlay hard-requires a WAL; insert_cas_durable forbids acknowledging a 0-LSN as durable) — prove a WAL-less in-memory overlay is correctness-equivalent to the owned tree for insert/contains/get/iter/zipper across arbitrary V before this lands. Red-team: the formal gate's PublicDictionaryNodeTraversal + zipper correspondence (wrong overlay-zipper = silent wrong query results); + the ::new()-overlay equivalence across V $\in${(),i32,String,u64}; + no green-gate window where reads are overlay-only while any ctor still yields route_overlay()==false.
  • L3.3 delete owned root field + holder TYPES + owned readers/converters ⚠️RT (biggest risk). Delete: root field (dict_impl.rs:280, mod.rs:429); TrieRoot/CharTrieRoot/CharTrieNodeInner; owned loaders load_root_from_disk(with_arena); D1 seams (owned_first_units/owned_units_under/owned_units_with_values_under/owned_has_empty_term_value/clear_owned); build_overlay_root_from_owned; reestablish_overlay_from_owned; unrouted* + byte contains_impl/get_value_impl + char owned_try_/owned_root_guard/navigate_to_prefix_from/collect_terms_; char owned mutators (insert_impl_no_wal*/ insert_impl_core/try_increment_impl_no_wal) + byte twins; inner_to_overlay/overlay_to_inner. KEEP Node/CharNode RECORD types + single-node serde + load_overlay_node_from_disk + CX codec. compaction_snapshot owned fallback → overlay unconditional. PRUNE UNSAFE rows 23-24 (+ any other owned-walk rows) + UNSAFE_CONTRACTS same-commit (inventory 100→fewer). Red-team: (a) every deleted owned reader/converter has ZERO surviving caller in any feature combo; (b) the codec + L3.1 reopen + L3.2 zipper fully subsume every owned capability across V types; (c) high-concurrency real-disk soak (owned GONE; #41 witness — every committed key survives reopen); (d) the feature-off build.

Formal deltas: L1 RecoveryRebuildOverlay (new); CX OverlayDenseCodecRoundTrip (correspondence mandatory, TLA optional);

L2/L3 re-RUN (not modify) the existing ConcurrentCheckpointSerialization/LockFreeDurableCheckpoint(+Eviction)/ OverlayEvictionCas/OverlayEvictionStale/LockFreeOverlayValueCas (all _Unsafe negative controls must still fire); PublicDictionaryNodeTraversal covers L3.2. Prune UNSAFE rows 23-24 + owned-walk rows at L3.3 (no inventory change before).

⚠️RT data-loss-critical steps needing a dedicated adversarial red-team before implementation:

L0.3, L1, CX, L2.1, L2.2, L3.1, L3.2, L3.3. Mechanical (no RT): L0.1, L0.2.

Critical files

  • core/overlay/flip.rs (route_overlay:348; apply_recovered_operation_overlay:1031; drain_segments_into_overlay:1281; build_overlay_root_from_owned:873; reestablish_overlay_from_owned:946; convert_owned_to_overlay_on_reopen:1482; load_root_immutable_seam:1557; owned_* D1 seams; kill_switch_to_owned:581)
  • persistent_artrie/overlay_checkpoint.rs (capture_owned_snapshot:113; serialize_root:515; serialize_overlay_node_to_disk:961)
    • core/overlay/checkpoint.rs (checkpoint_route_split:134 RES-4 owned arm; #41 ordering)
  • persistent_artrie/compaction_impl.rs (kill_switch_to_owned:209; insert_impl_no_wal:231; checkpoint:264; snapshot fallback:396)
    • persistent_artrie{,_char}/mmap_ctor.rs (recover-family ctors — L1)
  • persistent_artrie/dict_impl.rs (root:280; TrieRoot:484) + persistent_artrie_char/types.rs (CharTrieRoot:753; CharTrieNodeInner:468) + persistent_artrie_char/mod.rs (root:429; eviction owned arms:2141/2231; root():1330)
  • persistent_artrie/zipper.rs (has_path:190/…; L3.2) + persistent_artrie/overlay_fault.rs (load_overlay_node_from_disk:49)
    • persistent_artrie_char/disk_io.rs (load_overlay_node_from_disk:404)

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close