# DiskLoc.file_id u16 → u32 Widening Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Widen `DiskLoc.file_id` from `u16` to `u32` so file-id widths agree end-to-end (RAM, cache, comparisons) and the 65535-rotation aliasing ceiling is removed.

**Architecture:** Atomic struct/signature widening of `DiskLoc`, followed by a clean-up sweep of every `as u16` cast feeding `DiskLoc::new` and every `loc.file_id as u32` identity cast (53 sites across 7 files). Plus boundary-pinning tests at the struct layer, the `Shard::read_block` layer, the `BlockCache` layer, the VarTree/VarMap end-to-end layer, and the recovery roundtrip layer.

**Tech Stack:** Rust (workspace), `cargo nextest` for tests, `clippy -D warnings` gate.

**Spec:** `armdb/docs/superpowers/specs/26-05-17-disk-loc-file-id-u32.md`
**Review:** `armdb/docs/superpowers/reviews/26-05-17-disk-loc-file-id-u32.md`

---

## Task 1: Widen `DiskLoc` struct, signature, and all call-sites

**Files:**
- Modify: `armdb/src/disk_loc.rs`
- Modify: `armdb/src/shard.rs` (drop `as u16` at ~792; drop `as u32` at ~367, ~376, ~963)
- Modify: `armdb/src/compaction.rs` (drop `as u16` at ~191, ~217; drop `as u32` at ~303, ~331)
- Modify: `armdb/src/recovery.rs` (drop 12 `as u16` casts)
- Modify: `armdb/src/var_tree.rs` (drop 1 `as u16` cast, 20 `as u32` identity casts incl. one test-only)
- Modify: `armdb/src/var_map.rs` (drop 1 `as u16` cast, 17 `as u32` identity casts)
- Modify: `armdb/src/typed_tree.rs` (drop 5 `as u32` identity casts)
- Modify: `armdb/src/typed_map.rs` (drop 4 `as u32` identity casts)
- Modify: `armdb/src/durability.rs` (drop 2 `as u32` identity casts)
- Modify: `armdb/src/const_tree.rs` (drop `as u16` at ~969)
- Modify: `armdb/src/const_map.rs` (drop `as u16` at ~728)
- Test: `armdb/src/disk_loc.rs` (inline `#[cfg(test)]` module)

**Cascade warning:** Changing `DiskLoc::new`'s signature and the `file_id` field type breaks compilation everywhere casts exist. The casts must all be cleaned up in the same commit so the tree builds again. Treat the call-site sweep as part of the same atomic refactor — do **not** add temporary shims.

- [ ] **Step 1: Write the failing struct-invariant tests**

Append to `armdb/src/disk_loc.rs`:

```rust
#[cfg(test)]
mod tests {
    use super::*;
    use std::mem::{align_of, size_of};

    #[test]
    fn disk_loc_size_is_16() {
        assert_eq!(size_of::<DiskLoc>(), 16);
    }

    #[test]
    fn disk_loc_alignment_is_4() {
        assert_eq!(align_of::<DiskLoc>(), 4);
    }

    #[test]
    fn disk_loc_new_zeroes_padding() {
        let loc = DiskLoc::new(0xDEAD_BEEF, 0x1234_5678, 0x9ABC_DEF0);
        // SAFETY: DiskLoc is repr(C) plain-old-data with no niches.
        let bytes: [u8; 16] = unsafe { std::mem::transmute(loc) };
        // _pad is the last 3 bytes (after shard_id at offset 12).
        assert_eq!(&bytes[13..16], &[0u8, 0u8, 0u8]);
    }

    #[test]
    fn disk_loc_roundtrip_large_file_id() {
        for &fid in &[0u32, 1, 65535, 65536, 65537, 1_000_000, u32::MAX] {
            let loc = DiskLoc::new(fid, 4096, 1024);
            assert_eq!(loc.file_id, fid);
            assert_eq!(loc.shard_id, 7);
            assert_eq!(loc.offset, 4096);
            assert_eq!(loc.len, 1024);
        }
    }
}
```

- [ ] **Step 2: Run tests to verify size test fails**

Run: `cargo nextest run -p armdb disk_loc::tests::disk_loc_size_is_16 -- --nocapture 2>&1 | tail -20`
Expected: build error (cannot transmute `DiskLoc` (12 bytes) to `[u8; 16]`) OR test failure asserting `size_of::<DiskLoc>() == 12 != 16`.

- [ ] **Step 3: Widen the struct and `new` signature**

Replace the full body of `armdb/src/disk_loc.rs` (preserve the new `#[cfg(test)] mod tests` block):

```rust
/// Position of an entry's value on disk.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(C)]
pub struct DiskLoc {
    pub offset: u32,
    pub len: u32,
    pub file_id: u32,
    pub shard_id: u8,
    _pad: [u8; 3],
}

impl DiskLoc {
    #[inline]
    pub fn new(shard_id: u8, file_id: u32, offset: u32, len: u32) -> Self {
        Self {
            offset,
            len,
            file_id,
            shard_id,
            _pad: [0; 3],
        }
    }
}
```

- [ ] **Step 4: Run `cargo check` — expect cascade errors**

Run: `cargo check -p armdb --features var-collections 2>&1 | tail -20`
Expected: ~19 mismatched-type errors from the `as u16` casts now being narrower than the `file_id: u32` parameter (Rust silently allows narrowing, so these may compile fine — the `as u16` cast is now a *widening-less* truncation and is still legal). Plus 53 `clippy::unnecessary_cast` warnings.

If `cargo check` is clean, run `cargo clippy -p armdb --features var-collections -- -D warnings 2>&1 | tail -40` instead — that should fail with `clippy::unnecessary_cast` on every `loc.file_id as u32`.

- [ ] **Step 5: Remove every `as u16` cast feeding `DiskLoc::new`**

Use grep to find them: `grep -rn "as u16" armdb/src/ | grep -v "test"` should narrow it down (then re-include tests as needed).

In each of the 19 sites listed under Files above, change `DiskLoc::new(x as u16, ..)` → `DiskLoc::new(.., x, ..)`. The exact replacement pattern is mechanical: the cast preceded the `,` argument-separator inside a `DiskLoc::new(...)` call. Examples:

```rust
// before
DiskLoc::new(file_id as u16, offset, len)
// after
DiskLoc::new(file_id, offset, len)
```

- [ ] **Step 6: Remove every `loc.file_id as u32` identity cast**

Authoritative checklist: `grep -rn "file_id as u32" armdb/src/`. All 53 sites become identity casts after the widening and trip `clippy::unnecessary_cast`.

Examples:

```rust
// before
if inner.active.file_id == loc.file_id as u32 { ... }
// after
if inner.active.file_id == loc.file_id { ... }

// before
.find(|f| f.file_id == loc.file_id as u32)
// after
.find(|f| f.file_id == loc.file_id)

// before
shard.read_block(loc.file_id as u32, block_offset)
// after
shard.read_block(loc.file_id, block_offset)
```

**Do not remove** the `as u32` casts at `shard.rs:421, 431` — those operate on a `file_id: u32` parameter directly (not from a `DiskLoc`).

- [ ] **Step 7: Run `cargo check` (all relevant feature combos)**

Run in parallel:
```sh
cargo check -p armdb
cargo check -p armdb --features var-collections
cargo check -p armdb --features 'var-collections encryption'
```

Expected: all three exit 0.

- [ ] **Step 8: Run the new struct-invariant tests**

Run: `cargo nextest run -p armdb -E 'test(disk_loc::tests)'`
Expected: 4 tests pass.

- [ ] **Step 9: Run the full armdb test sweep**

Run: `cargo nextest run -p armdb --features var-collections`
Expected: all pre-existing tests still pass. None should regress; nothing in the existing suite depends on `file_id` being narrower than `u32`.

Also run encryption-feature sweep:
```sh
cargo nextest run -p armdb --features 'var-collections encryption'
```

- [ ] **Step 10: Run `cargo clippy -D warnings`**

Run: `cargo clippy -p armdb --features var-collections -- -D warnings 2>&1 | tail -20`
Expected: clean (no `clippy::unnecessary_cast` left).

If anything trips, use `grep -rn "file_id as u32" armdb/src/` to find missed sites.

- [ ] **Step 11: Run `cargo fmt --check`**

Run: `cargo fmt --check`
Expected: clean. If anything diff'd, run `cargo fmt` and re-check.

- [ ] **Step 12: Commit**

```bash
git add armdb/src/disk_loc.rs armdb/src/shard.rs armdb/src/compaction.rs \
        armdb/src/recovery.rs armdb/src/var_tree.rs armdb/src/var_map.rs \
        armdb/src/typed_tree.rs armdb/src/typed_map.rs armdb/src/durability.rs \
        armdb/src/const_tree.rs armdb/src/const_map.rs
git commit -m "$(cat <<'EOF'
refactor(armdb): widen DiskLoc.file_id from u16 to u32

DiskLoc was the last u16 site for file_id in armdb; everything else
(Shard::next_file_id, Active.file_id, ImmutableFile.file_id,
BlockKey.file_id, Shard::read_block) is already u32. The mismatch
forced ~19 `as u16` truncations on writes and ~53 `as u32` widenings
on reads, and would silently alias file IDs past 65535 rotations,
causing reads from the wrong file and BlockCache key collisions.

Widening the field (12→16 bytes, +4 bytes per index entry) removes
the asymmetry entirely. No on-disk migration: DiskLoc is never
serialized; file_id is reconstructed from filenames at recovery.
No replication wire change.

Adds struct-invariant tests (size, alignment, padding-zero, roundtrip
incl. u32::MAX file_id) and cleans up all 19 truncations and 53
identity casts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

## Task 2: `Shard::read_block` boundary tests at file_id > u16

**Files:**
- Modify: `armdb/src/shard.rs` (add `#[cfg(test)]` helper, add tests)

- [ ] **Step 1: Add a `set_next_file_id` test helper**

Find the `impl Shard` block. Locate the existing `#[cfg(test)]` helpers (search for `#[cfg(test)]` near other test-only methods). Add:

```rust
#[cfg(test)]
impl Shard {
    /// Force `next_file_id` to a target value. Used by tests that need
    /// to exercise the file-id space past u16 without writing 65536 files.
    pub(crate) fn set_next_file_id(&self, id: u32) {
        sync::lock(&self.inner).next_file_id = id;
    }
}
```

If a `#[cfg(test)] impl Shard` block already exists, add `set_next_file_id` inside it instead of opening a new one.

- [ ] **Step 2: Write the failing `read_block` boundary test**

Add to the existing `#[cfg(test)] mod tests` (or the equivalent in shard.rs):

```rust
#[cfg(test)]
#[test]
fn read_block_with_file_id_above_u16() {
    use crate::config::Config;

    let dir = tempfile::tempdir().expect("tempdir");
    let mut cfg = Config::test();
    cfg.shard_count = 1;
    cfg.write_buffer_size = 64 * 1024;
    cfg.max_file_size = 4096; // force rotation per entry
    let shard = open_test_shard(dir.path(), 0, &cfg).expect("open shard");

    // Bump next_file_id past the old u16 limit.
    shard.set_next_file_id(70_000);

    // Force rotation so the new active picks up file_id = 70_000.
    shard.rotate_active_for_test().expect("rotate");
    assert!(shard.active_file_id() >= 70_000, "rotation should advance");

    let key = b"abc";
    let value = vec![0xABu8; 256];
    let (disk, _gsn) = shard
        .lock_and_append(key, &value)
        .expect("append entry");
    assert!(
        disk.file_id > u16::MAX as u32,
        "disk.file_id should be wider than u16 (got {})",
        disk.file_id,
    );

    // Force flush + rotation so the value lands on an immutable file
    // before we read through read_block.
    shard.flush_active_for_test().expect("flush");
    shard.rotate_active_for_test().expect("rotate-2");

    let block = shard
        .read_block(disk.file_id, disk.offset as u64 & !4095)
        .expect("read_block");
    let start = (disk.offset & 4095) as usize;
    let end = start + value.len();
    assert_eq!(&block[start..end], &value[..]);
}
```

**Helper names to verify** before writing (they may already exist with slightly different names):
- `open_test_shard` — search `grep -n "fn open_test_shard\|fn open_shard_for_test" armdb/src/shard.rs`
- `rotate_active_for_test` — search `grep -n "fn rotate.*test\|fn rotate_active" armdb/src/shard.rs`
- `flush_active_for_test` — search `grep -n "fn flush.*test\|fn flush_active" armdb/src/shard.rs`
- `lock_and_append` / equivalent — search `grep -n "fn lock_and_append\|append_entry" armdb/src/shard.rs`

If a test helper has a different name, adapt the test code to match. If a needed helper is missing, add it inside the existing `#[cfg(test)]` block, mirroring nearby helpers' shape.

- [ ] **Step 3: Run test to verify it fails on baseline (sanity check)**

This step is here to confirm the test is meaningful. Temporarily revert Task 1 (or run `git stash` if convenient) and run:

Run: `cargo nextest run -p armdb -E 'test(read_block_with_file_id_above_u16)' --features var-collections`
Expected on baseline: panic or assertion failure (DiskLoc aliases at 70_000 as u16 ≠ 70_000).
Expected on Task-1 branch: pass.

If reverting is awkward, skip this sanity check — the test still pins the new behavior going forward.

- [ ] **Step 4: Write the write-buffer variant test**

Add alongside the previous test:

```rust
#[cfg(test)]
#[test]
fn write_buffer_read_with_file_id_above_u16() {
    use crate::config::Config;

    let dir = tempfile::tempdir().expect("tempdir");
    let mut cfg = Config::test();
    cfg.shard_count = 1;
    cfg.write_buffer_size = 64 * 1024;
    cfg.max_file_size = 1 << 20;
    let shard = open_test_shard(dir.path(), 0, &cfg).expect("open shard");

    shard.set_next_file_id(70_000);
    shard.rotate_active_for_test().expect("rotate");

    let value = vec![0x5Au8; 200];
    let (disk, _gsn) = shard
        .lock_and_append(b"k", &value)
        .expect("append");
    assert!(disk.file_id > u16::MAX as u32);

    // Don't flush — read should hit the write buffer.
    let inner = sync::lock(&shard.inner);
    let bytes = inner
        .write_buf
        .read(disk.offset as u64, disk.len as usize)
        .expect("write-buf read");
    assert_eq!(&bytes[..], &value[..]);
}
```

- [ ] **Step 5: Run both new tests**

Run: `cargo nextest run -p armdb --features var-collections -E 'test(read_block_with_file_id_above_u16) | test(write_buffer_read_with_file_id_above_u16)'`
Expected: both pass.

- [ ] **Step 6: Run clippy + fmt**

Run:
```sh
cargo clippy -p armdb --features var-collections -- -D warnings
cargo fmt --check
```
Expected: clean.

- [ ] **Step 7: Commit**

```bash
git add armdb/src/shard.rs
git commit -m "$(cat <<'EOF'
test(armdb/shard): pin read_block for file_id > u16

Adds set_next_file_id test helper and two tests that bump next_file_id
past 65535, force rotation, and assert that read_block (immutable file
path) and write_buf.read (active write buffer path) both return the
correct bytes. Without the file_id u32 widening these would either
panic or silently read aliased data.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

## Task 3: `BlockCache` disambiguation across u16 boundary

**Files:**
- Modify: `armdb/src/cache.rs` (add test)

- [ ] **Step 1: Write the failing disambiguation test**

Append to `armdb/src/cache.rs`:

```rust
#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::CacheConfig;
    use crate::io::aligned_buf::AlignedBuf;

    #[test]
    fn block_cache_disambiguates_file_ids_across_u16_boundary() {
        let mut cfg = CacheConfig::default();
        cfg.max_size = 1 << 20;
        cfg.estimated_items = 32;
        let cache = BlockCache::new(&cfg);

        let buf_a = Arc::new(AlignedBuf::new_zeroed(4096));
        let buf_b = Arc::new(AlignedBuf::new_zeroed(4096));
        // Mark the buffers so we can tell them apart.
        // SAFETY: AlignedBuf::as_mut_slice is the standard mutation API; we
        // hold the only Arc clones at this point.
        unsafe {
            (*Arc::as_ptr(&buf_a) as *mut AlignedBuf).as_mut().unwrap().as_mut_slice()[0] = 0xAA;
            (*Arc::as_ptr(&buf_b) as *mut AlignedBuf).as_mut().unwrap().as_mut_slice()[0] = 0xBB;
        }

        let key_low = BlockKey { shard_id: 0, file_id: 65_535, block_offset: 0 };
        let key_high = BlockKey { shard_id: 0, file_id: 65_536, block_offset: 0 };

        cache.insert(key_low, buf_a.clone());
        cache.insert(key_high, buf_b.clone());

        let got_low = cache.get(&key_low).expect("low present");
        let got_high = cache.get(&key_high).expect("high present");

        assert_eq!(got_low.as_slice()[0], 0xAA, "low file_id mapped to its own block");
        assert_eq!(got_high.as_slice()[0], 0xBB, "high file_id mapped to its own block");
        assert!(
            !Arc::ptr_eq(&got_low, &got_high),
            "BlockCache must not alias 65535 and 65536",
        );
    }
}
```

**Verify before writing:**
- Inspect `armdb/src/io/aligned_buf.rs` to confirm the correct mutation API (`as_mut_slice`, `fill`, etc.) and the constructor signature (`new_zeroed`, `with_size`, etc.). Adapt the test if names differ.
- If `AlignedBuf` exposes a safer mutate-then-`Arc::new` flow, prefer it over the `Arc::as_ptr` unsafe pattern above. The goal is just to make the two buffers distinguishable.

- [ ] **Step 2: Run test**

Run: `cargo nextest run -p armdb --features var-collections -E 'test(block_cache_disambiguates_file_ids_across_u16_boundary)'`
Expected: pass.

- [ ] **Step 3: Run clippy + fmt**

Run:
```sh
cargo clippy -p armdb --features var-collections -- -D warnings
cargo fmt --check
```
Expected: clean.

- [ ] **Step 4: Commit**

```bash
git add armdb/src/cache.rs
git commit -m "$(cat <<'EOF'
test(armdb/cache): pin BlockKey disambiguation across u16 boundary

Inserts two blocks at (shard 0, file_id 65535) and (shard 0, file_id
65536) and confirms they round-trip independently. Before the
DiskLoc.file_id u32 widening, the index would have collapsed the two
file IDs onto the same DiskLoc, and the cache would silently return
whichever block was inserted last.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

## Task 4: VarTree end-to-end test at file_id > u16

**Files:**
- Modify: `armdb/src/var_tree.rs` (add test)

- [ ] **Step 1: Write the failing e2e test**

Append to the existing `#[cfg(test)] mod tests` block (or the test module in var_tree.rs):

```rust
#[cfg(all(test, feature = "var-collections"))]
#[test]
fn var_tree_get_with_file_id_above_u16() {
    use crate::config::Config;

    let dir = tempfile::tempdir().expect("tempdir");
    let mut cfg = Config::test();
    cfg.shard_count = 1;
    cfg.write_buffer_size = 128 * 1024;
    cfg.max_file_size = 4096; // force rotation per entry
    cfg.cache.max_size = 1 << 20;
    let db = open_test_db(dir.path(), &cfg).expect("open db");

    let tree: VarTree<[u8; 8]> = open_var_tree(&db, "t").expect("open var_tree");

    // Bump next_file_id on shard 0 past the old u16 limit.
    db.shard(0).set_next_file_id(70_000);
    db.shard(0).rotate_active_for_test().expect("rotate");

    let key = 42u64.to_be_bytes();
    let value = vec![0xC3u8; 512];
    tree.put(&key, &value).expect("put");

    // Force the entry onto an immutable file so the read goes through
    // read_value_from_disk_locked / Shard::read_block.
    db.shard(0).flush_active_for_test().expect("flush");
    db.shard(0).rotate_active_for_test().expect("rotate-2");

    let got = tree.get(&key).expect("get").expect("present");
    assert_eq!(got.as_ref(), value.as_slice());
}
```

**Verify helper names** before writing:
- `open_test_db` — search `grep -n "fn open_test_db" armdb/src/`
- `open_var_tree` — most likely `db.open_zero_tree` or `armdb_macros::zero_tree!`; mirror an existing test in var_tree.rs
- `db.shard(0)` — confirm `Db::shard` exists and returns `&Shard` or `Arc<Shard>`; otherwise use whatever accessor existing tests use
- `flush_active_for_test`, `rotate_active_for_test`, `set_next_file_id` — these come from Task 2

If the var_tree test idiom uses macros (`zero_tree!`), adapt the open call accordingly while keeping the test's assertions intact.

- [ ] **Step 2: Run test**

Run: `cargo nextest run -p armdb --features var-collections -E 'test(var_tree_get_with_file_id_above_u16)'`
Expected: pass.

- [ ] **Step 3: Repeat for VarMap**

Append a parallel test to `armdb/src/var_map.rs`:

```rust
#[cfg(all(test, feature = "var-collections"))]
#[test]
fn var_map_get_with_file_id_above_u16() {
    use crate::config::Config;

    let dir = tempfile::tempdir().expect("tempdir");
    let mut cfg = Config::test();
    cfg.shard_count = 1;
    cfg.write_buffer_size = 128 * 1024;
    cfg.max_file_size = 4096;
    cfg.cache.max_size = 1 << 20;
    let db = open_test_db(dir.path(), &cfg).expect("open db");

    let map: VarMap<[u8; 8]> = open_var_map(&db, "m").expect("open var_map");

    db.shard(0).set_next_file_id(70_000);
    db.shard(0).rotate_active_for_test().expect("rotate");

    let key = 7u64.to_be_bytes();
    let value = vec![0xD7u8; 512];
    map.put(&key, &value).expect("put");

    db.shard(0).flush_active_for_test().expect("flush");
    db.shard(0).rotate_active_for_test().expect("rotate-2");

    let got = map.get(&key).expect("get").expect("present");
    assert_eq!(got.as_ref(), value.as_slice());
}
```

Apply the same helper-name verification as Step 1.

- [ ] **Step 4: Run both e2e tests**

Run: `cargo nextest run -p armdb --features var-collections -E 'test(var_tree_get_with_file_id_above_u16) | test(var_map_get_with_file_id_above_u16)'`
Expected: both pass.

- [ ] **Step 5: Run clippy + fmt**

Run:
```sh
cargo clippy -p armdb --features var-collections -- -D warnings
cargo fmt --check
```
Expected: clean.

- [ ] **Step 6: Commit**

```bash
git add armdb/src/var_tree.rs armdb/src/var_map.rs
git commit -m "$(cat <<'EOF'
test(armdb): VarTree/VarMap e2e roundtrip with file_id > u16

Both tests use shard_count=1, write_buffer_size=128 KiB,
max_file_size=4096 to force per-entry rotation, bump next_file_id
past 65535, write a value, flush + rotate to push it onto an
immutable file, then assert that get() returns the right bytes via
the locked disk read path. Before the file_id u32 widening these
would either panic (file not found via aliased id) or return wrong
bytes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

## Task 5: Recovery roundtrip at file_id > u16

**Files:**
- Modify: `armdb/src/recovery.rs` (add test) or the most-canonical recovery test location

- [ ] **Step 1: Locate the existing recovery test pattern**

Run: `grep -n "fn recovery_\|fn recover_" armdb/src/recovery.rs | head -10`

Pick one existing recovery test as the structural template; the new test should mirror its shape (tempdir, Config, write entries, close, reopen, assert) and only differ in that the file IDs cross 65535.

- [ ] **Step 2: Write the failing recovery test**

Append to `recovery.rs` (or wherever recovery tests live):

```rust
#[cfg(test)]
#[test]
fn recovery_handles_file_id_above_u16() {
    use crate::config::Config;
    use std::fs;

    let dir = tempfile::tempdir().expect("tempdir");
    let mut cfg = Config::test();
    cfg.shard_count = 1;
    cfg.write_buffer_size = 64 * 1024;
    cfg.max_file_size = 4096;

    // Phase A: open DB, bump file_id past u16, write some entries, close.
    {
        let db = open_test_db(dir.path(), &cfg).expect("open db");
        db.shard(0).set_next_file_id(70_000);
        db.shard(0).rotate_active_for_test().expect("rotate");
        // ... use the canonical write API matching the chosen test template
        for i in 0u32..4 {
            let key = i.to_be_bytes();
            let value = vec![i as u8; 200];
            // Adapt this call to whichever collection the template uses
            // (e.g., a VarTree opened on the db).
            write_entry_for_test(&db, &key, &value).expect("write");
        }
        // close (Drop)
    }

    // Phase B: reopen, confirm the wide-id entries are reachable.
    {
        let db = open_test_db(dir.path(), &cfg).expect("reopen db");
        for i in 0u32..4 {
            let key = i.to_be_bytes();
            let expected = vec![i as u8; 200];
            let got = read_entry_for_test(&db, &key)
                .expect("read")
                .expect("present");
            assert_eq!(got.as_ref(), expected.as_slice(),
                "entry {} survived recovery with file_id > u16", i);
        }

        // Verify some active or immutable file has file_id > u16.
        let max_fid = db.shard(0).file_ids().into_iter().max().unwrap_or(0);
        assert!(max_fid > u16::MAX as u32,
            "max file_id should exceed u16 (got {})", max_fid);
    }
}
```

**Adapt the helper calls** (`write_entry_for_test`, `read_entry_for_test`) to whatever the chosen recovery-test template uses — VarTree, VarMap, ConstTree, ConstMap, or a low-level write directly through `Shard::lock_and_append`. The exact collection does not matter; what matters is that the entry roundtrips through close + reopen.

- [ ] **Step 3: Run test**

Run: `cargo nextest run -p armdb --features var-collections -E 'test(recovery_handles_file_id_above_u16)'`
Expected: pass.

- [ ] **Step 4: Run clippy + fmt**

Run:
```sh
cargo clippy -p armdb --features var-collections -- -D warnings
cargo fmt --check
```
Expected: clean.

- [ ] **Step 5: Commit**

```bash
git add armdb/src/recovery.rs
git commit -m "$(cat <<'EOF'
test(armdb/recovery): roundtrip entries with file_id > u16

Bumps next_file_id past 65535 before writing, closes, reopens the DB,
and confirms the entries are still reachable and at least one file_id
on disk exceeds u16::MAX. Pins the recovery code path for the new
file-id range so future refactors cannot reintroduce the aliasing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

## Task 6: Final verification

**Files:** none (verification-only)

- [ ] **Step 1: Run all relevant feature combos in parallel**

Run in parallel:
```sh
cargo check -p armdb
cargo check -p armdb --features var-collections
cargo check -p armdb --features 'var-collections encryption'
cargo nextest run -p armdb
cargo nextest run -p armdb --features var-collections
cargo nextest run -p armdb --features 'var-collections encryption'
cargo clippy -p armdb --features var-collections -- -D warnings
cargo clippy -p armdb --features 'var-collections encryption' -- -D warnings
cargo fmt --check
```

Expected: all green.

- [ ] **Step 2: Targeted spot-checks**

Run: `cargo nextest run -p armdb --features var-collections -E 'test(file_id) | test(read_block) | test(disk_loc)'`
Expected: every test from Tasks 1-5 passes; total count ≥ 9.

- [ ] **Step 3: Confirm no `file_id as u16` or `loc.file_id as u32` left**

Run:
```sh
grep -rn "file_id as u16" armdb/src/ || echo "clean: no file_id as u16"
grep -rn "loc\.file_id as u32\|disk\.file_id as u32\|snap\.file_id as u32" armdb/src/ || echo "clean: no DiskLoc.file_id as u32"
```

Expected: both grep commands report "clean" (no matches).

**No commit for Task 6** — verification only.
