Skip to content

Latest commit

 

History

History
143 lines (79 loc) · 146 KB

File metadata and controls

143 lines (79 loc) · 146 KB

Progress log

(one line per completed milestone; agents read this at session start)

M0.1 done 2026-07-15 — notes: SPEC 00 written; 5 consumers pinned (meilisearch fff2ef5a, heed v0.22.1, arroy v0.6.4, hannoy v0.1.3, cellulite v0.3.2); 61 MUST / 12 SHOULD / 9 WON'T. Human decisions pending before 0.2+: oracle must target the Meilisearch LMDB fork (mdb.master.nested-rtxns) not stock LMDB; M1.9 rescope (nested READ txns in a wtxn = hot-path MUST, nested write = unused); M1.7 DUPSORT unused by all consumers (descope candidate); PLAN gaps in SPEC 00 Findings §B (PREV_SNAPSHOT, WRITE_MAP, try_clone_inner_file, static_read_txn, cursor-mutation ops, get_greater_than/lower_than_or_equal_to). PLAN.md deliberately NOT edited.

M0.2 done 2026-07-15 — notes: SPEC 01 flag matrix written from fork lmdb.h/mdb.c @ cd767228 (heed v0.22.1 submodule pin); 46 rows: 15 MUST / 8 SHOULD / 23 WON'T (20 of the WON'Ts are D-004 dup flags, revived 2.8); zero SPEC 00 mismatches. Semantics notes §S1–S5 for 1.4/1.10 implementers: NOOVERWRITE returns existing value in caller buffer before KEYEXIST; APPEND compares only vs last key, equal = KEYEXIST; PREVSNAPSHOT auto-clears after first commit (milli rollback depends on it). Open question flagged in §S4: fork max key = 511 B constant vs milli's NonZeroU16 assumption — confirm via oracle in 0.3 before Phase 1 fixes the bound.

M0.4 done 2026-07-15 — notes: SPEC 02–06 written (the Phase 1 source of truth): 02 pages (own format, 32-byte header, body-relative offsets, mandatory meta CRC32C over [0,168), env-creation protocol both-slots-txnid-0), 03 btree (full cursor state machine, split/rebalance math, INV-1..21), 04 txn/MVCC (TXN-1..67: single-writer, reader table w/ SeqCst publish-and-verify pin + Arc-swapped published-snapshot object, nested-read quiescence, value-borrow contract incl. contiguous overflow frames), 05 GC (GC-1..28: BE txnid keys, freelist_save bars ALL GC draws — loose/extend only, huge-txn baseline), 06 recovery (REC-1..22: crash-stage table H0–H4, honest sector-tear/CRC mechanics, two-mechanism crash protocol). ADR-0002 (format, Proposed) + amendment (OQ1 BE, OQ2 last_pg). Adversarial spec-review: 7 blockers found & fixed (incl. reader-pin roots race, freelist self-consumption leak, MapFull off-by-one), verify pass confirmed closure; 2 new oracle tests pin put_current+APPEND (flags pass through — APPEND ignores cursor position; SPEC 00 row 33 note pending). D-005 PROPOSED (writer quiescence). Human ratifications pending: ADR-0001, ADR-0002, D-005, REC-2 one-valid PREV_SNAPSHOT policy, REC-22 guard choice. Checks: fmt/clippy/test (25 passed)/miri green; fuzz-quick 188,305 runs clean.

M0.5 ADR DRAFTED 2026-07-15 (NOT done — awaiting human approval per CLAUDE.md rule 6): ADR-0003 heed-integration strategy, Proposed. Recommendation: Option D = standalone heed-zerodb crate (1:1 heed 0.22.1 surface over zerodb native API, re-exporting heed-traits/heed-types verbatim) now, optional upstream-backend-trait convergence Phase 3+. Best A/B story: [patch.crates-io] swaps backends; oracle can link heed(LMDB) + heed-zerodb in one binary. heed test scope defined (28 inline tests: in/partial/out mapped; nested-rtxns.rs example is the M1.9 key). 7 open questions for human incl. D-005 must be resolved before 1.13. NOTHING IMPLEMENTED. Phase 1 blocked until: ADR-0003 approved (this), ADR-0002 approved (format, blocks M1.1), ADR-0001 ratified (oracle heed dep), D-005 decided, REC-2/REC-22 PREV_SNAPSHOT policies ratified.

M0.1 decisions approved 2026-07-15 (Quentin, chat) and applied: oracle = Meilisearch LMDB fork (lmdb-master-sys 0.2.6); M1.9 rescoped to nested READ txns over a wtxn (critical path, ADR-first; nested writes = D-003); M1.7 DUPSORT descoped to Phase 2.8 (D-004, contingent on 0.5 ADR test-suite scope); SPEC 00 Findings §B folded into milestones 1.2/1.3/1.4/1.8/1.10. PLAN.md, CLAUDE.md, DIVERGENCES.md, SPEC 02/03/04, agents, justfile updated in the same change.

M1.1 done 2026-07-16 — notes: first engine code. zerodb-core::page implements SPEC 02 in full — #![forbid(unsafe_code)] (unsafe block count = 0; all field access via safe from_le_bytes/to_le_bytes in raw.rs, no #[repr(C)]). Modules: crc32c (in-house Castagnoli software table per ADR-0002 D1, KATs from RFC 3720 + the 0xE3069283 check value), raw (unaligned LE helpers), header (32-byte common header + generic PageRef classifier/downcast), meta (DBRecord 48B, MetaPage encode/validate/select — validate is the single owner of §3.2's 5-rule predicate returning rich MetaValidity; select implements (txnid[0]<txnid[1]) XOR prev_snapshot over valid slots with typed both/one/none MetaChoice), tree (leaf+branch node-pointer-array + cell-heap: insert/remove-with-heap-compaction/lookup, free-space accounting, max_node_size inline threshold), overflow (head + payload extraction across a run), geometry (page-size validate, map_pages, corrected MapFull next_pgno+n > map_pages, overflow page-count, GC BE-u64 txnid codec). Format locks: §3.5 creation-meta, §4.3 leaf, §4.4 branch transcribed byte-for-byte (all pass); §5.1 overflow payload spans the page boundary. Tests: 15 lib unit + 13 byte-lock (spec02_format.rs) + 5 proptest round-trips (proptest_roundtrip.rs) = 33 new, all green. fmt/clippy(-D warnings, incl. fuzz crate)/test --workspace all pass. Miri (cargo +nightly miri test -p zerodb-core) green: 15 lib + 13 format under Miri; proptest file gated #![cfg(not(miri))] because the proptest harness reads current_dir (Miri isolation blocks it) — deterministic tests cover the same codec paths. Fuzz target fuzz_page_decode added to fuzz/ (psize sweep 4096/8192/65536, forces all 4 type interpretations + full accessor walk); cargo +nightly fuzz build OK, ran 61s = 4,334,391 runs, 0 crashes (10-min gate left to parent). Spec ambiguity flagged (not amended — pending human/REC-2): SPEC 02 §3.2 does not define selection when exactly ONE slot is valid AND prev_snapshot is set; there is no older valid slot to fall back to, so select returns MetaChoice::OnlyOne(single valid slot) regardless of the flag — documented on the enum variant; matches the already-pending REC-2 item, SPEC 06's territory. No new deps. Test-writer pass: +40 adversarial tests (page_edges.rs: page-full off-by-one exactness at 4K/64K, insert-order/compaction fragmentation, meta single-bit rejection matrix incl. [172,psize) non-coverage documenting REC-22 sector-tear reality, selection truth table all 8 combos, 511/threshold flip points, overflow run math ±1; proptest boundary biasing + GC BE ordering property) — zero bugs found. Acceptance gates: fuzz_page_decode 10 min = 19,990,771 runs 0 crashes; fuzz-quick (diff_ops) 10 min clean; miri 169s green. SPEC 02 §3.2 amended with the ratified one-valid+PREV_SNAPSHOT → env-layer Invalid mapping (REC-2†).

M1.2 done 2026-07-16 — notes: env open/close + meta protocol + same-process registry (SPEC 02 §3.2, SPEC 06 §1, SPEC 04 §7). New: zerodb-core::error (heed-shaped Error/MdbError; all open-time failures → MdbError::Invalid per REC-1..5); zerodb-core::env (miri-clean, I/O-free via a Backing trait — the mapped bytes reach it through the trait so selection logic runs on Vec<u8> under miri): reads both meta slots, selects via the M1.1 predicate/select_meta, maps REC taxonomy, EnvInner behind Arc (Env: Clone), process registry HashMap<PathBuf,(id,Weak)> behind a Mutex+OnceLock (EnvAlreadyOpened, id-guarded Drop against the re-register race), prepare_for_closing/EnvClosingEvent (SignalEvent = Mutex+Condvar, fires after the map is released — TXN-53), would_map_full, EnvInfo, PREV_SNAPSHOT plumbed incl. one-valid+PREV → Invalid (REC-2†). zerodb-io (memmap2 added; unsafe block count = 1, in mmap::Mmap::map, SAFETY-commented): Mmap read map, MmapBacking{mmap,file} impl Backing (field order = map unmapped before fd closed), plain-file helpers (create_env_file writes both creation metas + sync_all, positioned read_page/write_page via unix FileExt, probe_page_size, real_disk_size, try_clone), open_or_create (dir-env, creates zerodb.dat, persisted-vs-requested map_size). zerodb public: EnvOpenOptions(map_size/max_dbs/max_readers/page_size/flags)→Env, hand-rolled EnvFlags(PREV_SNAPSHOT MUST, READ_ONLY accepted-stored), open canonicalizes the dir (registry key), re-exports Error/Env/EnvInfo/EnvClosingEvent. Oracle state-machine hoist (M0.3 precondition) DONE: new driver::classify(op, TxnState, dbs_empty) is the single authority for op-validity/Skip; LmdbEngine::apply now gates on it at the top (its inner db_at/write_txn/read_source remain only as handle resolvers) — behavior byte-identical, all 30 existing oracle tests pass unchanged. Added Engine::implements (default true) + symmetric gate in run, Skip::NotImplemented, and ZerodbEngine (implements only Op::Reopen at M1.2; everything else gated out symmetrically so the differential restricts to supported ops — M1.3+ fills ops in). Tests: 9 core env unit (miri-clean) + 12 zerodb-only integration (crates/zerodb/tests/env_lifecycle.rs: create/reopen, EnvAlreadyOpened, PREV_SNAPSHOT older-meta, corrupted-higher-slot fallback, both-torn→Invalid, one-valid+PREV→Invalid, garbage→Invalid, try_clone, closing-event) + 5 differential (env_lifecycle_differential.rs: create/open/reopen cycles, reopen-larger, mixed-only-env-survives, 64-seed random, garbage-store error-kind parity). fmt/clippy(-D warnings, --all-targets)/cargo test --workspace (all green) / cargo +nightly miri test -p zerodb-core (24 lib incl. 9 env + 28 page_edges + 13 format, green) / cargo +nightly fuzz build (both targets OK). Divergence found by the random differential: heed rejects any map_size not a multiple of the OS page size (Io(InvalidInput); 16384 on Apple Silicon), zerodb is lenient (maps file len) → logged DIVERGENCES D-006 PROPOSED; tests use OS-page-safe sizes (kib mult of 16). SPEC 02 §8 updated (data-file name zerodb.dat, runtime map_size selection, D-006). just fuzz-quick (diff_ops self-test) FAILS with a PRE-EXISTING SEGV — NOT M1.2, NOT zerodb code: AddressSanitizer: SEGV in the fork's _mdb_cursor_put via LmdbEngine::put_flagged; reproduces identically on committed M1.1 (git stash + replay confirmed). Minimal 59-byte repro decodes to PutFlagged(APPEND, key≈520 B [over the 511 max], value 1 MB) into a 1 MiB map on a committed db — the fork faults instead of returning BadValSize/MapFull. run_self_test is LMDB-vs-LMDB so zerodb is not invoked. Per CLAUDE.md rule 3 (no drive-by) / rule 2 (no test weakening): NOT fixed, NOT worked around — flagged for human triage (artifacts: fuzz/artifacts/diff_ops/crash-min-append-oversized-key). Env-lifecycle scope itself is fully green. No new deps beyond memmap2 (allowlisted). unsafe total added = 1 (zerodb-io mmap); zerodb-core stays #![forbid(unsafe_code)].

M1.3 done 2026-07-16 — notes: read path (B+tree search, get, full cursor, RoTxn) + test loader + differential flip-on. zerodb-core::btree (SPEC 03 §2–§4, #![forbid(unsafe_code)], miri-clean): Tree{bytes,psize,root,depth} + get(); Cursor with (pgno,ki) stack + INITIALIZED/EOF flags — first/last/next/prev (leaf-hopping ascend/descend; EOF parks, prev-from-EOF=last, prev-past-min→before-begin uninitialized), set_exact, set_range(≥), get_greater_than(>), get_greater_than_or_equal_to, get_lower_than_or_equal_to(≤), get_lower_than(<), get_current; each op cites its §4 subsection. Values resolve zero-copy incl. contiguous overflow runs (§3). prefix_successor (all-0xFF/empty→None). Depth-bounded descent guards a corrupt/cyclic tree (typed error, no panic/loop). zerodb-core::builder (public builder, the M3.4 bulk_load prototype): build_single_db_image(psize,map_size,txnid,sorted_entries,fill) packs leaves at ~90% fill, builds branch levels upward (fix_branch_groups keeps every non-root branch ≥2 children, INV-8), spills big values to overflow runs, writes both meta slots → a valid openable file; produces trees the read path + invariant walk accept identically to a write-path tree. zerodb-core::rotxn (re-exported from zerodb): Env::read_txn()->RoTxn (pins the live meta's main_db root/depth + borrows the mapped &[u8]; M1.8 seam — no reader slot yet, noted), Env::main_database()->Database, heed-shaped get/len/is_empty/first/last/neighbor-seeks + lazy zero-copy RoRange iterator: iter/rev_iter/range/rev_range (all Bound combos)/prefix_iter/rev_prefix_iter (prefix = [P, prefix_successor(P))). env refactor: EnvInner.backing Mutex<Option<Box<dyn Backing>>> → plain Option<Box<>> + backing_bytes() (Drop's &mut self releases before signalling; shared &self reads borrow the map for the txn life, SPEC 04 TXN-37) — zero new unsafe; zerodb-core stays forbid(unsafe). Named-DB catalogs scoped OUT (M1.6): only the main/unnamed DB is exposed (noted). Oracle: read+shadow-write surface flipped on differentially. ZerodbEngine = rebuild-world-on-commit: Put/PutFlagged(Append §S1 / NoOverwrite §S2)/PutReserved/Del/ClearDb buffer in a BTreeMap shadow (write-txn reads served from it = uncommitted view); Commit materializes the shadow through the builder into a fresh zerodb.dat reopened for read-txn reads through the real tree. Now differential (via implements()): BeginRo/BeginRw/Commit/Abort, CreateDb(Unnamed), ClearDb, Get, Put, PutFlagged, PutReserved, Del, Len, IsEmpty, First, Last, SetExact, SetRange, GetGreaterThan, GetLowerThanOrEqualTo, Iter, RevIter, PrefixIter, RevPrefixIter, VerifyGet, Reopen. Gated OFF (symmetric): CreateDb(Named)/DropDb (M1.6), BeginNestedRo/EndNestedRo (M1.9), IterMut* (M1.4). diff_ops fuzz target switched from run_self_test to run::<LmdbEngine,ZerodbEngine>. Divergences found during bring-up and fixed zerodb-side (LMDB observed, never touched): (1) key-size validation asymmetry — writes (put/put_reserved) reject empty AND >511 (BadValSize); get/set/set_range/seeks reject only empty (>511 → Ok(None), search finds nothing); del rejects only empty (>511 → Ok(false); del does NOT check maxkey up front, unlike put); forward prefix_iter empty→BadValSize but rev_prefix_iter empty→full reverse (seeks via last, not set_range). SPEC 03 §2.1 added with the full observed table. (2) harness drift: LmdbEngine::reopen did not reset cleared_in_txn (FORK-1 guard fact) while ZerodbEngine::reopen did → asymmetric KnownForkBug-vs-NoDb after reopen-while-cleared; fixed LMDB side (reopen is a txn boundary). Both pinned by regression tests. Harness tuning (M1.3): Value large branch multi-MB→1..64 KiB @1/16 (multi-page overflow coverage without MapFull/huge temp files); both engines base map = 64 MiB (DIFF_MAP_SIZE, so no ≤64-op sequence exhausts it — MapFull unmodeled by the shadow) + Reopen sizes rounded to 64 KiB (round_map_size, sidesteps D-006 so heed accepts them). Multi-MB overflow (5 MB) covered by a zerodb-only read-API unit test instead. Tests: btree 7 + builder 5 unit (miri); zerodb read_api.rs 5 (range all Bounds, rev, prefix, 5MB overflow, empty); oracle read_differential.rs 11 (edge key-size, overflow readback, in-txn-vs-committed, append/no_overwrite, clear, reopen-guard regression) + read_proptest.rs (400 cases). Checks (verbatim in handback): fmt --check clean; clippy -D warnings clean (#[allow(should_implement_trait)] on Cursor::next — MDB_NEXT naming); cargo test --workspace green (all suites, exit 0); miri -p zerodb-core green (36 lib incl. btree+builder+env + 28 page_edges + 13 format, no UB); fuzz build both targets OK; diff_ops differential ~120s = 16,131 runs, 0 crashes 0 divergences (2 real zerodb bugs found & fixed en route, artifacts retired to corpus). No new deps; zerodb-core unsafe still 0. Spec ambiguity (noted, not amended): SPEC 03 §4's prefix-iteration text describes the algorithm but not heed's forward-vs-reverse empty-prefix asymmetry nor the read/write key-size split — both were resolved by oracle observation and captured in the new §2.1 table (behavior clarification within scope, per CLAUDE.md rule 3). The core zerodb::Database read API is deliberately lenient on key size (search-based); the LMDB-parity key-size errors are applied at the oracle adapter / will be re-imposed at the heed adapter boundary (M1.13) — documented in rotxn.rs/zerodb_engine.rs.

M0.3 done 2026-07-15 — notes: first Rust code. zerodb-oracle built: Op enum over the SPEC 00 MUST surface (+ Arbitrary), Engine trait, LmdbEngine (heed =0.22.1, WithoutTls), normalized OpResult/OracleError/Skip, run/run_self_test op-by-op comparison. Native zerodb Engine seam left for M1.2+ (no stub). ADR-0001 (oracle links heed, Approved) indexed in DECISIONS.md. Tests: trivial-sequence acceptance self-test, proptest (256 cases) + fuzz share decode_ops, and §S4 key-bounds. §S4 confirmed empirically (key_bounds.rs): max_key_size()==511 constant across map_size 1 MiB & 64 MiB; empty key and >511 both rejected as MdbError::BadValSize; 1..=511 accepted — SPEC 01 §S4 updated with the observed table. fuzz/ = cargo-fuzz, own workspace (not a member), target diff_ops runs run_self_test; built with +nightly and ran 30s clean (17,696 runs, 0 divergences); 10-min gate deferred to handback. fmt/clippy(-D warnings)/test --workspace all green. Harness modeling notes for M1.2+ implementer: one active txn at a time; iteration ops are whole-Vec snapshots (no persisted cursors); DropDb-then-Abort restoration not modeled (documented limitation, firm up in M1.6). One sanctioned unsafe: 'static lifetime-extension of heed txns held across apply() calls (Box-stabilized Env, drop order enforced) — SAFETY comments at each site, confined to the oracle per the unsafe policy. Test-writer pass added tests/divergence_detection.rs (7 tests: FaultyEngine proves run() catches byte-flip/false-NotFound/dropped/swapped entries/suppressed-error/len-off-by-one at exact op index + negative control) and tests/flag_semantics.rs (7 tests pinning §S1/S2/S5; SPEC 01 annotated). just fuzz-quick gate RUN: 247,936 execs/10 min, 0 divergences 0 crashes. Scoped spec-review (voluntary): no blockers; unsafe verified sound against heed source; fixes applied same-change: commit-failure now truncates dbs like abort (dead-dbi leak), double-reopen-failure poisons engine (comparable error, no panic), ADR-0001 flipped to Proposed (awaiting Quentin), libfuzzer-sys added to ADR-0001 scope. Deferred to M1.2 (reviewer structural note): hoist op-validity/Skip state machine out of LmdbEngine into a shared driver so the zerodb Engine impl can't drift — this is a M1.2 precondition, not optional.

M1.2 FORK-1 triage (same session): fuzz gate initially FAILED with a SEGV inside the vendored fork's _mdb_cursor_put — isolated by child-process probes to a 4-ingredient recipe (same-txn clear of another db + successful APPEND into a committed db + second APPEND that should be KeyExist; value size irrelevant; plain build crashes, exit 139). Filed docs/UPSTREAM-BUGS.md FORK-1 (repro kept: examples/fork_segv_repro.rs, run VARIANT=with_clear), D-007 PROPOSED (zerodb returns KeyExist — a crash is UB, not oracle-assertable behavior). Harness guard: driver::classify now takes cleared_in_txn; PutFlagged(Append) in a cleared txn → Skip::KnownForkBug, symmetric; pinned by tests/fork_bug_guard.rs (crash sequence completes, coverage preserved without clear, guard resets at txn boundary). Gate re-run PASSED. NOT yet reported upstream — human should file against meilisearch/lmdb with the repro. M1.4 done 2026-07-16 — notes: the write path, per ADR-0004 (Approved same day, OQ1–5 resolved: Mutex<Arc> publish-cell placeholder until M1.8; map full map_size at open / never remap (D-006 → APPROVED); fsync = std sync_data as-is; freed-page leak window until M1.5 accepted; always-compiled commit hooks). zerodb-core (#![forbid(unsafe_code)] intact, unsafe count still 0): dirty (HashMap<pgno, Box<[u8]>> stable frames — psize tree frames, contiguous N×psize overflow runs keyed by head, TXN-41/44), btree::Source enum (Map | Writer{dirty-first}) threaded through Tree/Cursor + TxnRead trait so the whole M1.3 read API serves RoTxn AND RwTxn (M1.9/M1.10 seam), rwtxn (RwTxn holding the TXN-6 mutex guard for life; COW first-touch + top-down parent rewrite §5; put/put_with_flags(APPEND §6.3 last-key-compare + append split, NO_OVERWRITE §S2)/put_reserved(TXN-47, non-split path never zeroes; split path zero-placeholder documented)/delete/delete_range/clear; split = §6.4 median-fit-adjust transcribed with prefix sums; delete rebalance §10 borrow-else-merge (left-absorbs-right, cascading) + §9 root grow/shrink; errored-txn (MDB_TXN_ERROR parity, new MdbError::BadTxn) on mid-mutation failure; loose-page fast path GC-7/8 + GC-10 trailing shrink; extend-only alloc w/ GC-17 MapFull; freelist_save = C1-hook stub, freed pages leak until M1.5 per OQ4), commit = ONE commit_pipeline fn C0–C6 with always-compiled H0–H4 hooks (Option<Arc> on EnvInner), sync_data barriers, meta → slot txnid&1, REC-13 poisoning, C6 publish in TXN-19 order (swap Arc then commit_point.store SeqCst); unchanged-commit skip; RwCursor (iter_mut primitive) tracks position BY KEY — at most one cursor exists across a mutation under the borrow model, so LMDB's sibling-cursor fix-up is not built (SPEC 03 §5.4 amended, ADR-0004 D5); check::check_image = SPEC 03 §11 walker (INV-1..9,11..21 + GC INV-14/23/25/26; INV-10/22 excluded per leak window) — M1.12 tool will wrap it. env: snapshot cell + commit_point(SeqCst) + write mutex + poisoned(Acquire/Release) + hooks on EnvInner; readers clone the published snapshot (meta page read once at open, TXN-18); testutil::mem_env (miri). zerodb-io (unsafe still the 1 mmap block, SAFETY extended for beyond-EOF map + own-fd writes at TXN-62-safe pages): Backing gains write_at_page/sync_data; open maps max(map_size, file_len); remap() removed. Oracle: ZerodbEngine rebuild-world DELETED — real txns held 'static via the LmdbEngine transmute pattern (2 SAFETY sites, oracle-sanctioned); IterMutPutCurrent/IterMutDelCurrent flipped ON differentially; debug builds re-check the committed image after every commit. SPEC amendments (same-change, rule 3): TXN-2 rewritten — aborted txnids ARE reused (required by TXN-63 slot parity + TXN-67; old "assumes non-reuse" wording was self-contradictory — flag for human review); SPEC 03 §5.4 single-cursor fix-up scope; SPEC 02 §8 mapping strategy + D-006 APPROVED. Divergence log: zero differential divergences through bring-up (13 deterministic write_differential tests, 200-case write-biased proptest, 400-case read proptest, 24,477-run/181s diff_ops fuzz — all first-pass clean); 1 pre-differential bug caught by own unit test (APPEND-into-empty-tree missed entries+=1). Crash smoke: H0–H4 child-abort matrix (crash_smoke.rs) recovers per REC-6 (H0/H1/H2→N-1, H3→{N-1,N}, H4→N), data all-or-nothing, walker clean — ran 5×5 hooks green; full 10k-cycle harness is M1.11 (hooks + fault-backend seam ready). Checks: fmt --check clean; clippy -D warnings clean; cargo test --workspace 30 suites 0 failures (58 core lib incl. 14 write-path/TXN-49 miri suite; 12 write_api incl. 5MB-overflow commit/reopen + exact-fit split boundaries at 4K & 64K + PREV_SNAPSHOT rollback over real commits); cargo +nightly miri test -p zerodb-core exit 0 (58+28+13, 1935s); fuzz build OK; diff_ops 181s = 24,477 runs 0 crashes 0 divergences. just crash-test-quick N/A (crash-harness binary is M1.11). No new deps. M1.4 FIX 2026-07-16 (post test-writer coverage pass) — branch borrow-from-right shifted-index bug: rwtxn.rs sibling surgery did remove(0); remove(0) — after the first removes the empty-key node-0 sentinel, the real-keyed old node 1 shifts into index 0 and branch_cell_len(index0=true) rejects its key → panic cell was validated on construction: BadKeySize (tree.rs:688). Reachable only at depth ≥ 3 with wide keys (branch-level rebalance where the underful branch is the leftmost child, i.e. sibling on the right and borrowable). Fix: remove at explicit unshifted indices highest-first (remove(1) then remove(0) then reinstall the sentinel). Symmetric-path audit (same bug class): borrow-from-LEFT safe (sibling removal is last-index ≥ 2; P's remove(0) provably targets the sentinel — every parent-node removal is index ≥ 1 so an underful branch's lone survivor is always node 0); branch MERGE safe (all removals/insertions at index ≥ 1); update_parent_key safe; leaf paths have no positional sentinel rule. Coverage: the test-writer's storm tests (deep_tree_rebalance.rs, write_rebalance_differential.rs — left untouched, now green) + 4 new symmetric regressions (deep_tree_rebalance_symmetric.rs, write_rebalance_symmetric.rs: descending cascade → borrow/merge-from-left, instrumented-verified 64 borrow-from-left hits; interior-band deletes → merges both directions). Checks after fix: fmt clean; clippy -D warnings clean; cargo test --workspace 39 suites 0 failures (incl. test-writer's abort_poison_semantics/concurrency_smoke/loose_page_and_trailing_shrink/put_reserved_adversarial, all green); miri -p zerodb-core green; diff_ops fuzz 120s clean (counts in session report).

M1.4 review round (session lead): test-writer found a REAL depth>=3 wide-key branch-borrow-from-right panic (double remove(0) vs node-0 sentinel) — fixed by critical-implementer, symmetric paths audited, regression tests added. Verified: 219 workspace tests green, full miri exit 0, fuzz gate 77,915 runs/10 min clean (real write path + per-commit invariant walk). Inline pipeline review vs REC-6/7: barriers ordered, poison-on-fsync-failure, publish-after-durability. TXN-2 amendment (aborted txnid reuse required by slot parity) RATIFIED 2026-07-16 (standing directive). Spec-reviewer agent launch was interrupted; replaced by session-lead inline review of pipeline + rebalance fix.

M1.5 done 2026-07-16 — notes: free-page management (GC) per ADR-0005 (Approved same day; OQ1 sound mutexed interim reader gate, OQ2 GC-11 comment fix, OQ3 bands as proposed, OQ4 non_free_pages_size in scope). zerodb-core (#![forbid(unsafe_code)] intact, no new atomics — the interim reader registry is a Mutex<BTreeMap<txnid, count>> on EnvInner, clone+pin atomic under the registry mutex (pin_reader, lock order registry→snapshot-cell, no verify-loop needed — race-freedom argument in the doc comment), replaced wholesale by M1.8; no loom needed (nothing lock-free added)): TreeId {Main, Free} selector threaded through the whole rwtxn mutation path (~20 fns; same split/merge/COW code mutates the GC tree; M1.6 named-DB seam), drains: BTreeMap<F, remaining> (GC-20) + reclaimed: HashSet (TXN-62 assert + loose classification incl. reclaimed-then-refreed) + AllocMode {Normal, GcSave} + save_touched; gc_reclaim (GC-18/19: BE ascending scan, gate min(oldest live reader, txnid−1), smallest-id / first-within-PIL-run picks), freelist_save at C1 (fixed-point loop: pending-rewrite set for GC-20 remainders/deletes, txnid-entry RESERVE write, GC-10 re-shrink + loose fold, FREELIST_SAVE_MAX_ITERS=64 guard), PIL codec in page::geometry, non_free_pages_size/free_page_count (GC-22..24), check_image INV-22/24 flip-on UNCONDITIONAL (free-set collection + reachable-XOR-free partition; M1.4 leak-window comments deleted). SPEC AMENDMENT PENDING HUMAN RATIFICATION: implementation proved original GC-12 (ALL GC draws barred in freelist_save) makes PLAN §1.5 bounded growth UNACHIEVABLE — the in-save COW of a committed GC page can't reuse its predecessor same-commit (TXN-62), so the ban extends the file ~+1 page/commit forever (measured; guard test failed 233472→266240/30 cycles). Amended GC-12/GC-13 (SPEC 05, pre-amendment text preserved; ADR-0005 finding section): in-save draws from the already-loaded drain pool allowed (never fresh tree reads), anti-leak carried by the fixed point (touched entries re-rewritten before exit); matches the fork's me_pghead behavior. With amendment all guards pass. Two M1.4-era exact-size tests updated +1 page (commits now write a GC leaf instead of leaking — documented in-test). Tests: gc_reclaim.rs 7 guards (steady-state churn flatness 1.05x, partial-drain across reopen, self-write+GC-26 spill baseline [504 pages freed, clear=14–17ms], reader-gate corruption probe + release-reuse, INV-27 identity, H0–H4 crash-churn matrix w/ GC atomicity); oracle gc_churn_parity.rs (200 cycles × 1000 keys vs the fork, both psize 4096): observed steady-state ratios zerodb/lmdb = 0.684 general (band [0.5,1.5]), 0.798 no-overflow (band [0.75,1.25]), zerodb curve exactly flat half→final; Engine::real_disk_size + per-Commit size tripwire (BASE 1MiB + 1.5×) active in run() → every fuzz case. Checks: fmt clean; clippy -D warnings clean; cargo test --workspace 41 suites 0 fail; miri -p zerodb-core green; diff_ops 181s = 20,701 runs 0 crashes 0 divergences (tripwire armed). crash-test-quick N/A (M1.11 binary). No new deps; unsafe count unchanged.

M1.6 done 2026-07-16 — notes: named databases and the catalog (PLAN §1.6, SPEC 02 §6). zerodb-core (#![forbid(unsafe_code)] intact, unsafe count still 0): public Database now carries DbSel {Main | Named(u32 dbi index)} (Copy handle, heed-shaped); env-level NamedRegistry on EnvInner (Mutex<{names: Vec<Box<[u8]>>, by_name, max_dbs}> — ZeroDB's me_dbxs analogue, maps dbi→name only; append-only within a process, an interim simplification like the M1.5 reader registry, unobservable through the SPEC-00 surface because records always resolve catalog-side). Named-DB records = 48-byte F_SUBDATA inline leaf values in the main tree (SPEC 02 §6); DBRecord::to_bytes/from_bytes codec added; Tree::get_catalog_entry returns node flags so open/create detect a plain-user-key collision (Incompatible). TreeId::Named(u32) threaded through the whole mutation path; RwTxn.open: HashMap<dbi, NamedTree{name, rec, dirty}> holds per-txn working records loaded lazily from the catalog (SPEC 04 TXN-10 step 3); record/record_mut/record_for/ensure_open resolve Main/Free/Named. F_SUBDATA threaded through OwnedLeafCell.flags so the flag survives leaf splits/merges/rebalances (load-bearing: the check walker uses it to follow sub-DBs). Catalog write-back mechanics CHOSEN (documented, SPEC 02 §6.1 / SPEC 04 §9 C1a): dirty named records flushed into the main catalog at a new commit step C1a, immediately before freelist_save (matches LMDB mdb_txn_commit sub-DB flush order — the write-back COWs main leaves / frees pages that freelist_save must then capture); rejected the "write-back on every mutation" alternative. Env::create_database (eager empty F_SUBDATA entry, MDB_CREATE semantics, abort-discards), Env::open_database (read path, None if absent, Incompatible on user-key collision), Database::stat → new public DatabaseStat, Database::clear (keep entry) / Database::drop_db (remove entry; main-DB drop degrades to clear per LMDB). New MdbError::DbsFull/Incompatible (Debug renders match heed for oracle taxonomy parity). max_dbs threaded through open_with_backing/open_or_create/mem_env. RwCursor carries DbSel so iter_mut cursor mutation works on named DBs. check.rs: the main-DB walk now follows every F_SUBDATA catalog entry into its named-DB sub-tree (INV-18 sub-stats + INV-22 reachable-XOR-free coverage for ALL DBs; validates name length + 48-byte record shape). Oracle: ZerodbEngine rewritten to hold real zerodb::Database handles (create via create_database, resolve by index like LmdbEngine, DropDb/named create flipped ON in implements; only nested-read gated out for M1.9); VerifyGet opens by name via open_database. Two oracle divergences found & resolved (both PROPOSED in DIVERGENCES, need sign-off): (D-008) the on-disk DBRecord bytes are format-specific (root pgno at offset 0 vs LMDB MDB_db offset 40) — surfaced only by raw-iterating a root DB that holds catalog entries, which no consumer does (milli's primary DB is the named "main"; arroy/hannoy use only the true unnamed root and never create named DBs). Modeled faithfully: DbName::Unnamed → milli's named "main" (root stays a pure catalog never read as data); the true unnamed root is covered without catalog mixing by unnamed_root_differential.rs. (D-009) LMDB EINVAL when open_database opens a dropped-and-reused dbi in a fresh read txn concurrent with the open write txn — an LMDB dbi-reuse hazard; zerodb returns the correct MVCC None. VerifyGet (models a post-commit read) now Skip::VerifyDuringWrite while a write txn is open (concurrent reader isolation is a M1.8 reader-table concern). Two real bugs found by the differential fuzz & fixed (both harness-shim ordering, not core): ZerodbEngine::verify_get applied the empty-read-key BadValSize shim before open_database (an uncommitted name must resolve to None first, mirroring LMDB mdb_get order) — reordered to after Some(dbh). Divergence log: 2 real zerodb-side bugs = 0 (both fuzz finds were harness-shim order, fixed harness-side; no zerodb-core divergence). Tests: core +2 (named_db_in_txn_create_put_read_clear_drop [miri], named_db_bad_name_rejected); crates/zerodb/tests/named_db.rs +7 on real files (persist/reopen, catalog-leaf split preserving F_SUBDATA [150 named DBs, depth≥2, check clean], stat-vs-walk, clear-keeps/drop-removes, create-abort, DbsFull, user-key Incompatible); oracle +4 files (multi_db_differential 8, stat_differential 2 [entries exact all sizes, depth ±1 documented], name_edge_differential 4 [empty/511/512 parity + 0x00 byte-name zerodb-only], unnamed_root_differential 2). Stat-parity scope: entries compared exactly cross-engine; depth exact for empty(0)/single-leaf(1), bounded ±1 elsewhere (format fan-out); page counts NOT compared cross-engine (format-specific, D-002) — verified against the full check walk zerodb-side instead. Checks: fmt --check clean; clippy -D warnings --all-targets clean; cargo test --workspace all suites 0 fail; miri -p zerodb-core green (incl. the 2 new named-DB core tests); fuzz build OK; diff_ops differential 180s clean after the 2 shim fixes (2 crash inputs retired to corpus). No new deps. SPEC 02 §6.1 + SPEC 04 §9 (C1a) amended in-change (behavior clarification, rule 3).

Split-heuristic parity change done 2026-07-16 (user-initiated task, post-M1.5): SPEC 03 §6.4 amended (ratified) — end-of-page inserts (newindx==nkeys) split at the insert point like the fork, leaves only (branch case would violate min_keys=2; fork lands end-insert branch splits at nkeys-1 — documented). D5 workload: 132 → 58 leaf pages (fork ~68). New ascending_fill_parity.rs (matched page size): ratios 0.929/0.989. Tripwire BASE 72 → 16 pages. Churn no-overflow band breached favorably (0.741 vs 0.75 lower edge) — flagged, not papered over; resolved by option (a): lower edge → 0.60 with ADR-recorded rationale (sanity bound; OS-page-size platform artifact; integrity via INV-22+differential). All checks green; 180s fuzz clean with tightened tripwire; targeted miri on rwtxn 17/17.

M1.8 done 2026-07-16 — notes: MVCC reader table + concurrency per ADR-0006 (Approved via session-lead review under Quentin's standing directive of 2026-07-16; spec-review verdict: "committable conditionally — protocol PASSES site-by-site on ARM, no live bug", all conditions applied). zerodb-core (#![forbid(unsafe_code)] intact, unsafe count still 0): new readers.rs — ReaderTable (Box<[CachePadded<AtomicU64>]>, TXN-14 sentinels RDR_FREE/RDR_CLAIMED, claim CAS Acquire/Relaxed TXN-15, store_pin/scan SeqCst TXN-17/20 w/ the two-case proof quoted at the site, release Release TXN-18a, publish_and_verify = the factored TXN-17 protocol core) + SnapshotCell (Option B: Mutex<Arc<Snapshot>> w/ O(1) critical sections — mem::replace, unlock, drop-old-outside — + SeqCst commit_point; TXN-19 swap-then-store; pin = claim → verify-loop → clone → adopt-newer tail w/ slot≤snap debug_assert); new sync.rs shim (std native/miri, loom under --cfg loom); M1.5 interim Mutex<BTreeMap> reader registry DELETED (pin_reader→(Arc<Snapshot>,slot)+ReadersFull, release_reader(slot), oldest_live_reader=table scan — gate expression in rwtxn unchanged); MdbError::ReadersFull (TXN-16); max_readers threaded through open_with_backing (+ zerodb EnvOpenOptions pass-through, default 126); RoTxn reworked to EnvHandle{Borrowed,Owned} + slot field (release-then-drop-env order), Env::static_read_txn() (TXN-24, heed shape, blocks close via refcount), RoTxn: Send compile-asserted; per-txn oldest cache (TXN-22 as annotated, ratified — no refresh-on-miss); debug shadow gate debug_assert_gate (fresh table re-scan asserting F ≤ every live pin at BOTH GC draw sites, multi-copy-atomicity caveat documented). loom sanctioned as [target.'cfg(loom)'.dependencies] (never in native/miri builds, verified absent from cargo tree); crossbeam-utils added (allowlisted). SPEC 04 amended in-change: TXN-18 ratified amendment (cell may be bounded-O(1) mutex; no-blocking scoped to the write transaction; ArcSwap/unsafe parenthetical struck), TXN-9 cross-ref, TXN-21 registry history note + shadow check, TXN-22 per-txn-cache ratification, §11 loom/stress row. Suite results: just loom (--release + forced debug assertions) 6/6 — L1a/L1b/L2/L3/L4/L5; mutation-check record (in readers.rs + ADR R1): TXN-19 inversion CAUGHT (L2+L4+L5 fail); SeqCst-access weakenings NOT loom-detectable — with-mutex models make them genuinely safe (cell-mutex HB chains), and the mutex-free L2b fallback-world model was built and WITHDRAWN: loom 0.7 fails the correct all-SeqCst code (canonical all-SC store-buffer litmus under loom explores the both-miss C++20/AArch64-RCsc forbid — pre-P0668 SC-access semantics, tokio-rs/loom#180 class); ADR-0006 R1 amended with the standing precondition that any Option-D/E lock-free-cell migration brings its own StoreLoad verification. Stress (reader_stress.rs, 8 readers × double-walk digests + 1 GC-churn writer, plain+static txns, final check_image): 4/4 default (~5s); just stress 180s run = 5401 writer commits, 6121 reader double-walks, 0 violations, shadow gate armed throughout (debug build). ReadersFull differential vs the fork: direct two-engine test (readers_full_differential.rs, 2/2) — outside the op model (the harness models one active txn at a time, so held-N-readers is inexpressible as ops; noted in-file); boundary/recovery/held-reader parity confirmed. D-010 PROPOSED (max_readers(0): fork EINVAL at open [probed empirically], zerodb ReadersFull per-txn; no consumer passes 0). Checks: fmt --check clean; clippy -D warnings --all-targets clean; cargo test --workspace 49 suites 0 fail; miri -p zerodb-core exit 0 (69 lib tests incl. 6 readers + Send assert, 2068s); diff_ops fuzz 181s = 6,523 runs 0 crashes 0 divergences (fuzz-quick separately green per session lead). Zero differential divergences; no new deps beyond the sanctioned loom (cfg-gated) + crossbeam-utils.

M1.10 done 2026-07-16 — notes: write flags and modes (PLAN §1.10, SPEC 01 Table 1 §S6/§S7, SPEC 04 §6.4, SPEC 06 §3). zerodb-core (#![forbid(unsafe_code)] intact, unsafe count still 0): new DurabilityFlags{read_only,no_sync,no_meta_sync,map_async,write_map} on EnvInner (immutable at open); Backing::sync(async_flush) trait method (default → sync_data; the writable-map backing overrides it with msync sync/async); commit_pipeline honors the durability lattice — C3 skipped under no_sync, C5 skipped under no_sync||no_meta_sync, map_async passed through as async_flush (SPEC 06 REC-9); write_txn on a read-only env → EACCES (Io(PermissionDenied), os error 13, matching the fork via heed) checked before the write mutex (TXN-8); Env::force_sync() (mdb_env_sync parity — synchronous barrier under the write mutex, EACCES on RDONLY, poisons on failure REC-13). open_with_backing gains a durability param (#[allow(clippy::too_many_arguments)] — 8 distinct pre-resolved scalars). zerodb-io (unsafe now 3 mmap blocks, all SAFETY-commented): new MmapWritable (memmap2::MmapMut; map=map_mut writable MAP_SHARED, write_at=raw-ptr copy_nonoverlapping into the map under the single-writer discipline, flush=msync sync/async) + WriteMapBacking (Backing over it: write_at_page=memcpy into the map, sync=msync(+fdatasync on the sync path per REC-12)); open_or_create gains write_map (set_len file to map_size so no store faults past EOF — matches the fork's ftruncate-to-mapsize, confirmed by probe: LMDB writemap file = map_size at open). zerodb public: EnvFlags gains NO_SYNC/NO_META_SYNC/WRITE_MAP/MAP_ASYNC (bit-exact LMDB constants); open() builds DurabilityFlags from flags and threads write_map + durability; Env::force_sync/is_read_only/durability re-exported. WRITE_MAP realization (SPEC 04 §6.4 amended, TXN-45a): implemented as a commit-time write strategy — dirty bytes live in the heap dirty-page store for the whole write txn (identical to default mode: value-borrow contract, nested-reader dirty reads, abort-by-drop all unchanged, so NO map unsafe in zerodb-core and M1.4 miri covers writemap's during-txn path for free), copied into the writable map at C2 and msync'd at C3/C5. Observably identical to the fork's live-map writes through the heed/oracle surface (all env_writemap_* differential tests pass); true zero-copy live-map mutation deferred to Phase 3 (needs a zerodb-io-brokered map-slice API + a bench). NOT a DIVERGENCES entry (no observable divergence). Oracle: EngineMode{write_map,no_sync,no_meta_sync,map_async} + Engine::new_in_mode(mode) (both engines open with the matching flags) + run_in_mode (size tripwire disabled under WRITE_MAP since both engines set_len to map_size). flag_notls_rotxn_is_send slug added. Tests (all green, 0 divergences): oracle write_flags_differential.rs (8): env_writemap_put_get_parity, env_writemap_put_reserved, durability_mapasync_writemap, durability_nosync_no_fsync, durability_nometasync, env_rdonly_rejects_write (direct two-engine, EACCES kind parity), flag_notls_rotxn_is_send (compile assert + cross-thread static_read_txn move), milli_indexing_flag_combo_replay (the PLAN acceptance: WithoutTls + named DBs + APPEND-sorted + del + clear + nested reads mid-txn, compared exhaustively); zerodb-core durability_barriers.rs (5, control-flow parity via a counting Backing: default=2 syncs, NO_META_SYNC=1, NO_SYNC=0, NO_SYNC|NO_META_SYNC=0, MAP_ASYNC=2 async); zerodb write_flags.rs (6 e2e on real files: writemap put/get/reserved/overflow persist + reopen, writemap→writemap, nosync/nometasync + force_sync, mapasync writemap, RDONLY rejects write+force_sync, RDONLY missing store). Fuzz: diff_ops first byte seeds EngineMode (~25% WRITE_MAP, slice with relaxed durability) via run_in_mode. Docs: SPEC 01 M1.10 landed flag-matrix table (every flag → landing test); SPEC 04 §6.4 TXN-45a amendment; SPEC 01 header addendum. Checks (verbatim in handback): fmt --check clean; clippy -D warnings --all-targets clean; cargo test --workspace 54 suites / 303 tests 0 fail; miri -p zerodb-core exit 0 (lib 73 + durability_barriers 5 + page_edges 28 + spec02_format 13 + value_borrow_contract 3 = 122, 0 UB, 2071s lib); fuzz build OK + 180s shakeout 22,011 runs 0 divergences 0 crashes (WRITE_MAP fuzz dimension active). loom untouched (no concurrency/atomic/reader-table changes). No new deps (memmap2 already allowlisted); zerodb-core unsafe still 0; zerodb-io mmap unsafe 1→3 (writable map + write_at).

M1.9 review round (session lead): spec-review verdict committable/no blockers — transmute soundness, Send-via-Sync derivation, TXN-29 guard coverage, orderings all verified clean. Conditions applied: TXN-33 amended+ratified (commit-side runtime check only; drop-side deliberately absent — a drop assert would false-positive on harmless mem::forget while FFI misuse is covered by the per-op TXN-29 guard); ADR-0007 D6 covariance compile test added (nested.rs _assert_covariant); release() comment softened to match the honest L6 mutation record; PLAN §1.9 acceptance bullet updated (nested write = unrepresentable, not error). Gates: fuzz-quick 10 min exit 0 (first fuzz exposure of BeginNestedRo/EndNestedRo), miri full crate MIRI_EXIT=0, loom 8/8, workspace 284/284.

M1.11 done 2026-07-16 — notes: recovery/torn-write/crash-consistency harness per ADR-0008 (Approved same day; 6 OQ answers ratified & recorded in the ADR: per-image cycle accounting, async SIGKILL in CI, thin adversarial NO_SYNC floor, in-house splitmix64/xoshiro256** PRNG, nightly-CI venue + ≤30-min local large run, 80/20-by-cycles split w/ ≥1k SIGKILL floor). zerodb-io fault.rs (pure safe Rust — zero new unsafe; the module is a journaling wrapper over a real backing, ADR-0008 D1 Option B): REC-20 barrier model (sync(false) folds pending→durable; MS_ASYNC deliberately NOT a barrier), crash materialization at 512 B sector granularity — per-write fates Applied/Dropped/TornSectors(prefix+suffix)/TornBytes(sub-sector)/SectorMask, quota-enforced deterministic plans (guaranteed sub-sector meta tear cut at byte 32 between header/body txnid = always-rejected; both sector-aligned old/new tears = CRC-valid txnid-selection path per REC-19; all-data-dropped; lost-extension REC-14), plans_ordered (NO_SYNC ordered-writeback sub-model) + plans_adversarial, broken_data_barriers mutation-self-test switch, D5 page-multiple debug_assert on every journaled write (continuous O_DIRECT-sizing audit; O_DIRECT itself deferred to Phase 3.5), PRNG bit-stream pinned by KATs. zerodb-oracle crash/ (workload = oracle decode_ops over PRNG bytes gated by driver::classify, values 0B–16MB, 5 durability modes; shadow model Exec cross-checks EVERY op against the live env pre-crash [D6.3] and replays standalone for the SIGKILL parent; REC-18 verifier with self-adapting legal window = floor/ceil txnids from durable-only/all-applied materializations + shape asserts ceil−floor≤1 [bounded modes] and floor≥acked-at-cut [strict modes]) + --bin crash-harness (hand-rolled CLI: --cycles/--seed/--mechanism/--jobs/--variants/--modes/--repro/--keep-going/--isolate; violation → nonzero exit + image/plan/ops saved under target/crash-repro; ≥1k-SIGKILL floor enforced on ≥10k runs). justfile recipes honored as-is. REAL FINDING (needs ratification): NO_META_SYNC reclaim-clobber window — txn N+1's legally reclaimed pages (freed by N = snapshot N−1's pages, GC-18) can persist while un-fsynced meta N tears → recovery lands on structurally corrupted N−1 (INV-20 future-stamp; repro seed 15797139550980166469). REC-10's blanket "never corruption" overclaims; LMDB shares the window under MDB_NOMETASYNC (libmdbx steady/weak metas exist to close it). SPEC 06 REC-10 amended w/ scoped claim (conflict-block item 4, human ratification pending); verifier scoped exactly (full REC-18 whenever recovery = newest issued meta; clobber-window images = window/taxonomy only, counted as "stale fallbacks" — 5 in 5,014 pinned-mode cycles); steady-meta gating = Phase 3 candidate; default/WRITE_MAP immune & fully asserted. Two harness-model bugs found & fixed during bring-up (both regression-pinned): acked-at-cut vs workload-end under capture-and-continue (seed 8467876453780440666), and clear-on-empty-db IS txnid-effective (probed; LMDB mdb_drop(dbi,0) parity). Mutation self-test green both ways: broken data barriers caught at cut 1 (INV-9/18/19 meta-without-data), negative control clean. SPEC 06 §5 M1.11 amendment block added (cycle accounting, NO_SYNC sub-model split, legal-window encoding, env-creation-window REC-18.1 scope note, D5 alignment audit). Cycle counts (all zero violations): crash-test-quick 210–214/run (gate, first-ever runs); crash-test-full 10,007 (39.0s, 1,995 SIGKILL ≥1k floor) — PLAN §1.11 ≥10k acceptance MET LOCALLY; extended 100,007 (384.1s, 19,887 SIGKILL, 2 abandoned env-pressure cuts); post-fix re-runs 20,010 (75.2s) + 20,008 (76.5s) + 5,014 nometasync-pinned; ~180k total this session. Checks: fmt clean; clippy -D warnings clean; cargo test --workspace 321/321; miri -p zerodb-core exit 0 (core untouched by M1.11 — engine changes = none); loom 8/8 unchanged; diff_ops 180s = 17,386 runs 0 crashes. Nightly-CI obligation: crash-test-full on the Graviton runner alongside fuzz-long (ratified OQ5). No new deps; zerodb-core untouched; zerodb-io unsafe count unchanged (fault.rs is safe code). M1.12 done 2026-07-17 — notes: tools + migration + compacting copy per ADR-0009 (Proposed; awaiting ratification). zerodb-core (#![forbid(unsafe_code)] intact, unsafe count still 0): Cursor::current_flags() (leaf node-flags at the current scan position); rotxn::collect_entries_flagged (owned (key, flags, value) full scan) + rotxn::named_databases (F_SUBDATA catalog names, sorted) + RoTxn::{snapshot,map_bytes} accessors; builder promoted to multi-DB (build_tree_into shares one PageStore; new build_multi_db_image(main_user, named[], fill) packs each named DB bottom-up then the main catalog over user data ⊎ F_SUBDATA records, sorted — the M3.4 bulk_load prototype extended). zerodb public: new copy.rs — CompactionOption{Enabled,Disabled} + CopyToFile extension trait (Env lives in I/O-free core, so the file-writing copy is a trait in the public crate; M1.13 maps heed's inherent method onto it). copy_to_file opens its OWN internal RoTxn (SPEC 00 row 17): Disabled = raw copy of pages [2, last_pg] from the map + two fresh meta slots synthesized from the pinned snapshot (freelist preserved, opens at exactly T even if the live env advanced); Enabled = fresh compact rebuild via build_multi_db_image (no free pages). Correctness on a LIVE env: reachable-from-T pages are immutable under the reader's GC gate (TXN-20/21) so raw copies never tear live data; already-free pages may be concurrently reused but are never read as live data (harmless); no write mutex held (matches LMDB). zerodb-tools (single binary + lib; hand-rolled CLI, no clap): stat (env + per-DB stats), dump (logical mdb_dump-shaped text: VERSION=3 / per-DB blocks / hex key-value lines / DATA=END — clean-room from mdb_dump.c; logical, omits physical geometry so dumps compare byte-identically across engines/page-sizes; hex-encoded names round-trip D-008 embedded-NUL names), load (parse dump → build_multi_db_image → zerodb.dat, invariant-checked), check (wraps check::check_image + probe_page_size; nonzero exit on violation), migrate-from-lmdb (behind off-by-default migrate-lmdb feature → heed =0.22.1; enumerates LMDB subdbs generically via try-open-as-subdb, streams every DB into a fresh zerodb env in batched write txns, verifies per-DB counts). Offline guard: best-effort flock(LOCK_EX|LOCK_NB) on zerodb.dat (lock.rs); refuses if held (D-001: no cross-process protocol, engine takes no flock, so this catches other tools/processes — documented "run offline"). UNSAFE-POLICY EXPANSION (needs human sign-off in CLAUDE.md): one libc::flock FFI block in zerodb-tools::lock (SAFETY-commented) + one heed::open unsafe under the migrate feature — CLAUDE.md sanctions unsafe only in core::{page,readers}/io/oracle; zerodb-core stays forbid(unsafe). ADR-0001 amended (zerodb-tools MAY link heed behind migrate-lmdb, ratified "session lead under standing directive 2026-07-17"); ADR-0009 written (Proposed); DECISIONS indexed; SPEC 00 row 17 marked landed; README tools section. Divergences: zero new (copy_to_file matches LMDB logically both options; migrate dumps byte-identical; D-002 already covers the format difference). copy_to_file deliberately NOT added to the fuzz op model (own internal txn + file output, not an in-env state change) — direct differential tests cover it. Tests: core +2 builder (multi_db check-clean incl. overflow/empty; empty image); zerodb copy_to_file.rs 3 (Disabled/Enabled logical-equality + check-clean, compacted ≤ raw size); zerodb-tools dump_format 5 (render/parse round-trip, CRLF, NUL-name, bad-version) + dump_load_roundtrip.rs 2 (byte-identical dump→load→dump, stat on closed env) + live_env_refusal.rs 2 (locked env refused, missing env clean) + (feature) migrate_acceptance.rs 2 (LMDB→migrate→dump byte-identical to heed-rendered dump; milli-shaped real-index proxy point queries match); oracle copy_to_file_differential.rs 2 (heed copy vs zerodb copy dump-equality, both options). Checks: fmt --check clean; clippy -D warnings --all-targets clean (default AND --features migrate-lmdb); cargo test --workspace all green + tools --features migrate-lmdb green; miri -p zerodb-core PENDING/green (pure-safe core additions); diff_ops 181s = 18,309 runs 0 crashes 0 divergences. New deps: libc (allowlisted) + zerodb-io in tools; heed behind migrate-lmdb (ADR-0001 amendment). Human review: ADR-0009 ratification; ADR-0001 amendment line; the CLAUDE.md unsafe-policy expansion to zerodb-tools flock (+migrate heed open).

M1.13 done 2026-07-17 — notes: heed backend plumbing per ADR-0003 Option C/D (Approved). New crate crates/heed-zerodb — a 1:1 re-implementation of heed 0.22.1's public surface over ZeroDB's native API, re-exporting heed-traits (=0.20.0) / heed-types (=0.21.0) verbatim (codec/trait identity preserved) + pub use byteorder. Modules: error (Error/MdbError 1:1, Encoding/Decoding publicly constructible from BoxedError, From<zerodb::Error>), flags (hand-rolled EnvFlags/PutFlags/DatabaseFlags, bit-exact LMDB values, no bitflags dep), txn (RoTxn<'e,T=AnyTls> as a #[repr(transparent)] enum over the three ZeroDB read sources — committed RoTxn / NestedRoTxn / owned RwTxn — with the WithTls/WithoutTls→AnyTls deref chain; RwTxn<'p> derefs to RoTxn<WithoutTls>; nested_read_txn/static_read_txn), env (EnvOpenOptions<T:TlsUsage>, Env<T> over zerodb::Env, EnvInfo/EnvStat/EnvClosingEvent/CompactionOption/FlagSetMode/DefaultComparator/IntegerComparator/env_closing_event; copy_to_file/copy_to_path via the M1.12 CopyToFile trait, staged through a temp path), database (Database<KC,DC,C,CDUP> Copy + DatabaseOpenOptions + DatabaseStat; full read/write surface typed through the codecs, read dispatch over the 3-variant txn enum via with_read!), iterator (Ro{Iter,RevIter,Range,RevRange,Prefix,RevPrefix} over zerodb::RoRange; Rw{…} via a lifetime-erased pointer cursor RwGuts with del_current/put_current/put_current_with_options/put_current_reserved_with_flags; remap_*/lazily_decode_data), reserved_space (ReservedSpace io::Write over a heap buffer). Adapter unsafe (mirrors heed's inherent pointer model; NEEDS human sign-off vs the CLAUDE.md unsafe-module policy — heed-zerodb is not currently a sanctioned unsafe module): unsafe impl Send for RoTxn<WithoutTls> (SPEC 04 TXN-13; RwTxn inherits Send — milli moves &mut RwTxn into a rayon install/join scope, exactly as heed relies on for its MDB_txn), the two repr(transparent) TLS-marker deref retags, the RwGuts lifetime-erased write cursor (sound under heed's documented "no live borrow across a mutating call" contract), and one libc::sysconf for D-006 (libc allowlisted). The unsafe fn open/flags/remove/cursor mutators carry no unsafe body (vestigial heed-shape parity). Boundary re-impositions (each tested, tests/boundary.rs): max_readers(0)→Io(InvalidInput) at open (D-010), map_size not an OS-page multiple→Io(InvalidInput) (D-006), DB-name embedded NUL reproduces heed's CString::new(n).unwrap() panic (D-008 secondary, probed against heed =0.22.1). Read-key taxonomy (SPEC 03 §2.1, applied at the heed boundary per the SPEC note "heed-zerodb at M1.13"): empty key→BadValSize on get/del/neighbor-seeks/forward-prefix (found via the adapter differential). Consumer patch shim crates/heed-shim (standalone crate literally named heed v0.22.1, pub use heed_zerodb::* + no-op feature passthroughs; excluded from the workspace so it never collides with the oracle's real crates.io heed): cargo [patch] matches by crate name (the package = rename is silently ignored — verified). Oracle through the adapter (ADR-0003 accept #6): HeedZerodbEngine (near-copy of lmdb.rs, backend import swapped; the one structural change is boxing the write txn — heed's nested reader is a heap-stable C pointer, ZeroDB's nested reader holds a real Rust borrow into the parent RwTxn, so the oracle's transmute-to-'static-then-move would relocate it and dangle → SIGSEGV without the box) + tests/heed_adapter_differential.rs (7 deterministic op-category sequences + a 200-case decode_ops proptest, LmdbEngine vs HeedZerodbEngine, zero divergences); diff_ops fuzz target gains ZERODB_FUZZ_PAIR=heed runtime engine-pair selection (default LmdbEngine vs native ZerodbEngine unchanged). heed suite subset (tests/heed_suite.rs, 13 tests, scoped per ADR-0003 IN/OUT): ported examples all-types/clear-database/cursor-append/multi-env/nested-rtxns(dep-free std-thread fan-out)/prev-snapshot + inline env tests (close_env, EnvAlreadyOpened, create-without-commit, open-existing) + txn ro_txns_are_send + lmdb_error variant/constructibility (partial IN) + the D-003 nested-write-unrepresentable note (replaces heed's nested.rs); OUT = encryption/custom-comparator/DUPSORT/nested-write/rmp-serde/resize/nosubdir (each with a reason). Consumer compile gate (compile-only, cargo check): milli GREEN and hannoy GREEN on the ZeroDB backend, zero .rs edits — patch diffs saved to docs/patches/{hannoy-v0.1.3,meilisearch-milli}-heed-zerodb.patch, clones reverted clean. Adapter gaps the compile surfaced & fixed: Env<T> methods must be unbounded (not T:TlsUsage; cellulite uses an unbounded generic), open_database/DatabaseOpenOptions::open take &RoTxn (AnyTls) not &RoTxn<T>, RwTxn:Send (milli rayon), shim feature passthroughs (posix-sem/serde-*). SPEC 00 rows 23–26/51–54/61 + 10–17/30–49 marked satisfied-by-heed-zerodb; ADR-0003 flipped to implemented note; DIVERGENCES D-006/D-008/D-010 annotated as re-imposed at the adapter boundary. No new engine deps (heed-traits/heed-types sanctioned by ADR-0003; byteorder+libc allowlisted/transitive); zerodb-core untouched. Checks (verbatim in handback).

M1.14 PARTIAL GATE — consumer test suites 2026-07-17 (PLAN §1.14, consumer-suite portion; fuzz-24h + Graviton bench + index-scheduler/meilisearch-level suites remain the open 1.14 CI items). Ran the real consumer TEST SUITES on the ZeroDB backend via the M1.13 heed-shim [patch.crates-io] (saved patches applied verbatim to the scratchpad clones). hannoy v0.1.3 @ e1e2f4d — FULLY GREEN: 44 passed / 0 failed / 1 ignored (upstream #[ignore="just cause"] HNSW level-distribution, storage-independent) + 15 doc-tests passed. Suite includes the recall/reachability guarantees on the ZeroDB store: all_items_are_reachable_3_3 (proptest, up to 10 000 items × 768-dim Cosine, asserts a full nns(n).ef_search(n) returns EVERY stored item = 100% reachability), search_on_candidates_has_right_num, search_by_item_*, and the arroy→hannoy conversion tests — all green. hannoy ships no recall-number bench (benches/{benchmark,speed}.rs are divan TIMING benches, not recall); the standalone example/ crate was patched+run end-to-end on a real on-disk zerodb.dat env — query [0,1,0] returned item 1 @ dist 0.0 (exact-match, recall=1.0), example patch reverted after. arroy 0.6.4 (hannoy dev-dep) rides the same shim transitively, no issue. milli v1.50.0 @ fff2ef5 — FULLY GREEN (staged): lib 271 passed / 0 failed / 3 ignored (all upstream #[ignore] w/ PR links: prompt template ×2 PR#5593, cutoff degraded-search PR#6116 — backend-independent); integration tests/mod.rs 92 passed / 0 failed; doc-tests 2 passed / 0 failed / 1 ignored (upstream). No milli test asserts LMDB-internal geometry: non_free_pages_size/used_size/database_sizes/stat-derived compute_size are production stats functions (index.rs), consumed only at the meilisearch/meilitool level (out of scope), never asserted in milli's own suite; no data.mdb-path snapshot test in milli (those live in index-scheduler/meilisearch, out of scope). FIND+FIX LOG: EMPTY — zero zerodb-side or heed-zerodb-side fixes required. Both suites passed first-pass against the unmodified M1.13 adapter; the saved patches (docs/patches/{hannoy-v0.1.3,meilisearch-milli}-heed-zerodb.patch) are UNCHANGED (Cargo.toml [patch.crates-io] only; consumers reverted-clone → re-patched, only Cargo.toml+regenerated Cargo.lock differ). Zero expected-incompatible tests encountered (none of milli's/hannoy's suites reach LMDB file-layout or size-accounting assertions). zerodb workspace checks after the run (no code changed this session, tree still @ 2697973): cargo fmt --all -- --check exit 0; cargo clippy --workspace --all-targets -- -D warnings exit 0; cargo test --workspace exit 0 = 67 suites / 368 tests / 0 failed. miri NOT run (zerodb-core/io untouched). Clones left PATCHED in the scratchpad (hannoy @ e1e2f4d, meilisearch @ fff2ef5). Not committed.

M2.1+2.5+2.6 done 2026-07-20 — Phase 2 tranche A ("heed API completion"), all three additive; zero existing heed-mirrored signatures changed (PLAN ground rule 2). 2.1 stat/info: new zerodb::Env::stat() -> EnvStat{page_size,depth,branch_pages,leaf_pages,overflow_pages,entries} over the main tree, and EnvInfo completed from map_size-only to the full MDB_envinfo shape (map_size,last_pgno,last_txnid,max_readers,num_readers). Both read the published snapshot rather than opening a read txn — so neither blocks a writer nor consumes a reader slot; this also fixes the Phase-1 adapter stat(), which opened its own rtxn and silently degraded to all-zeros when the reader table was full (env_stat_works_with_the_reader_table_exhausted). me_mapaddr deliberately not exposed natively (MDB_FIXEDMAP is WON'T; the adapter keeps a null map_addr field for heed shape parity). heed_zerodb::Env::{info,stat,max_readers} now report real values (Phase 1 returned 0s and a hardcoded 126). Database::stat audited field-by-field against MDB_stat: complete, no gaps (ms_psize is env-level, already on EnvStat). Divergence found and replicated, not fixed: D-011 — LMDB's MDB_envinfo::me_numreaders is a high-water mark, not a live count (mti_numreaders only ever increments, via if (i == nr) ti->mti_numreaders = ++nr;; ending a txn just clears mr_pid). Caught by the differential probe, then confirmed in the fork's mdb.c; zerodb now reproduces it exactly via a fetch_max at slot claim, and exposes the genuinely-live count separately as the new EnvInfo::live_readers / heed_zerodb::Env::live_readers() (a method, not an EnvInfo field, so heed's struct keeps its shape). Filed PROPOSED in DIVERGENCES.md — not approved. 2.5 explicit sync: audited force_sync against the fork's mdb_env_sync0 and added the missing parameter as Env::sync(force) (heed exposes only the forced form); all three mdb_env_sync0 decisions reproduced in order — MDB_RDONLY→EACCES first, flush only if force || !NO_SYNC (so sync(false) on a NO_SYNC env is a silent success no-op), and MS_ASYNC only when MAP_ASYNC && !force. Proved with the M1.11 FaultBacking: journal non-empty before force_sync, empty after, and the crash-floor image then opens and reads back every committed key. 2.6 page size: EnvOpenOptions::page_size promoted to a documented public knob (+ get_page_size, MIN_PAGE_SIZE/MAX_PAGE_SIZE/DEFAULT_PAGE_SIZE) and added as a new method on heed-zerodb (heed has none; feature-neutral — omitting it reproduces pre-2.6 behavior exactly). Validation = power of two in [4096,65536] → Io(InvalidInput) at open, matching the D-006/D-010 open-time taxonomy. Creation-only: reopen adopts the persisted geometry silently (no "wrong expectation" error — same contract as map_size). No oracle psize dimension is possible (LMDB cannot change page size), so the differential is zerodb-vs-zerodb: identical op streams must yield identical logical content at 4K/8K/16K/32K/64K — a new property test that passed first try. No ADR: nothing format-affecting, decisions recorded inline in module docs + SPEC 00's new third table (ZeroDB extensions). Tests added: zerodb/tests/env_stat_info.rs (12), zerodb/tests/page_size_selection.rs (10), zerodb-oracle/tests/env_info_differential.rs (6), zerodb-oracle/tests/force_sync_durability.rs (8), heed-zerodb/tests/phase2_extensions.rs (9) = 45 new, all green.

M1.14 CONSUMER SUITES (Meilisearch level) 2026-07-20 — index-scheduler / meilitool / meilisearch-auth / meilisearch --lib on the ZeroDB backend via the M1.13 heed-shim [patch.crates-io]. SCORECARD: index-scheduler 83 passed / 0 failed / 0 ignored + 1 doc-test passed (61.0s test time); meilitool builds green, 0 tests (crate ships none — compile-only gate, 44s); meilisearch-auth 4 passed / 0 failed (23s); meilisearch --lib 12 passed / 0 failed / 2 ignored (option_test::test_meilli_config_file_path_{valid,invalid}, upstream bare #[ignore], config-file parsing, backend-independent) (270s). FIND+FIX LOG: EMPTY — zero zerodb-side or heed-zerodb-side fixes required; every suite passed first-pass against the adapter. index-scheduler is the load-bearing result: it exercises the poly Database<Unspecified,Unspecified> task queue, the index registry with EnvClosingEvent/prepare_for_closing open-close churn (index_mapper/index_map.rs), max_readers via MEILI_EXPERIMENTAL_INDEX_MAX_READERS, max_dbs, and Env::static_read_txn in the async handlers (lib.rs:1093) — all green. Snapshot tests redact size fields (used_database_size/database_size/internal_database_sizes => "[bytes]" in scheduler/test.rs:1037), so no LMDB geometry is asserted; zero expected-incompatible TESTS encountered. OPEN FINDING (not a test failure — needs a human decision, likely an ADR): the env data-file NAME is zerodb.dat, but heed/LMDB's on-disk contract is <dir>/data.mdb, and Meilisearch hardcodes data.mdb in PRODUCTION paths — index-scheduler/src/scheduler/process_batch.rs:707 (let src_path = index.path().join("data.mdb"); std::fs::metadata(&src_path)? → ENOENT on a ZeroDB env, then persists the compacted copy to a name the env never reads), process_snapshot_creation.rs:138/202/212 (copy_to_path(dst.join("data.mdb")) → the snapshot dir is unopenable as a ZeroDB env), scheduler/enterprise_edition/s3.rs:238/301/311, meilitool/src/main.rs:491. Verified empirically with a throwaway probe (since removed): an adapter-created env dir contains exactly ["zerodb.dat"] — no data.mdb, no lock.mdb. The suites do NOT catch this: index-scheduler ships no compaction test (grep -rn "compact" src/ | grep test = 0 hits), so index compaction and snapshot-restore are latent-broken on ZeroDB while the gate is green. Consequence of D-002 (own format) but distinct from it — D-002 sanctions the format, not the filename. NOT fixed here: on-disk naming is an ADR-class decision (CLAUDE.md rule 6) and outside the 1.14 consumer-suite scope. Recommend a D-011 entry + ADR on whether heed-zerodb should present data.mdb/lock.mdb at the adapter boundary (as it already re-imposes D-006/008/010). **ENVIRONMENT NOTE: the scratchpad meilisearch clone had been wiped by the macOS /private/tmp reaper (source tree emptied to bare directory skeletons, .git stripped of HEAD/config); re-cloned meilisearch @ fff2ef5a4 (v1.50.0, the same pin as the M1.14 partial gate), restored the surviving patched root Cargo.toml + Cargo.lock (verified: the patched Cargo.toml is byte-identical to the fresh clone plus the saved 4-line [patch.crates-io] block) and the surviving 4GB target/ cache. docs/patches/meilisearch-milli-heed-zerodb.patch is UNCHANGED and still applies cleanly. GATE CAVEAT: the workspace gate was run against a tree carrying ANOTHER SESSION'S concurrent, in-flight Phase 2 work (M2.1/2.5/2.6 EnvInfo/EnvStat completion, Env::sync, EnvOpenOptions::page_size) — 4 modified src files + 3 untracked new test files, mtimes advancing DURING this session (00:09→00:16:52 vs. session start 00:06). Results therefore do not describe a clean 1a5803e tree: cargo clippy --workspace --all-targets -- -D warnings exit 0; cargo fmt --all -- --check exit 1 (7 diffs, ALL in the concurrent Phase 2 files: env_stat_info.rs, readers.rs, env_info_differential.rs); cargo test --workspace exit 101 — compile error in zerodb-oracle/tests/force_sync_durability.rs:387,412 (cannot find module or crate libc``, missing dev-dep), a file created at 00:16:52, i.e. between my clippy and test runs. NONE of these are mine: this session changed ZERO zerodb/adapter source. Deliberately did NOT run cargo fmt --all (write mode) or add the missing dev-dep — that would corrupt/conflict with the other session's in-flight milestone. The Phase 2 tree state must be re-gated by that session. Not committed.

ADR-0010 done 2026-07-20 — env data-file naming (Option A, Approved; D-012 flipped to APPROVED/resolved). The problem: an adapter-created env dir held exactly ["zerodb.dat"], while Meilisearch hardcodes data.mdb in SIX production call-site families (index-scheduler process_batch.rs:707, process_snapshot_creation.rs:138/202/212, enterprise_edition/s3.rs:238/301/311, meilisearch routes/tasks/compact.rs:158/161, meilitool/src/main.rs:491). Compaction failed loudly on the fs::metadata probe; snapshot restore failed silently — copy_to_path(dst.join("data.mdb")) succeeded, then reopening the restored dir found no zerodb.dat and created a fresh EMPTY env. Latent behind a fully green M1.14 gate because index-scheduler ships no compaction or snapshot round-trip test. The fix (M1.13 boundary-re-imposition pattern, applied to the filesystem surface): zerodb::EnvOpenOptions::data_file_name(impl Into<OsString>) + get_data_file_name(), default DATA_FILE_NAME; new public zerodb::HEED_DATA_FILE_NAME = "data.mdb"; threaded to the single existing join point (lib.rs, canonical_dir.join(...)) — zerodb-io was already name-agnostic, so no I/O-layer change. Validated at open: empty or any embedded path separator → Io(InvalidInput) (D-006/D-010 taxonomy); the check is on the RAW name, so "a/" is rejected rather than silently normalized to a (a knob whose on-disk result differs from the value passed is a trap). heed-zerodb sets it unconditionally (heed_zerodb::DATA_FILE_NAME, re-exported), so an adapter env dir contains exactly ["data.mdb"] — no zerodb.dat, and no lock.mdb (the ADR's grep found exactly one hit in the whole consumer tree, a doc comment; fabricating a placeholder would imply cross-process locking D-001 does not provide). No fallback probing in the engine — open uses exactly the configured name, so the adapter always reads the name it writes; a dir with both names is two independent databases (tested). zerodb-tools (read paths only) probes both via the new naming module: both-present is a hard Ambiguous error naming both files, never a silent pick; lock::acquire_existing now returns (EnvLock, EnvDataFile) so the tool opens the file it locked; open_ro takes the resolved name; stat prints an engine line (zerodb (magic ZDB1, format_version 1); data file <name>) — ADR-0010's D2-in-spirit, the magic being the authoritative discriminator, not the name. Write tools (load/migrate) still create native envs but refuse a dir holding a non-empty env under EITHER name (refuse_existing_env), so they can never manufacture the ambiguous state. migrate-from-lmdb gained the magic-based mismatch error: a source data.mdb carrying ZDB1 is rejected explicitly ("is a ZeroDB env, not an LMDB one") instead of an opaque heed MDB_INVALID, because since ADR-0010 the file NAME no longer discriminates. Tests — the two the consumer suites miss are the heart of this change (crates/heed-zerodb/tests/env_file_naming.rs, 8 tests): snapshot round-trip (copy_to_path(dst.join("data.mdb")) for BOTH CompactionOptions → reopen dst through the adapter → full logical equality) and compaction-persist round-trip (the exact process_batch.rs sequence: fs::metadata probe → copy_to_file into data.mdb.cpy → rename over the live mmapped original → prepare_for_closing().wait() → reopen → contents equal; Enabled-after-deletes also asserts the file SHRANK, so "compaction" can't be a silent no-op), plus the dir-listing assertion, the metadata probe, and the s3 try_clone_inner_file streaming shape. Tripwire verified: all 8 FAIL with the one adapter line disabled, all 8 pass with it. Also crates/zerodb/tests/data_file_naming.rs (6: native default regression, configured-name round-trip, the no-adoption/no-probing case, two-names-are-two-envs, invalid-name taxonomy, arbitrary name) and crates/zerodb-tools/tests/data_file_probe.rs (6: both names readable by stat/dump/check, engine line, both-present hard error across all three tools, lock-guard probe, load refusal) and 2 new migrate tests. Oracle (criterion 6): env_lifecycle_differential.rs garbage-file case extended to the adapter name — now a TRUE same-name differential (LMDB and heed-zerodb both handed a garbage file at the identical path; both must say Invalid), which is also the sharpest check that the adapter reads the name it writes: a fallback would ignore the garbage and create a fresh env. Plus a positive case asserting an adapter dir is ["data.mdb"] only and that real LMDB rejects it LOUDLY on the ZDB1 magic rather than misreading it (D-002 holds — the name is shared, the format is not). Through-adapter oracle re-run stayed at zero divergences. One judgment call worth flagging: my first draft of the round-trip comparison included the main DB's F_SUBDATA catalog records, and the two CompactionOption::Enabled cases failed while both Disabled cases passed — the catalog value is a DBRecord holding the sub-tree's ROOT PAGE NUMBER, which a compacting copy legitimately renumbers. Comparing it asserts physical page layout, not the logical equality a snapshot must preserve; the helper now excludes catalog records. Not an engine bug. SPEC 02 §8 layout bullet rewritten (name is opener-chosen, outside the format, resolved once before any write so no SPEC 06 invariant depends on it); SPEC 00 row 7 amended (adapter satisfies the data.mdb half; lock.mdb intentionally absent); README tools section, ADR status → Approved/Implemented with its three open questions resolved inline (knob is pub; no legacy auto-rename; numbering settled). No new deps, no new unsafe, no format change (format_version stays 1). Checks verbatim in handback. Not committed.

ADR-0010 follow-up (same session, 2026-07-20) — adapter-pair fuzz gate turned GREEN; two "engine bugs" root-caused to ONE oracle-harness defect. The ZERODB_FUZZ_PAIR=heed 180s run had surfaced two divergences where LMDB returned EINVAL and the adapter succeeded: (1) IterMutPutCurrent → lmdb Err(io:invalid input parameter) vs adapter Bool(false); (2) Put with an ordinary 14-byte key → lmdb EINVAL vs adapter Ok. Both verified pre-existing (reproduced on the clean tree at 4405153 by stashing), so not ADR-0010 regressions. Root cause (one, not two). Instrumenting the LMDB engine showed the EINVAL in case (1) came from db.iter_mut() = mdb_cursor_open, NOT from put_current — and mdb_cursor_open's only EINVAL path is !TXN_DBI_EXIST(txn, dbi, DB_VALID) (MDB_TXN_BLOCKED returns MDB_BAD_TXN, a different code). mdb_put's first check is the same gate with DB_USRVALID, which is why case (2)'s key size was a red herring — a dead dbi yields EINVAL regardless of key/value. The dbi was dead because of the rule in lmdb.h on mdb_dbi_open: "The database handle will be private to the current transaction until the transaction is successfully committed. If the transaction is aborted the handle will be closed automatically" — implemented by mdb_dbis_update(txn, keep=0) from mdb_txn_end, which clears me_dbflags[i] and bumps me_dbiseqs[i] for every DB_NEW dbi. Additionally mdb_drop(txn, dbi, del=1) calls mdb_dbi_close directly (env-level, NOT undone by a later abort), while del on a core dbi (dbi < CORE_DBS) takes the else-branch and only CLEARS the DB — "Can't delete the main DB". The defect was in the oracle harness, shared verbatim by all three engines: it modeled "which handles survive a rollback" with a positional watermark committed_dbs into the dbs vec, sound only while that vec is append-only. drop_db does dbs.remove(idx) from the middle, after which dbs.truncate(committed_dbs) retains the WRONG set. Traced empirically: dbs=[db0(committed), main(uncommitted)] committed=1 → drop removes idx 0 (db0) → dbs=[main] committed=1 → abort truncates to 1 → keeps main, whose creating txn just aborted (dead handle) while having discarded the committed db0. The harness then issued a use-after-close; LMDB correctly said EINVAL, ZeroDB served it. Fix: replaced the watermark with a per-entry DbEntry.committed flag in all three engines (lmdb.rs, zerodb_engine.rs, heed_zerodb_engine.rs) — mark_dbs_committed() on commit (mirrors mdb_dbis_update keep=1), close_dbs_opened_in_aborted_txn() = dbs.retain(|e| e.committed) on every rollback path (abort, failed commit, nested variants), and drop_db no longer clamps the watermark since the env-level close is permanent. Fixed in the harness, NOT the engine, because that is where the actual defect is: no oracle expectation was changed and no engine behavior was weakened — the harness had simply been generating an API-contract violation, and after the fix both engines are compared on legal usage only. Regression tests: new crates/zerodb-oracle/tests/dbi_handle_lifetime.rs (5 tests, each run against BOTH engine pairs) pinning the exact op shapes — cursor_after_abort_of_txn_that_opened_the_handle and put_after_abort_of_txn_that_opened_the_handle (the two artifacts, minimized), plus positive controls committed_handle_survives_a_later_abort, dropped_handle_is_not_resurrected_by_abort, recreate_after_drop_and_abort. Tripwire verified and precisely targeted: reverting the retain to the old keep-everything behavior fails exactly the two bug tests while the three controls stay green. Residual behavioral question filed as D-013, PROPOSED and explicitly NOT approved: ZeroDB's Database is a plain value, not an env-level dbi slot with a validity flag + generation counter, so it cannot go stale the way LMDB's does. No legal usage observes this (milli/hannoy commit the txn that creates their databases; using a handle from an aborted txn is a consumer bug regardless), and re-imposing it would mean giving the engine a handle registry with generation counters — a design change, not a boundary check, so not done unilaterally. Maintainer decides. Also confirmed the flagged tempfile usage in crates/zerodb/tests/data_file_naming.rs is a FALSE POSITIVE: the only match is the comment "no tempfile dep on the allowlist"; the file uses the in-house TempDir and crates/zerodb/Cargo.toml has no dev-dependencies at all. Full gate re-run green including BOTH fuzz pairs. Not committed.

M2.2/2.3/2.4/2.7 done 2026-07-20 (Phase 2 tranche B) — notes: 2.2 Env::reader_list() (slot / pinned txnid / age, occupied slots only, mdb_reader_list-shaped) + clear_stale_readers() kept returning 0 with the D-001 justification documented rather than removed. 2.3 copy_to_file_with_progress + CopyProgress; heed-parity signatures untouched and delegating, byte-equality-pinned; callbacks all fire before any destination write so a panicking callback leaves no partial copy (tested for both modes, pre-seeded destination). 2.4 safe Comparator trait (object-safe, Send + Sync) + FnComparator closure form, per named DB via {create,open}_database_with_comparator; threaded through leaf_lookup / child_index / Tree / Cursor / RwTxn::search_path / APPEND's last-key test / range termination bounds; main DB (= catalog) and GC tree memcmp by construction. SPEC 03 amended with a new §2.0. Defect found: heed_zerodb::DatabaseOpenOptions::key_comparator::<C>() had silently ignored C since M1.13 — now honored, with DefaultComparator short-circuiting so the consumer default path is unchanged. D-014 filed (PROPOSED): comparators are not persisted (LMDB-identical hazard); a fingerprint needs DBRecord space that does not exist (48/48 bytes assigned, the two spare values reserved for 2.8) — a format change, so an ADR + maintainer call, deliberately not taken. Scope left open and documented: compacting copy refuses a custom-comparator env; check/dump/load stay byte-defined. 2.7 swept SPEC 00's second table: landed max_key_size() (was hardcoded 511 → zerodb::MAX_KEY_SIZE, the M2.1 max_readers defect class) and RoTxn::id()/RwTxn::id(); deferred WithTls real TLS semantics, runtime set_flags, and get_or_put* with reasons (see the session summary). Tests: zerodb/tests/{reader_introspection,copy_progress,custom_comparator}.rs (8+7+13), heed-zerodb/tests/phase2_extensions.rs (+8), zerodb-core cmp unit tests (5).

M2.8 PARKED 2026-07-20 (Quentin's call — "we've been on dupsort since this morning; if it's too complicated let's not do it or revisit later"). Stage 2.8a was implemented and reviewed but NOT landed; the working tree was reverted to 8ab3758 and the full implementation preserved in git stash (stash message names it; git stash apply restores it verbatim).

What is KEPT (committed, durable): the Q5 pin list — 12 fork behaviors observed and pinned as executable tests (crates/zerodb-oracle/tests/dup_pin_semantics.rs, dup_pin_ffi.rs) + SPEC 03 §12.1 + the adjudication in ADR-0011. That is the expensive part and it does not rot: whoever resumes 2.8 starts from observed truth rather than assumptions, including O8 (the fork has NO flags-mismatch check at open, falsifying SPEC 01 §S8 item 8) and O1b (zero-length dup value hits an unsigned underflow in mdb_cursor_put's maxkey guard).

WHY parked, recorded honestly for whoever resumes: no consumer uses DUPSORT (that is why it was descoped from Phase 1 as D-004), and stage A demonstrated the cost of adding it — the dup-aware cursor changed what collect_entries_flagged yields, which silently broke three paths Meilisearch DOES use: copy_to_file(Enabled) (heed's snapshot path) wrote a structurally invalid file while returning Ok; dump→load discarded duplicates with every step reporting success; and the adapter's open re-introduced the register-after-fingerprint-check defect, making a custom-dup-comparator DB permanently unopenable. All three were invisible to a green 546-test suite. Stage 2.8b (sub-tree promotion into the GC lifecycle) raises that risk materially.

RESUME PRECONDITIONS if 2.8 is revived: (1) git stash apply the parked work; (2) fix the three blockers and the two format-fence gaps (SPEC 02 §10.4 leaves upper undefined for the packed sub-page while INV-23 constrains it; §10.1 leaves the promoted sub-tree's embedded DBRecord fields undefined) BEFORE the format freezes; (3) renumber the dup invariants — SPEC 02 §10.6 and SPEC 05 §9 both define INV-22..27 with incompatible meanings, both live in check output; (4) fix the oracle's iteration blind spot (both engines short-circuit on the first Err, so "errored then continued differently" compares equal — it structurally cannot see the get_duplicates step-2 divergence); (5) run both fuzz pairs at the mandated 600s, not 180s.

M3.4 NOT STARTED — premise falsified 2026-07-20, redirected to 3.7. Reading milli's live indexer (update::new::indexer, the path the scheduler runs) showed the PLAN §3.4 consumer premise ("milli's grenad-sorted output replacing APPEND loops") is stale: it describes the legacy index_documents indexer. The live indexer's main write loop is interleaved individual put/del off a bbqueue (unsorted at the write site, values pre-merged); the sole PutFlags::APPEND site (milli facet/bulk.rs:141) is unreachable from the new indexer (it constructs FacetsUpdateBulk::new_not_updating_level_0, delta_data=None, which skips the append branch); facet higher levels are sorted put into a cleared key-sub-range of a populated DB, not an empty tree. Whole-tree bulk_load therefore has NO live milli consumer. Its only live consumers are zerodb-tools load + copy_to_file (both materialise the full env image in RAM) — a real but tooling-scoped win, recorded in PLAN §3.4 for later. This is the scope rule (CLAUDE.md 7, added the same day) working as intended: the falsifying evidence cost ~30 min of reading the consumer, not a 4-crate build.

M3.7 CHOSEN 2026-07-20 (Quentin, after verifying each Phase-3 candidate against consumer source). Prefetch/access hints has a VERIFIED live consumer: hannoy Reader::prefetch_graph (src/reader.rs:447-539) hand-rolls madvise (direct madvise crate + READER_AVAILABLE_MEMORY env var + raw mmap pointers + Windows cfg-out). Chosen over 3.6 (aligned/fixed tables — real but format-adjacent + needs a hannoy rewrite for often-marginal gain) and 3.8 (snapshot reads — no concrete hack to replace). Next: ADR-0012 (Phase-3 ADR-first gate, rule 6) for a safe slice-level RoTxn::will_need(bytes) primitive; NOT implemented until human approval.

BENCH HARNESS added 2026-07-21 (Quentin asked "did you bench vs LMDB?" — answer had been no). crates/zerodb-oracle/benches/engine_comparison.rs (criterion, harness=false; just bench / bench-quick): dual-backend, both linked in one binary (heed=LMDB fork + heed-zerodb), identical op bodies via a macro, seeded data, named DB, page size pinned to the OS page so LMDB (locked to it) and zerodb compare at equal geometry, NO_SYNC groups isolate CPU + a SYNC group for commit cost. zerodb is benched THROUGH the heed-zerodb adapter (what consumers actually see), not the native API. First indicative run (macOS aarch64, 16K OS page, --quick, warm cache — NOT representative of Graviton/EBS): zerodb slower across the board on the unoptimized Phase-1 engine — seq_put 3.4×, rand_put 3.7×, get_rand 5.1×, commit(sync) 2.3×, full scan 29×; overflow_put (2×page values) at parity (~1.0×, memcpy-bound). Gaps are CPU-path (NO_SYNC removes fsync from most groups; bench profile has debug_assertions off so the M1.8 shadow gate is compiled out), pointing at read-path enum/trait indirection (Source/TxnRead), per-page PageRef validation, dirty-store Box<[u8]> allocation, and no cursor page caching — classic correctness-first-no-optimization overhead. NOT a Phase-1 acceptance regression (parity/correctness gates stay green); it quantifies Phase-3 headroom. Note the standout (scan 29×) may include heed-zerodb iterator-wrapping overhead — separating adapter-vs-core is a next step. Harness runs unchanged on Graviton; only the medians move. Not committed.

HANNOY END-TO-END BENCH 2026-07-21 (latest v0.1.7-nested-rtxns, both backends; patch verified via cargo tree — heed→heed-shim→heed-zerodb→zerodb, no "patch not used"). benchmark.rs build_hnsw = time the HNSW graph BUILD over 5000 random vectors (vector insertion is untimed setup; the timed .build() reads vectors from the store to compute distances + writes graph links). Medians (macOS, indicative): DIM 512 : LMDB 895 ms vs zerodb 5.37 s = 6.0x slower DIM 768 : LMDB 1.08 s vs zerodb 5.72 s = 5.3x slower DIM 1536: LMDB 1.47 s vs zerodb 6.26 s = 4.25x slower CORRECTION: I predicted this would land near parity ("distance math dominates, storage barely matters"). WRONG. HNSW build is READ-BOUND — every candidate distance eval fetches that vector from the store (hnsw.rs lmdb.item(id) = a db.get per evaluation), so zerodb's ~5x get overhead (storage microbench) flows straight through. hannoy is one of the MORE storage-read-sensitive consumers, not less. The gap does narrow with dimension (6.0x→4.25x) as the fixed-cost distance math grows and dilutes the storage share — directionally what I claimed, but far weaker: storage still dominates at 1536-dim. Caveat to isolate: hannoy uses the default WithTls env path (a Phase-1 adapter shim), so part of the gap may be adapter-WithTls overhead above the core engine — not yet separated. Lesson: stop predicting end-to-end from workload intuition; measure. milli next (different workload — text extract/tokenize is heavier CPU — but MEASURE, don't assume).

PERF SPIKE step 1 (cursor leaf memoization) 2026-07-21 — ROOT CAUSE of the 29x scan found and fixed. LeafRef::new validates EVERY cell on the page (O(num_keys)); Cursor::next/current reconstructed that view on EVERY step, making a full scan O(num_keys^2) per page instead of O(num_keys) (K~60 at 16K pages/256B values => ~60x redundant validation). Fix: memoize the validated leaf in the cursor keyed by pgno (Cursor::leaf_cache, leaf_at). NO semantic change — every page is still fully validated before any access; this only stops repeating it while the cursor stays on one page. Sound because the cursor holds Source<'a> whose &'a [u8] / &'a DirtyStore are immutable borrows, so the borrow checker forbids any mutation during the cursor's life => a cached view cannot go stale. Deliberately did NOT delete the eager validation: the accessors (key(i)/value(i)) do UNCHECKED slicing and rely on it; removing it would turn corrupt-page handling from a typed error into a panic. RESULT (same-run ratios, macOS --quick): scan_all 29x -> 2.18x (zerodb 26.2ms -> 1.93ms = -92.6%, p<0.05, while the LMDB control was FLAT at 886us, -0.45% p=0.50 — a clean control, so the win is real and not machine drift). get_rand UNCHANGED at ~5.6x (zerodb -0.14%, p=0.80) exactly as predicted — each page is visited once per descent so there is nothing to memoize. Gate: cargo test --workspace exit 0, 81 suites / 494 passed / 0 failed. PROCESS NOTE (my errors, recorded): (1) the replace_all used to swap the leaf-load sites also rewrote the line INSIDE the new leaf_at helper, making it call itself -> stack overflow/SIGABRT in heed-zerodb::env_file_naming; the suite caught it. (2) I first reported "tests green, exit 0" when cargo had actually ABORTED — my shell wrapper echoed the script's exit status (a trailing awk) instead of cargo's. Both fixed; exit code is now captured explicitly. CAVEAT on the other numbers: run-to-run variance is high (LMDB's own seq_put moved -35% between runs with zero code change), so only the controlled scan result is unambiguous. Remaining gaps (same-run): get_rand 5.6x, seq_put 5.3x, rand_put 3.9x, commit 2.0x, overflow_put 1.8x.

PERF SPIKE batch 1 (A1 + A4) 2026-07-21 — A1: RoTxn now memoizes resolved named-DB records per txn (named_memo: Mutex<Vec<(dbi, DBRecord)>> — Mutex not RefCell to keep RoTxn auto-Sync; sound because the pinned snapshot's catalog is immutable (TXN-18/20) and the dbi registry is append-only (M1.6), so (dbi→record) is constant for the txn's life). Kills the per-read registry-lock + name-clone + full catalog descent; RwTxn/NestedRoTxn verified already covered by the open working table BEFORE editing. A4: PageRef::new_trusted_psize skips only the env-immutable validate_page_size re-check in btree::load_page (all per-buffer checks kept; public new unchanged). Gate: fmt 0 / clippy 0 / cargo test exit 0 = 81 suites, 494 passed, 0 failed. Bench (2 runs, ratios within-run since the machine's absolute numbers shifted +30-150% after a sleep/wake — LMDB control itself moved, so only ratios are meaningful): get_rand 5.6x -> 3.54x / 3.87x; scan 2.18x -> 1.96x / 2.11x; commit 1.89x / 1.71x; overflow 1.09x / 0.98x (parity, zerodb faster in run 2). Next (batch 2): A2a txn-scoped validated-pages memo — LeafRef/BranchRef new_prevalidated (O(1) header/bounds checks, skip the per-cell loop) gated by a per-txn Mutex<HashSet>, map-sourced pages only (dirty frames always fully validated — they mutate mid-txn); covers RoTxn AND the Writer map-fallback (map bytes are immutable during a txn — all writes buffer in the dirty store until C2, incl. WRITE_MAP per TXN-45a), which is what hannoy's read-bound build goes through.

PERF SPIKE batch 2 (A2a) 2026-07-21 — txn-scoped validated-pages memo. LeafRef/BranchRef gain new_prevalidated (O(1) structural checks: type flag re-read, reserved tail, bounds; the O(num_keys) cell walk skipped); Source::bytes_from_classified reports map-vs-dirty provenance; memo-aware leaf_view/branch_view in btree.rs threaded Tree->Cursor; every descent/ascend/search site swapped; per-txn ValidatedPages (Mutex<HashSet>) on RoTxn AND RwTxn (nested delegates to parent). SOUNDNESS: only map-sourced bytes enter the memo — a reader's snapshot is GC-pinned (TXN-20/21); a writer buffers every mutation in the dirty store until C2 incl. WRITE_MAP (TXN-45a), so map bytes never change under a live txn; dirty frames are re-checked on EVERY access (a pgno that gains a frame is auto-excluded — entries can be shadowed, never stale-trusted); the type flag is re-read per wrap so leaf/branch confusion is impossible. check.rs/builder/tools/GC tree deliberately keep full validation. Gate: fmt 0 / clippy 0 / test 0 = 81 suites, 494 passed. Bench (--quick, same-run ratios): get_rand 3.7x -> 1.13x (zerodb 22.4ms vs LMDB 19.9ms); scan 2.0x -> 1.38x; writes (untouched this batch — rwtxn's own descents still fully validate): seq_put 5.45x, rand_put 3.23x, commit 1.92x, overflow ~1.0x. READ PATH IS NEAR PARITY; remaining gap is the write path (B1/B2/B3/B6 + rwtxn-local descents). Correctness follow-ups launched before declaring done: just fuzz-quick (600s differential) + cargo miri -p zerodb-core, both running; hannoy end-to-end re-bench queued after (its read-bound build goes through the writer map-fallback, so batch 2 should collapse most of its 4-6x).

HANNOY RE-BENCH after batches 1-2, 2026-07-21 — build UNCHANGED, as the dirty-frame analysis predicts. Valid same-day pairing (first attempt at an LMDB baseline was INVALID — my git stash of the patch silently failed (Cargo.lock untracked -> pathspec error swallowed by >/dev/null) and the run benched zerodb twice; redone with an explicit resolution assertion that ABORTS if heed resolves to the shim. Useful side-effect: two independent zerodb runs agree, ~6.3/6.5/7.5s). Today: LMDB 1.021/1.28/1.509s vs zerodb ~6.3/6.5/7.5s = ~5-6x, same as pre-spike. WHY batches 1-2 don't touch this: the bench (and milli's real vector-store pattern — fill + build in ONE indexing wtxn) reads items from DIRTY FRAMES, which the validation memo excludes by soundness design; batches 1-2 fixed committed-RoTxn reads (hannoy SEARCH + milli query-time — not yet re-measured; speed.rs would show it). BATCH 3 (targets this exact shape): (a) trust engine-authored dirty frames — dirty provenance gets the prevalidated wrap (frames are either COW copies of pages validated at touch, or the engine's own encoder output; disk bytes still fully validated at first map entry — LMDB's own model), 2-line change in leaf_view/branch_view; (b) route rwtxn's ~8 internal descent as_leaf/as_branch sites through the memo-aware views. Then full gate + fuzz-quick + miri + hannoy re-bench.

PERF SPIKE batch 3 (dirty-frame trust) 2026-07-21 — engine-authored dirty frames now get the O(1) prevalidated wrap: (a) leaf_view/branch_view dirty arm -> new_prevalidated (frames are COW copies of pages validated at first map entry this txn, or the txn's own encoder output; disk bytes still fully validate on first map access — LMDB's own-dirty-pages model); (b) rwtxn's search_path/rightmost_path/collect_tree routed through the memo views, page_stats + ALL 20 mutation-helper LeafRef/BranchRef::new(frame..) sites (each verified reading self.dirty.bytes) -> new_prevalidated; load() -> new_trusted_psize. Invariant now: disk bytes cell-validated exactly once per txn at first map entry; engine bytes get structural O(1) checks per wrap; the per-cell walk never repeats; corrupt disk still yields typed errors. Gate: fmt 0 / clippy 0 / test 0 = 81 suites / 494 / 0. Bench (same-run ratios): seq_put 5.45x -> 2.94x (zerodb 90.7 -> 43.5ms, -52% absolute with LMDB flat); rand_put 3.23x -> 1.69x (-43%); get ~1.27x, scan ~1.20x (1.1-1.4 band across runs); commit unchanged 1.92x (fsync/syscall-bound = B4 pwritev, as predicted); overflow ~1.0-1.26x (LMDB run noisy). Everything except commit now < 3x; reads ~1.1-1.3x. fuzz-quick + miri launched (non-negotiable for this batch — it moves the validation boundary furthest); hannoy re-bench after they pass.

HANNOY REFEREE after batch 3, 2026-07-21 — the end-to-end indexing gap collapsed: ~5-6x -> 1.28-1.44x. Same-day valid pairing (resolution asserted before benching): zerodb build_hnsw medians 1.472s / 1.638s / 2.089s (DIM 512/768/1536) vs LMDB 1.021 / 1.28 / 1.509s. Ratios 1.44x / 1.28x / 1.38x — from ~6x/5x/5x before batch 3. Absolute zerodb-side: ~6.5s -> ~1.5s (~4.3x faster) with the LMDB baseline measured the same day. Confirms the dirty-frame thesis end-to-end: hannoy's build is item-reads from dirty frames + link-writes through search/insert, exactly the paths batch 3 untaxed. Correctness stack for the batch: 494 workspace tests + 47,068 differential fuzz runs (0 divergences) + miri (130, 0 UB). Remaining end-to-end gap has named causes from the inventory: B1 (RwCursor owned yields — not on hannoy's path), B2 (split-path owned-cell materialization — hot under inserts), B3 (dirty-store HashMap+Box), B4 (commit pwritev), B6 (adapter ReservedSpace). Spike threshold ("under ~2x or stop") now MET on the consumer workload; microbench: reads 1.1-1.3x, rand_put 1.69x, seq_put 2.94x, commit 1.92x. NOT COMMITTED (three batches + bench harness + ADR-0012 + docs all uncommitted — awaiting maintainer decision).

PERF SPIKE B2+B4 2026-07-21 — B2 (alloc-free leaf splits): split_leaf rewritten with two paths — END-insert fast path (§6.4 ratified rule: append/newindx==nkeys): the LEFT frame is left byte-identical and untouched, the right frame is created with the single new cell (was: extract K owned cells + rewrite both frames); general mid-page path: the old frame is DirtyStore::removed as an owned address-stable Box and both frames pack from BORROWED slices via pack_leaf_range — OwnedLeafCell materialization gone from splits (survives only in delete/merge); RESERVE-in-split zero-fills via the frame's own zero-init (no scratch vec). Dead OwnedLeafCell::used/write_leaf_frame removed. B4 (vectored commits): Backing::write_pages_at with a default that loops write_at_page (FaultBacking journaling + WriteMapBacking memcpy semantics provably unchanged at the trait level); MmapBacking overrides with file::write_pages_vectored — pwritev in <=512-iovec chunks (ONE new unsafe block, zerodb-io = sanctioned crate, SAFETY-commented; short writes restart the chunk with idempotent per-frame pwrite); C2 loop coalesces consecutive pgnos (overflow runs advance by their page span) — LMDB's MDB_COMMIT_PAGES structure. libc added to zerodb-io (allowlisted). Gate: fmt 0 / clippy 0 (after a house-pattern too_many_arguments allow on split_leaf) / test 0 = 81 suites, 494 passed / crash-test-quick 0 violations (mandatory: C2 rewritten). Bench (same-run): seq_put 2.94x -> 1.25x (B2's end-insert fast path — milli/hannoy's dominant insert pattern); rand_put 1.69x -> 1.40x; commit 1.92x -> 1.80x (modest on the page-cache Mac as predicted — B4's real target is EBS syscall/latency); overflow 0.76x and get 0.79x show zerodb FASTER this run but LMDB's own numbers swung (get 16.6->31.6ms across runs, no code change) — honest read: parity region, zerodb's numbers notably STABLE across runs (21-25ms get) while LMDB's swing with machine state; scan 1.27x. fuzz-quick + miri running; hannoy referee after.

HANNOY REFEREE after B2+B4, 2026-07-21 — build medians 1.405 / 1.471 / 1.888s vs LMDB 1.021 / 1.28 / 1.509s = 1.38x / 1.15x / 1.25x (from 1.44/1.28/1.38 after batch 3; from ~5-6x pre-spike). Correctness stack this round: 494 tests + crash-test-quick 0 violations (C2 rewritten) + fuzz-quick 36,635 runs 0 divergences + miri 130/0. Committing B2+B4.

PARITY+FIX 2026-07-21 — adapter page-size default = OS page size (clamped to [MIN,MAX]_PAGE_SIZE; SPEC 00 row 164 amended): LMDB derives me_psize from sysconf(_SC_PAGE_SIZE) (capped 64K), so a store created through the heed surface now gets the fork's geometry (16K on Apple Silicon / 64K-page ARM distros, 4K on x86_64) instead of the native engine's fixed 4K — a heed-observable divergence (Env::stat().page_size) and an unfair handicap in every consumer bench until now. Native zerodb::EnvOpenOptions default unchanged (4096, engine determinism). Contract test omitting_page_size_keeps_the_pre_2_6_default deliberately REWRITTEN to pin the new parity contract (renamed ..._defaults_to_the_os_page_size) — behavior change, not a test weakening; libc dev-dep added to heed-zerodb (allowlisted). The change immediately exposed a latent SIGBUS via the oracle's garbage_data_mdb_error_kind_parity_through_the_adapter: the engine reads meta slot 1 at [psize, 2*psize) THROUGH THE MAP, and a garbage/truncated file whose length leaves that range on OS pages wholly past EOF faults instead of erroring — on a 16K-OS-page Mac it needed DB psize > 4K to fire (hence never seen), but on 4K-page Linux it was reachable all along with any garbage file < 2*psize. Fix in zerodb-io open_or_create: reject non-empty files shorter than 2 * page_size as Mdb(Invalid) BEFORE mapping (SPEC 02 §3.2 step 0, amended) — LMDB parity (it preads the header pre-map, never faults). Native regression test added (16K/64K psize garbage + truncated-real-env shapes). Gate: fmt 0 / clippy 0 / test 0 (full workspace incl. the previously-SIGBUSing differential) / fuzz-quick 44,339 runs 601s 0 divergences / crash-test-quick 0 violations.

PERF SPIKE B1+B6 2026-07-21 — B1 (stack-carrying write cursor): engine RwCursor parks/resumes the read cursor's root-to-leaf stack (btree.rs SavedCursor, park/resume/entry_pos + Tree::entry_at); advance = amortized O(1) stack step, yields = lending borrows via two-phase position-then-rematerialize through the txn validated-pages memo; mutations drop the parked path, record the key (CurPos), next advance re-seeks once (documented residual vs LMDB in-place fixup). Full seek surface added (first/last/ge/gt/le/lt + move_prev + repositioning put). Adapter RwGuts rewritten onto ONE engine cursor (was: per-step db.get_greater_than(txn, last) + last=k.to_vec()); lifetime erased once at construction (sanctioned M1.13 clause; per-yield 'txn stretch SAFETY-commented). New referee: rw_cursor_walks_match_btreemap_model_under_mutation — 40 pseudo-random walks x 60 steps, both directions, put_current/del_current/put interleaved 1-in-3 so park/resume and CurPos-fallback alternate constantly vs a BTreeMap model + final store==model sweep; passes native AND under miri. B6 (in-frame reserved puts): adapter put_reserved wraps the engine's slot directly (alloc+memset+copy gone), zero-fills only the unwritten tail. Fork-pinned SHIPPED-DIVERGENCE FIX bundled: failing closure leaves the entry in place (len=data_size) and errors as Io — observed on the fork via new oracle pin put_reserved_failing_closure_leaves_entry_parity (pre-B6 adapter: no entry + Encoding); SPEC 00 row 35 amended. Gate: fmt 0 / clippy 0 / test 0 (81 suites) / miri 130/0 + 2 cursor tests / fuzz-quick 32,659 runs 644s 0 divergences / crash-test-quick 0 violations. milli end-to-end referee: UNCHANGED — 2.68x -> 2.81x (same-run medians 9.59s vs 26.94s; absolute drift both sides, ratio flat). Hypothesis falsified; sample profiles of both backends mid-run: milli indexing is GET-BOUND on both (LMDB: mdb_node_search 126 + mdb_cmp_memn 126 + cursor_set 89 in 8s window, extractors idle in rayon sleep; zerodb: Cursor::search 209 + ValidatedPages::contains 130 (Mutex+SipHash memo probe = #2 zerodb frame) + Tree::get 120 + view/prevalidated/read_u64 ~500 more) — zerodb get machinery burns ~3-4x LMDB's cycles, matching the 2.7x. Next lever (profile-directed): lock-free insert-only validated-pages set + trusted-on-memo-hit view constructor (+ A3 read_unaligned in leaf_lookup/child_index if cheap).

PERF SPIKE A8 2026-07-21 — profile-directed read/write-path constants; milli end-to-end 2.68x -> 1.19x. Three parts: (1) ValidatedPages memo LOCK-FREE (was Mutex+SipHash, #2 zerodb frame in the milli profile; milli shares one RoTxn across rayon): insert-only open-addressed AtomicU64 slots in geometric OnceLock levels (splitmix64 mix, 1/2 load gate, saturation degrades to revalidation — memo can never affect correctness), Release-publish/Acquire-probe orderings justified inline; std::sync not the loom shim (ADR-0006 scopes the shim to the reader table; memo contract is advisory); 4-thread stress test native+miri. (2) Memo keys KIND-TAGGED (bit 63) -> hits construct views via new_trusted (two raw header reads, zero checks); mismatched kind = miss = loud revalidation. (3) THE milli lever, found by reading the write-phase call tree (91% of main thread in write_to_db; LeafRef::new ~2,100/3,570 samples): LeafMut/BranchMut::from_valid — "wrap an already-validated page" — silently re-ran the FULL O(num_keys) cell walk on EVERY mutation (every put re-walked its target leaf; 4x worse at 16K parity pages); a batch-3 miss inside the mutable wrapper. All 22 sites audited = dirty.bytes_mut frames (engine-authored) -> now O(1) new_prevalidated, matching the read-side dirty path. Also: oracle Active::Ro boxed (RoTxn +~176B inline memo tripped large_enum_variant). Method note for the log: B1+B6 landed first and did NOT move milli (2.68->2.81x flat) — the flat-frame profile aggregation (zerodb-named symbols only) mis-ranked the levers; the call-tree read found from_valid in minutes. Gate: fmt 0 / clippy 0 / test 0 (630) / miri 0 (132 incl. concurrent-memo + cursor model tests) / fuzz-quick 41,161 runs 601s 0 divergences / crash-test-quick 0 violations. millibench referee (median of 3, alternated, same-run): LMDB 9.47s vs zerodb 11.23s = 1.19x (from 2.68x baseline; zerodb -58% absolute); store still 17% denser (503MB vs 604MB). hannoy re-referee pending (get-bound; A8 memo should only help).

HANNOY RE-REFEREE after A8, 2026-07-21 — search_hnsw: zerodb FASTER than LMDB at every dim (medians 2.524/2.734/3.381 ms vs 2.626/2.768/3.699 ms = 0.96x/0.99x/0.91x; first consumer read-path WIN, credit batches 1-3 + A8). build_hnsw THIS RUN inconclusive: LMDB side ran second on a hot machine and swung 2.0-7.0s at dim 1536 (median 5.36s, 12 samples) while zerodb held 2.85-3.17s — by median zerodb was 1.8x FASTER at 1536, not credible either way; last thermally-clean build reference remains 1.38x/1.15x/1.25x (B2+B4 era, pre-A8). Clean alternated build rerun queued behind the B3 work.

PERF SPIKE A3 2026-07-22 — the sanctioned-unsafe card played: read_{u16,u32,u64}_unchecked in page/raw.rs (explicit offsets + ptr::read_unaligned — the exact pattern the unsafe policy prescribes, in its sanctioned home). zerodb-core went forbid(unsafe_code) -> deny + ONE #[allow] scoped to mod raw (+ per-fn allows at call sites), documented at both sites. Converted (each SAFETY-commented against one contract — view construction proves cell bounds via the full walk, trusted views inherit it per batch-3/A8): LeafRef/BranchRef cell_abs/node_flags/key/value/child_pgno + leaf_lookup (the profile's hottest descent loop). Public accessors now HARD-assert i < num_keys (strictly stricter than before: the old checked reads could return a garbage in-bounds offset for a bad index without panicking); internal loops ride the binary-search invariant; debug_asserts keep contracts loud in every test/fuzz build. B3 PARKED same day on profile evidence (ledger note: dirty-store probe <60 samples vs search_path ~1,760; TXN-41 redesign risk not worth <5%). Gate: fmt 0 / clippy 0 / test 0 (630) / miri 0 — the star witness for this batch / fuzz-quick 44,354 runs 601s 0 divergences / crash-test-quick 0 violations. millibench referee: LMDB 7.72s vs zerodb 8.65s = 1.12x (from 1.19x; zerodb -23% absolute this session; store 503MB vs 604MB = 17% denser). Scoreboard: milli 1.12x, hannoy search <1.0x, hannoy build needs clean rerun, micro get/scan/overflow parity, commit ~1.8x (EBS pending).

PERF SPIKE C1 2026-07-22 — streaming compaction: peak RAM O(depth x psize) (~100KB) instead of ~2x env size. Core: PageSink trait (core stays I/O-free; VecSink = in-memory compat) + TreeStream (the batch packer's exact greedy fill + last-two rebalance, push-driven: one leaf scratch + one overflow scratch + <=2 child lists per branch level; overflow runs emitted page-by-page from the caller's borrow) + EnvStream/MainStream (named DBs -> catalog records merge-interleaved into the main push stream in memcmp order) + for_each_entry_flagged (borrowed cursor walk; shares StreamBuildError with the builder so pushes need no error conversion). Engine: copy_to_file(Enabled) rewritten onto the borrowed walk + a positioned-write FileSink into a sibling temp file (<name>.copy-tmp-<pid>), atomically renamed onto path AFTER the last progress callback — the documented M2.3 panic contract ("path untouched") holds verbatim and is now STRONGER (mid-copy process kill cannot leave a half-written dest; the old single fs::write could); durability unchanged (no fsync — mdb_env_copy parity); progress contract untouched (section order moved named-first, which the contract deliberately does not pin). Batch build_*_image wrappers and their whole M1.3 test corpus left byte-identical. Referee: NEW stream_builder_matches_batch_builder differential — mixed corpus (multi-level, single-leaf, empty named DBs on both sides of the keyspace incl. end-of-main drain, multi-page overflow) -> both images check_image-clean, identical logical content with catalogs compared by FOLLOWING them, identical page economy (same counts per kind, same depth; only pgno layout order differs) — passed first run. Residual (ledger): zerodb-tools load/migrate still batch (fine at tool scale). Gate: fmt 0 / clippy 0 (after doc-seam repair + MSRV map_or + for-loop lint) / test 0 (632) / miri 0 (streaming builder is pure logic — fully miri'd) / fuzz-quick 43,674 runs 601s 0 divergences / crash-test-quick 0 violations; copy suites 10/0, oracle copy differential 2/0, tools 15/0.

HANNOY CLEAN RE-REFEREE (A8+A3+C1 state), 2026-07-22 — both sides tight this run, and the FIRST matched-geometry comparison (the page-size parity default landed after the old 1.38/1.15/1.25 reference, which unknowingly ran zerodb-4K vs LMDB-16K). search_hnsw: zerodb FASTER at every dim — 1.971/2.164/2.772 ms vs LMDB 2.083/2.217/2.926 ms = 0.95x/0.98x/0.95x (fastest samples also lower: 1.624 vs 1.743 at 512). build_hnsw: 1.341/1.651/1.990 s vs 0.937/1.108/1.566 s = 1.43x/1.49x/1.27x — the honest 16K-vs-16K build number (the old 1.15x at 768 was partly the accidental 4K-page advantage on this get-heavy workload: fewer cells per leaf_lookup). Scoreboard closes the spike: milli 1.12x, hannoy search <1x, hannoy build 1.3-1.5x, micro get/scan/overflow parity, commit ~1.8x Mac (pwritev target = EBS), store 17% denser, compaction O(depth x psize) RAM. Next levers if the build gap matters: the descent residuals (A5/A6 branch-level caching), and the EBS/Graviton run for B4/commit.

PERF BATCH #9+#19 (descent overhead), 2026-07-22 — the three costs the hannoy-build call TREE named (per-symbol inclusive aggregation, both backends, normalized by the hannoy anchor frames; get-chain ~1.33x LMDB's, matching the 1.3-1.5x build gap): (1) SipHash on EVERY page load inside the write txn — RandomState::hash_one<&u64> under Cursor::page AND AGAIN under leaf_view/branch_view (~3.9k of ~28.7k get-chain samples): the DirtyStore HashMap probe fired TWICE per level (type dispatch + view construction). (2) One malloc+free per Tree::get: RawVec<Box<[u8]>>::grow_one directly under Cursor::search = the cursor stack Vec's first push (identical-code-folding renamed the (u64,usize) monomorphization; ~1.1k stacks + xzm_free 3.8k vs LMDB 1.9k). (3) Per-level view reconstruction (~3k; next-batch material). Landed: #9 PgnoHasher (splitmix64 finalizer, BuildHasherDefault, no new dep; DoS argument: pgno keys are engine-authored) on DirtyStore.frames + GC reclaimed + check walker maps; node_view — one bytes_from_classified per descent level (was 2; leaf level was 3 counting Tree::get's own re-resolution, now 0 extra via the search-populated leaf cache), same treatment in rwtxn search_path/rightmost_path (put descents shared the double probe); error parity preserved (map_page_err maps uniformly to Invalid; wrong-type arm byte-identical). #19/A6 PathStack — inline [(u64,usize); 32] (LMDB CURSOR_STACK; SPEC 03 §4 "Path bound" added), Vec-shaped, overflow = typed depth_exceeded (corrupt u16 depth cannot overrun), park/resume moves by value; load_page/Cursor::page deleted (dead). Gate (final tree): fmt 0 / clippy -D warnings 0 / test 0 (81 suites) / miri 0 / fuzz-quick 37,891 runs 601s 0 divergences / crash-test-quick 0 violations (212 cycles). REFEREE PENDING: the post-batch hannoy run was discarded — interactive machine load (WindowServer 52%, OrbStack 35%), 8-32s tail samples on BOTH sides, both sides 35-55% above their own baselines; per the thermal-contamination protocol no ratio is claimed from it. Rerun queued for a quiet machine.

PERF C3 (tools load streaming), 2026-07-22 — zerodb-tools load now streams the env straight into the data file: tools-side LoadSink (PageSink over positioned writes, mirror of copy.rs's private FileSink) driving EnvStream/MainStream; the whole-image Vec + fs::write are gone. map_size retry preserved (page content is map_size-independent — only metas record it — so an oversized result re-streams once with a covering map_size, the batch build's exact retry minus the buffer). Post-load invariant check kept verbatim (reads the file back — same fs::read the check subcommand uses; parse buffers dropped first so peak = max(parse, check), not the sum). RECORD CORRECTION: migrate-from-lmdb never used the batch builder (it streams via batched write txns) — the C1-residual/ledger claim was half-wrong, now fixed. Facade: zerodb re-exports EnvStream/PageSink/StreamBuildError/FIRST_DATA_PGNO (already-public core items, same family as the existing build_multi_db_image re-export). Batch build_*_image wrappers stay for tests + the stream-vs-batch differential. Addresses the load half of issue #63 (stays open for the double-buffered-writer idea). Gate: fmt 0 / clippy -D warnings 0 / test 0 (81 suites incl. tools acceptance + LMDB round-trip); engine core untouched — the 92a26f0 miri/fuzz(37,891 runs, 0 div)/crash results stand.

BUG #46 (durability), 2026-07-22 — parent-directory fsync on env creation: create_env_file fsynced the data file but never the directory entry, so a crash shortly after creation could leave the content durable but the NAME gone (durable-but-unreachable). Fix: fsync_parent_dir after the file sync — one fsync per env lifetime, zero steady-state cost; SPEC 02 §3.4 gains step 4 (same change). The copy_to_file rename deliberately does NOT get one: mdb_env_copy parity says snapshot durability is the caller's concern — now stated in the copy contract (with the caller recipe: fsync file + parent) instead of being an unexamined omission. Gate: fmt 0 / clippy -D warnings 0 / test 0 (81 suites) / crash-test-quick exit 0 (~200 cycles/s); zerodb-core untouched -> standing miri/fuzz results apply (fsync-at-creation is invisible to the differential oracle: no observable semantic change).

PERF #10 (put_reserved single-descent) + D-015, 2026-07-22 — the B6 residuals evaluated and split: (1) RESOLVED — ReserveLoc::Inline now carries at: Option<(leaf, slot)>: every no-split insert arm (same-size in-place overwrite, plain leaf insert, first-insert; append rides insert_into_leaf) proves the settled cell position it already has in scope, so Database::put_reserved (milli's document-serialization hot path) and the GC's per-commit put_pil do ONE descent instead of two; a split returns None -> key-search fallback. Proof refereed: debug builds cross-check every carried position against a real search (debug_assert_eq), so the entire test battery + fuzz corpus verify it continuously. (2) DEFERRED with the divergence RECORDED — the cursor reserved put (put_current_reserved_with_flags, adapter iterator.rs) heap-buffer emulation measurably diverges from heed 0.22.1/the fork (read from heed src/cursor.rs:333): fork reserves-then-fills (failing closure leaves a garbage-tail entry, error Io; under-fill = Io(UnexpectedEof)); ours fills-then-puts (failing closure keeps the OLD entry, error Encoding; under-fill accepted zero-tailed). No consumer calls this API; a faithful fix = engine-level cursor reserve (new public API, pin-first, M2.8a lesson) — filed as D-015 (PROPOSED, awaiting maintainer) instead of built unilaterally. Gate: fmt 0 / clippy -D warnings 0 / test 0 (81 suites) / crash-quick exit 0 / miri 0 / fuzz-quick 40,656 runs 601s 0 divergences (the differential itself referees placement: a wrong slot = instant content divergence).

REVIEW + FIX PASS 2026-09-09 — whole-project review (first activity since 2026-07-22), then the fixes it justified. Adapter write-iterator parity (heed-zerodb): (1) range_mut/rev_range_mut tested their terminating bound with memcmp while the underlying RwCursor seeks in the database's registered comparator order, so a custom-comparator DB stopped the scan at an arbitrary point (or ran past it) — fixed by a new zerodb::RwCursor::key_cmp() and testing the bound under it, exactly as the read-side RoRange and heed's RwRange (C::compare) do; (2) prefix_iter_mut("") scanned the whole DB where prefix_iter("") returns BadValSize (SPEC 03 §2.1, same MDB_SET_RANGE) — check_read_key now applied; (3) heed::Env::copy_to_file read_to_end'd the entire staged copy before writing (the C1 streaming work never reached this entry point, the one Meilisearch snapshots use) and staged under a predictable name in the shared temp dir — now io::copy through a freshly create_dir'd, mode-0700, drop-cleaned staging directory. Pinned by crates/heed-zerodb/tests/write_iterators.rs (4 tests; (1) and (2) verified to FAIL on the pre-fix tree). Not fixed, needs an oracle pin first (rule 1): RwGuts is not a fused iterator — after the bound is passed the cursor sits on the first out-of-range entry, so del_current/put_current after exhaustion act on it; heed's RwRange has the same shape, but what LMDB does on mdb_cursor_del at EOF has not been observed, so no behavior was changed. Also found, filed here not fixed: copy_raw (CompactionOption::Disabled) still builds a 1× env image in RAM and writes it with one fs::write; BIGDATA puts zero the whole overflow run before overwriting it (rwtxn.rs overflow branch); put_pil and Database::put_reserved are the same 40-line reserve algorithm twice; rwtxn.rs has one 2,148-line impl RwTxn block spanning catalog/alloc/GC/COW/split/rebalance/commit — a mechanical rwtxn/{alloc,gc,insert,delete,commit,cursor}.rs split is overdue; builder.rs keeps two complete packers (eager + streaming) by design, the eager one still clones every key/value. Docs truth pass (34 exact-match edits): ADR-0002 status line → Approved (ratified in 602169c, the file was missed); DECISIONS.md 0010/0011 → Approved (matching their files), stale git mv note dropped; README status paragraph now states which 1.14 exit criteria are open, the real unsafe locations, the real page-size default and the real sign-off queue; CLAUDE.md records the zerodb-tools flock/migrate unsafe as "in use, not ratified", the actual dependency set (heed-traits/heed-types/byteorder, loom, libfuzzer-sys, and the un-ADR'd tempfile dev-dep), heed-shim in the repo map, +nightly for miri, just loom/just stress; PLAN.md rule 5 defers to CLAUDE.md, 3.7 heading says Draft-ADR/no-code instead of ACTIVE, layout gains heed-shim; SPEC 06 REC-10 no longer says both "pending" and "ratified"; SPEC 03 §2.1 row covers prefix_iter_mut; SPEC 00 M2.7 deferred rows dated; seven stale "#![forbid(unsafe_code)]" / "no unsafe here" module comments corrected (core is deny with page::raw opened since A3; zerodb-io::file has the pwritev call; the adapter list gains reserved_space.rs); heed-zerodb/Cargo.toml no longer documents the package = patch form that heed-shim/Cargo.toml says does not work; PERF-GAP "state of play" no longer lists A6/C3 as remaining. CI: .github/workflows/ci.yml added — fmt/clippy/test on ubuntu-24.04 and ubuntu-24.04-arm, miri, loom, fuzz-quick (both arches), crash smoke per PR; stress + 10k crash cycles + 4 h fuzz nightly. Still no 64K-page kernel and no 24 h soak: both need a self-hosted Graviton runner. Gate on this tree (macOS, the oracle crate could not build locally — the cc-1.2.67 extraction in ~/.cargo/registry/src is missing its target/ dir; rm -rf of that one cache dir and a rebuild fixes it): cargo fmt --check clean; cargo clippy -p zerodb-core -p zerodb-io -p zerodb -p heed-zerodb -p zerodb-tools --all-targets -- -D warnings clean; tests on those five crates 351 passed / 0 failed; cargo +nightly miri test -p zerodb-core 134 passed / 0 failed (pre-fix tree; key_cmp is a trivial accessor). The oracle differential suite, fuzz-quick and crash-test-quick were NOT run here — first thing to do after the cache fix.

CONSUMER GATE — Meilisearch v1.53.1 on ZeroDB, 2026-09-09 (first re-verification since the v1.50.0 runs of 2026-07-17/20; first Meilisearch-level LMDB-vs-ZeroDB bench ever). New tooling: scripts/consumer.sh {check,suites,bench} + scripts/bench-compare.py + just consumer-* + docs/CONSUMER-GATE.md; docs/patches/ (absolute-path snippets) retired. The script patches [patch.crates-io] heed = { path = …/heed-shim } into a Meilisearch clone between marker lines, asserts lmdb-master-sys is ABSENT from the dependency tree of the crate under test, runs the suites, and for bench builds meilisearch in release twice (stock LMDB, ZeroDB) and runs the same cargo xtask bench --no-dashboard workloads on each binary (MEILI_PORT retargets the runner when 7700 is held locally). Drop-in (v1.53.1 @ 577f7af28, heed 0.22.1 / hannoy 0.1.3 / arroy 0.6.4 — same pins as the contract): cargo check -p milli -p index-scheduler clean, 0 warnings, ZERO source edits; lmdb-master-sys absent from milli, index-scheduler and the meilisearch binary. Suites on ZeroDB: index-scheduler 80 passed / 0 failed; milli lib 271 passed / 0 failed / 3 ignored (upstream #[ignore]s: prompt template ×2, cutoff degraded-search vector); milli integration 92 passed / 0 failed; doc-tests 1 + 2 passed / 1 upstream-ignored. FIND+FIX LOG: EMPTY. Bench (movies.json indexing run_count 10 + search/movies.json run_count 10, one round LMDB→ZeroDB, Apple M-series laptop, both release builds, Meilisearch's allocator): indexing total 4.486 s (LMDB) vs 4.503 s (ZeroDB) = 1.00x; indexing::write_db::all (the storage-touching phase) 2.583 s vs 2.565 s = 0.99x; indexing::documents::extract::docids_extract 0.86x; merge_and_send_docids 1.03x; search total 16.0 ms vs 16.5 ms = 1.03x (min 8.8 vs 9.4 ms — noise band). Full table: benches/results/2026-09-09-meilisearch-v1.53.1-movies-macos.md. Caveats: laptop, one round, ~32k-document dataset; the production referee is Graviton + EBS and a 1M-document workload (WORKLOADS="workloads/hackernews-*.json" ROUNDS=2). Environment note: the local ~/.cargo/registry/src extraction is corrupted for several crates (cc 1.2.63, rustversion 1.0.22 at least — files missing); the run used an isolated CARGO_HOME in the scratchpad. rm -rf ~/.cargo/registry/src/index.crates.io-* (safe; re-extracted from the .crate archives) fixes the machine.

CONSUMER GATE — hannoy v0.1.7-nested-rtxns on ZeroDB, 2026-09-09. New tooling: scripts/hannoy.sh {suites,bench} + scripts/divan-compare.py + just hannoy-* (same marker-patch / no-LMDB-assertion mechanism as consumer.sh). Suite: 34 passed / 10 failed / 1 ignored on ZeroDB — and IDENTICALLY on stock LMDB (control run, same clone, patch off): all 10 are insta snapshot mismatches on exactly one line, Version { major: 0, minor: 1, patch: 6 } vs patch: 7; the tag bumped the crate version without refreshing snapshots. Upstream, storage-independent; not a ZeroDB finding. (v0.1.3, milli's pin, was 44/0 on ZeroDB in July.) Bench (divan benchmark, one round, Apple M1 Pro): build_hnsw 512/768/1536 = 1.12x / 1.14x / 1.01x (July: 1.27–1.49x); search_hnsw 512/768/1536 = 1.07x / 0.90x / 0.92x (July: 0.95x). Table: benches/results/2026-09-09-hannoy-v0.1.7-macos.md. Same caveats as the Meilisearch run: laptop, one round; ROUNDS=2 alternated and Graviton are the referee.

RELEASE PREP v0.1.0, 2026-09-09 — decision (Quentin, chat): first release = git tag + GitHub release with prebuilt zerodb-tools (ADR-0013 Option A, Proposed), no crates.io for 0.x; ship with the open Phase 1 items (24 h fuzz, Graviton 4K/64K, EBS) listed as known gaps. Three read-only audits ran first (API/metadata, heed+LMDB coverage, security); everything below came out of them. Hygiene (a742aa3): crates/heed-shim/target (306 files, 49 MB, local absolute paths) had been tracked since M1.13 because .gitignore anchored /target — now target/; engine crates 0.0.1→0.1.0 with descriptions; publish = false on heed-zerodb and zerodb-oracle; MSRV 1.78→1.80 (the check-cfg lint key needs cargo 1.80) with a CI job that builds the consumer crates on it (dev-deps excluded: criterion's clap needs 1.85); #![deny(missing_docs)] on the four library crates (passes); zerodb-io's crash-injection backend behind a fault feature (oracle-only); zerodb-tools -V and unknown-option rejection. Adapter surface parity (71a00a7, d19cd41): Database handles carry their env identity and every op asserts it against the txn with heed's exact panic message (before: silent wrong-file reads); MdbError Display = LMDB's mdb_strerror text (pinned against real heed for all 21 variants + Other); range/prefix iterators carry heed's C = DefaultComparator type parameter; DatabaseOpenOptions Copy/Debug/pub new; EnvOpenOptions Debug/PartialEq/Eq; flags from_bits; MDB_NOSUBDIR refused at open (D-016 PROPOSED). Oracle pinned that the fork accepts NO_DUP_DATA/APPEND_DUP on a plain DB exactly as the adapter does (adapter_surface_parity.rs). Meilisearch v1.53.1 re-checked on the shim after each change: compiles, no LMDB in the tree. Security hardening (critical-implementer, spec-reviewed): open refuses (last_pg+1)*ps > file size, roots out of range, and txnid > MAX_COMMITTED_TXNID = RDR_CLAIMED - 2^32; write_txn re-enforces the bound at runtime (spec-review blocker: a few-id margin let a boundary meta reach the sentinels two commits after open); Source carries last_pg and the committed-map resolver refuses pages above it (typed error, not SIGBUS); zero-child branch rejected at decode (PageError::EmptyBranch); checker checked arithmetic + early return + RandomState + depth ≤ 32; write path depth ≤ 32; GC PILs validated (ascending, in range) before reuse and in free_page_count; checked pgno*psize in zerodb-io; writer lock replaced (Mutex<()>+MutexGuard → WriterLock occupied-flag + Condvar) so zerodb::RwTxn: Send is genuinely sound — before, milli's rayon handoff dropped a std guard on a foreign thread (UB/abort class; adapter SAFETY comments rewritten); files 0600, create_new + O_NOFOLLOW on env creation, exclusive randomized staging for compaction (raw copy dest stays create+truncate = heed's copy_to_path contract); max_readers/max_dbs ≤ 2^20; LeafMut/BranchMut::remove return Result; stat counters saturate. Two extra bugs found by the new fuzz_image_open target (meta txnid in the sentinel band → pin-protocol assert; hostile stat counters → overflow panic). Tests: zerodb-core/tests/hostile_input.rs (20), zerodb/tests/hostile_file.rs (7). SPEC 02 §3.2/§4.1, SPEC 03 INV-8, SPEC 04 TXN-6/14/38, SPEC 05 GC-18, SPEC 06 REC-1a amended; D-017/D-018 PROPOSED. Spec review verdict: committable with fixes — B1 (band margin) fixed, S1 (SAFETY comments) fixed, S3 (O_NOFOLLOW) fixed, S4 (raw copy dest) kept as heed parity and documented, S5 (DIVERGENCES) filed; nits taken. Release plumbing: CHANGELOG.md (0.1.0 section = release notes), SECURITY.md, CONTRIBUTING.md, docs/RELEASING.md, docs/COMPATIBILITY.md (every heed item + LMDB feature with status), .github/workflows/release.yml (tag → gate → tools tarballs for 3 targets → GitHub release), CI pinned to SHAs with least privilege, dependabot. Human decisions still open before tagging: ratify ADR-0013 (and its Q1/Q3), D-016/D-017/D-018, plus the pre-existing D-011/013/014/015 and ADR-0009/0012 queue; the zerodb-tools unsafe in CLAUDE.md. Gate on the final tree recorded in the commit message. RELEASE PREP v0.1.0 (cont.), 2026-09-09 — decision (Quentin, chat): publish to crates.io (ADR-0013 Q1 → Option B, superseding the Option A line above). Published: zerodb-core, zerodb-io, zerodb, zerodb-tools at one version with exact-pinned internal deps and full crates.io metadata; never published: heed-zerodb (heed's version line, immutable on crates.io), heed-shim (name taken), zerodb-oracle (links C). release.yml gained a final publish job (cargo publish --workspace --locked, CARGO_REGISTRY_TOKEN secret set by the maintainer); CI runs cargo publish --workspace --dry-run --no-verify per PR. Local dry run of the full workspace publish (package + verify build of each crate) passed; all four names were free on crates.io at the time. The heed drop-in path is unchanged (git [patch] on the shim at a tag). DIVERGENCES sign-off, 2026-09-09 — Quentin (chat) approved D-016 (NO_SUB_DIR refused at open), D-017 (geometry/txnid-validated open, no older-slot fallback under NO_SYNC) and D-018 (max_readers/max_dbs ≤ 2^20). No PROPOSED divergence remains that v0.1.0 depends on (RELEASING.md step 3).

BENCH LADDER + first measured run, 2026-09-10 — the dual-backend microbench was one flat file of six groups (seq/rand put, commit, overflow, get, scan): enough to say whether zerodb is slower, never where or why. Rebuilt as a rung-per-mechanism ladder — 48 rungs in 10 suites (env get scan seek put del commit mixed concurrent maint), arranged so adjacent rungs differ by exactly ONE mechanism and a ratio that jumps between two rungs names the cost. Layout crates/zerodb-oracle/benches/engine_comparison/{main,data,backend,harness,suites/*}.rs. Fairness is now enforced by types, not review: backend.rs keeps the single macro body over the two API-identical crate paths, harness.rs adds a Backend trait + generic rung shapes (ro_probe/ro_whole/ro_repeat/ro_span/wr_fresh/wr_loaded) so every timed body is written once, and pair!/pair_shape!/pair_op! name the shape and the operation exactly once per rung — a paste can no longer compare one engine's scan against the other's rev_scan. Tiering is a runtime skip (ZERODB_BENCH_TIER=long adds the 1M-entry depth rung + the concurrent suite), never a cfg, so the long rungs cannot rot. Tooling: just bench [suite] / bench-list / bench-long / bench-quick / bench-report; scripts/bench-report.py pairs the engines per rung, prints ratio = zerodb/lmdb plus each rung's delta against its ladder family's base rung, marks a rung ~ when the two engines are within a std dev, and flags Δ>0.15 rungs. docs/BENCH-MAP.md maps every rung → mechanism → PERF-GAP item, and states how to read a jump. TWO LADDER BUGS FOUND AND FIXED WHILE BUILDING: env/open/create reused one directory for all 20 opens (19 were reopens — it measured the wrong thing), and get/db/named_x8 used sequential probes while its family peers use scattered ones (the ladder step would have measured locality, not DB resolution; fixed via data::round_robin_probes + a permanent hit-count assertion in ro_probe_multi). First run (Apple M1 Pro, 16K pages both sides, default tier, benches/results/2026-09-10-engine-ladder-macos.md): read path at or ahead of parity — get/* 0.93–1.21× (overflow excepted), seek/* 0.89–0.93× (zerodb faster at every probe pattern), scan/full/fwd 1.11×, and txn setup not close: env/txn/ro_begin_abort 0.04×, rw_empty_commit 0.12× (ADR-0006's lock-free reader table). Confirmed closed: put/api/reserved 0.99× and equal to put/api/plain (B6/#10 done), mixed/rw/8dbs (the milli-shaped rung) 0.98×, get/db/named ÷ get/db/root = 1.00 (A1 done and staying done). Gaps cluster in four places, each now a ledger entry: B8 delete+rebalance 1.6–2.7× (del/range/half 2.74× the sharpest; churn/reinsert the least affected, so NOT freelist/B7) — the largest steady-state gap and the only one never predicted from reading mdb.c; B9 env/open/create 10.85×, fully explained and by design (two unconditional fsyncs, data file + parent dir, issue #46; F_FULLFSYNC ≈2.5 ms each on this SSD; once per env lifetime; env/open/reopen is 0.41×); B10 maint/copy/raw 2.77× / compact 4.72× (snapshot latency; the +1.95 step puts it in the rebuild, and copy_raw is still C1's unreached buffered path); and durable commit 1.64–1.92×, the standing B4/EBS row. One inference no single rung could give: the nosync commit family slopes 1.12× → 1.20× → 1.59× as batches shrink, yet the same boundary with ZERO dirty pages is 0.12× — so the per-commit cost is dirty-page write-out (B3/B4), not the commit machinery, and a delete dirties pages the same way, which likely ties B8 to the same mechanism. Two rungs where zerodb looks ~2× FASTER (put/val/v4k 0.48×, v2page 0.41×) fall inside the noise band at 10 samples and are NOT claimed; the read side of that shape goes the other way (get/val/v2page 1.41×, the one clear regression in an otherwise-flat family). Gate: cargo fmt --all --check clean / cargo clippy --workspace --all-targets -D warnings clean / bench binary runs all 48 rungs × 2 engines with 0 panics. macOS is indicative throughout — every fsync-dominated row (commit/sync/*, maint/*, env/open/create) needs the Graviton + EBS gp3 rerun before anything is claimed from it.

B8 DIAGNOSIS (del/range/half 2.74x), 2026-09-10 — followed the ladder's biggest steady-state finding to its mechanism. Method: a throwaway del_probe example (5 variants x 2 engines, best-of-5, 50K entries / 16K pages / NO_SYNC, delete the first 25K keys) plus sample-based profiles differenced against a fill-only spin so the delete phase is isolated from the fill, plus counters on the page primitives. NOT the cause, each ruled out by measurement: commit write-out (delete-without-commit costs the same, 21.2 vs 21.3 ms); the read side (the range scan delete_range does first is FASTER on zerodb, 1.61 vs 2.12 ms); freelist/GC churn (churn/reinsert is the least-affected rung, so not B7); the rebalance trigger (FILL_THRESHOLD 250 permille and minkeys 1 leaf / 2 branch are identical to mdb.c, verified by reading it). Cause, by ablation: same-size overwrite — the delete path's descent + COW with no cell churn — is 274 ns (LMDB) vs 393 ns (zerodb); a full delete is 390 vs 864 ns; so the removal+rebalance step alone is 116 vs 471 ns = 4.1x, and contributes +355 ns of the +474 ns total excess = 75% of the gap. Counted: 25,000 logical deletes issue 63,542 remove_cell calls (2.54/delete) and 44,041 insert_pointer calls (1.76/delete) where a delete needs 1 and 0 — the surplus is rebalance -> borrow_entry round-tripping every moved entry through a full remove_cell + insert_cell page rewrite (via an OwnedLeafCell with key.to_vec() + val.to_vec()), firing on nearly every delete because a borrow moves exactly ONE entry so the next delete re-crosses the threshold; the O(num_keys) pointer-adjust loop therefore runs 4,990,806 times = ~200 per delete at 78.5 keys/page. LMDB runs the same algorithm but moves node bytes page-to-page (mdb_node_move + mdb_node_del), no owned round-trip. Control: deleting every 8th key so fill never nears the threshold drops zerodb to 1.00 remove_cell / 0.00 insert_pointer per delete and the ratio 2.18x -> 1.47x. FALSIFIED, recorded so it is not retried: rewriting remove_cell's pointer loop as LMDB's fused single pass over a bounds-narrowed slice made it slower (23.8 vs 21.3 ms) — per-element indexing loses to copy_within; the lever is doing FEWER cell operations, not cheaper per-element checks. Second, independent finding — B8a: RwCursor::del_current clones the key, calls delete_tree (a fresh root descent), then discards the parked stack so the next move_next does set_range — a SECOND descent — per entry. LMDB's cursor delete is cheaper than its point delete (6.87 vs 9.54 ms; C_DEL resumes in place) while zerodb's is dearer than its own (29.2 vs 21.3 ms) = 4.2x. Database::delete_range dodges it by materializing every key into a Vec and issuing point deletes, but range_mut/del_current is public heed API, so the idiomatic consumer loop gets the 4.2x path. Both written up in PERF-GAP as B8/B8a with the ablation tables; both are design changes (a multi-entry or in-place move_entry borrow; C_DEL-equivalent cursor survival across a delete, which touches SPEC 03 §7) so ADR/spec first, no code. Tree left clean: instrumentation and the probe example reverted, cargo fmt --check clean, cargo clippy --workspace --all-targets -D warnings clean, zerodb-core + zerodb 301 tests / 0 failed.

CONSUMER GATE — delete-heavy workloads, Meilisearch v1.53.1, 2026-09-10 — the 2026-09-09 consumer bench was insert-only (movies.json = add documents + search) and found parity, so B8/B8a had never been exercised where it counts. Workloads chosen by READING milli rather than guessing: del_current turns out to appear at nine call sites in milli, four in the current indexer (IndexingStep::DeletingFromAllFilters, delete_old_fid_word_count_docids, words_prefix_docids, facet/new_incremental, post_processing::prefix::delete_prefixes), and clear_facet_levels calls db.delete_range — so settings-add-remove-filters (removes 2 filterable attributes) and settings-remove-add-swap-searchable hit B8 and B8a directly. Run: v1.53.1 @ 577f7af28 (same pin as 2026-09-09, directly comparable), 150k-people dataset, ROUNDS=2 alternated, run_count 5 => 10 runs/engine, isolated CARGO_HOME, M1 Pro. VERDICT: both findings reach Meilisearch. settings-add-remove-filters end-to-end 1.07x, but the totals hide it — broken out by SELF time the entire regression is ONE span: apply_index_operation 332.8 -> 805.1 ms = 2.42x, +472 ms, which is where the un-instrumented delete_old_fid_from_facet_databases lands (verified: neither it nor reindex's delete section carries a #[tracing::instrument], so their time falls to the caller's self time). Every instrumented phase is parity-or-better: write_db::all 0.96x, extract 0.96x, faceted 0.94x, merge_and_send_facet_docids 0.88x, facet_field_ids::string 0.92x. The 2.42x sits inside the ladder's del/* band (1.97-2.74x) — the microbench predicted the consumer number. Per-round with the order flipped: 1.67x (r1) / 2.45x (r2), zerodb stable at 826/785 ms while LMDB's r1 ran cold at 494 vs 320 ms, so warm-vs-warm is ~2.4x. settings-remove-add-swap-searchable is 0.99x end-to-end (extraction-dominated, write_db::all alone is 5.2 s of 12.6 s) but post_processing::prefix::delete_prefixes — another del_current loop — is 3.63x (3.9 -> 14.1 ms), reproducing at 3.65x/3.60x across both rounds against B8a's 4.2x microbench figure. Blast radius bounded (worst end-to-end 1.07x, on the workload built to trigger it) but linear in entries deleted, so a 10M-doc index removing a filterable attribute pays ~30 s where LMDB pays ~13 s. Unexplained, flagged not claimed: word_pair_proximity_docids_extraction 1.13x/1.11x — stable across rounds so real, but it is an EXTRACTION span and should not depend on the engine; look before assuming benign. Table: benches/results/2026-09-10-meilisearch-delete-heavy-macos.md; PERF-GAP B8/B8a upgraded to CONSUMER-CONFIRMED with the recommendation to take B8a first (larger ratio, more contained, nine consumers on public heed API). No engine code changed; both fixes still gated on ADR (B8) / SPEC 03 §7 amendment (B8a).

SPEC AMENDMENT — B8a cursor-position semantics, 2026-09-10 (spec + pin only, NO engine code). Investigating what to amend turned up that §7 was not the blocker: SPEC 03 §5 rule 4 mandated the slow mechanism — "The M1.4 write cursor tracks its position by key and re-seeks after each of its own mutations" — sitting in a normative list, so the retained-path optimization was spec-forbidden, not merely unimplemented. Two edits. (1) New §5.4a "Position preservation is a contract, not a mechanism": demotes the M1.4 sentence to one permitted mechanism; states the actual requirement (after a mutation through a cursor, that cursor's LOGICAL position MUST be unchanged — the spec does not require the position be recomputed, only that it be correct); lists the permitted mechanisms (re-seek by key / retain the physical path / retain-where-provable and re-derive otherwise); and tabulates the obligations if a path is retained, event by event (COW → remap pgnos; cell shift on the cursor's own leaf → adjust ki; borrow → adjust or discard; merge → repair to the surviving page or discard; root shrink → truncate or discard; poisoned txn → no obligation), noting that discarding is always a correct "repair" and is just the M1.4 mechanism applied selectively. (2) §7 del_current gains the observed position contract as a table plus the note that mechanism is unconstrained and governed by §5.4a. Rule 1 discharged first: the contract had been asserted since M1.4 but NEVER differentially observed — the oracle's Op::IterMutDelCurrent deletes and stops, so only surviving content was compared, never surviving position. New crates/zerodb-oracle/tests/cursor_delete_position.rs: 7 differential cases driving iter_mut/prefix_iter_mut on both engines from ONE macro body, comparing an event trace AND the surviving content — delete mid-page then next (successor), delete last-of-leaf then next (first of next leaf), delete final entry then next (None), full drain (every key once, strictly ascending, tree empty), tail drain (head survives), delete-every-other (cursor stays aligned across alternating delete/advance), prefix drain. Drains span ~2,000 entries at the OS page size so they cross leaf boundaries and force merges mid-walk — the case a retained path is most likely to get wrong. All 7 pass on both engines today, and the fork's literal answers are asserted in-test so a future reader sees the observation, not just that the two agreed. The test was written BEFORE any optimization exists, so per rule 2 it cannot be tuned to fit one. Gate: fmt clean / clippy --workspace --all-targets -D warnings clean / cargo test --workspace 543 passed, 0 failed (up from 536: the 7 new cases). PERF-GAP B8a updated — implementation now unblocked, still the recommended next lever ahead of B8.

B8a IMPLEMENTED, 2026-09-10 — RwCursor::del_current no longer pays two root-to-leaf descents per entry. (1) Delete at the held path: new RwTxn::delete_at_path deletes the entry a path already names; del_current hands it the cursor's parked stack via SavedCursor::frames_mut() (a &mut [(u64, usize)] slice — touch_path/rebalance already take exactly that shape, so it is zero-allocation, and touch_path's in-place pgno rewrite IS the COW remap the parked path needs). delete_tree is now search_path + delete_at_path, so there is one delete implementation, not two. (2) Park on the vacated slot: the deleted slot now holds the successor, so next settles there via the new Cursor::settle (LMDB's C_DEL shape) instead of re-seeking; if the delete vacated the leaf's tail, settle hops to the next leaf. Parking on the vacated slot rather than one before it is deliberate and load-bearing — milli's drain always deletes index 0, so a "park at ki-1" design would never have fired on the workload B8a exists for. (3) Repair rule: rebalance now returns whether it changed the tree's shape (borrow / merge / root shrink); on true the path is discarded and the old re-seek runs, which SPEC 03 §5.4a sanctions as a correct repair. Observable behaviour is unchanged by construction: the vacated slot is invisible except to next — current_key reports None while parked there, so a second del_current or a put_current stays the no-op it was (otherwise it would have silently deleted the SUCCESSOR), and prev discards the path and re-derives from the key exactly as before. NEW BENCH RUNG — the ladder could not see this fix: every pre-existing del/* rung reaches the tree BY KEY (Database::delete / delete_range), so none of them touches the cursor; added del/cursor/drain (range_mut + del_current, milli's shape) plus the cursor_drain backend op. Three-column referee (M1 Pro, 16K pages, medians): del/cursor/drain LMDB 9.22 ms | zerodb-before 31.19 ms (3.38x) | zerodb-after 25.35 ms (2.46x); by-key controls flat, as they must be (range/half 24.31→25.12, bulk/half 24.75→23.58, bulk/all 42.56→42.88). The number that matters is the relationship: draining through the cursor used to cost 28% more than the same span deleted by key (31.19 vs 24.31) where LMDB's cursor drain is cheaper than its by-key delete; after, the two are at parity (25.35 vs 25.12). The cursor-specific penalty is gone; the residual 2.46x is B8, paid by every delete path alike. FUZZER BLIND SPOT CLOSED: Op::IterMutDelCurrent deleted and stopped, so the differential fuzzer compared surviving content but never surviving position — the exact semantics this change touches. Added Op::IterMutDelThenWalk { db, nth, steps, drain } (all three engines) which deletes then keeps walking and returns the observed entries, so post-delete position is now differentially fuzzed. Consequence, flagged: extending Op changes Arbitrary decoding, so Spec::ops_for_round's seeds decode to different sequences and the seed-pinned regression_nometasync_reclaim_clobber_window_is_characterized stopped reaching the clobber window ("got 0 stale fallbacks"). NOT a correctness regression — and note its own guard could not catch it, since gen_spec never touches Op so the mode assertion still passed. Re-pinned to seed 11_834_834_180_059_103_290 (stale_fallback 4, verified 12) with all four assertions unchanged, and the structural reason documented inline for the next person. Gate on the final tree: fmt clean / clippy --workspace --all-targets -D warnings clean / cargo test --workspace 543 passed, 0 failed / miri -p zerodb-core 154 passed, 0 failed, no UB / fuzz-quick 72,532 diff_ops runs 601s, 0 divergences + 4,702,635 image-open runs / crash-test-quick 215 cycles, 0 violations. Still open: the consumer-level confirmation (delete_prefixes 3.63x should fall toward the by-key ratio) needs another scripts/consumer.sh bench run before that claim is made.