alkvault 0.2.0

Local key vault: BIP39 mnemonic generation, SLIP-0010 Ed25519 HD key derivation, AES-256-GCM encryption for securing provider keys, credentials, and identity material
Documentation
---
id: fuzz-cache-opseq-decision
name: Decide on the optional cache_opseq target and ticket the max_entries=0 finding
status: completed
depends_on: []
scope: narrow
risk: low
impact: phase
level: implementation
---

## Description

Resolve plan checklist item 12: decide whether `KeyCache` op-sequence
model fuzzing (`cache_opseq`) is worth a target, and, either way, turn
the existing finding into a proper ticket. This is a decision task:
write down the judgment, do not silently skip.

Known state from `docs/plans/fuzzing.md` (Scope decisions table,
Optional row) and the spec:

- `KeyCache` (`src/cache.rs`) is internal state logic with no
  untrusted bytes; the family precedent (alktty `session_opseq`)
  model-fuzzes this shape anyway.
- One concrete bug is already identified: a `max_entries = 0`
  configuration currently grows past the bound (the eviction
  comparison admits growth where the bound should hold). Verify this
  claim against `src/cache.rs` before writing the ticket: read the
  insert/evict path and, if true, write a failing deterministic test
  as the ticket's evidence. If the bound check has since been fixed,
  note that and close the item as resolved-no-ticket.
- The `service.rs` full lifecycle opseq was already decided Skip
  (overlaps the deterministic suite); `cache_opseq` is the same shape
  on a smaller surface, which is what makes it a real candidate.

Expected outcome is one of:

1. **Implement it now** (same wave): write the shared module with a
   reference model (plain `HashMap` + `Vec` mirror of `order`/
   `entries`), drive `insert`/`get`/`clear` op sequences with bounded
   keys, assert `order`/`entries` consistency, the `max_entries`
   length bound (including the 0 case), and LRU ordering against the
   model; add the target, seeds, replay test, README row; campaign
   runs follow in a later session. Pick this only if the review of
   the cache internals suggests TTL/LRU interleavings beyond the 0
   bound are likely hiding more.
2. **Ticket it and move on**: file the `max_entries = 0` bound bug as
   a ticket either in `docs/architecture/open-questions.md` or a new
   `docs/` ticket per the repo's convention, fix the bound directly
   with a regression test if it is a one-line comparison fix
   (`>` vs `>=` or an explicit zero-guard), and record the
   `cache_opseq` decision as deferred with rationale in the plan doc.

Option 2's fix is in scope regardless of which option is picked when
the fix is small: the bound violation is a real
(though config-reachability-limited, internal-only) bug.

Update `docs/plans/fuzzing.md`: the Scope decisions row and the
checklist item 12 both move to decided state, with session notes.

## Acceptance Criteria

- [x] The `max_entries = 0` growth claim is verified against
      `src/cache.rs` by direct code reading and, if true, by a
      deterministic failing test
- [x] If the bug is real: `max_entries` is honored (a `max_entries =
      0` config inserts and evicts without growing `entries`),
      regression test added, the fix documented in session notes
- [x] The `cache_opseq` go/defer decision is recorded in
      `docs/plans/fuzzing.md` (Scope decisions table + checklist
      item 12 + session notes) with rationale referencing the alktty
      precedent
- [x] Option 1 only: target, seeds, replay test, and README row exist
      and pass; Option 2 only: the ticket exists (or the fix is
      completed) and the deferral is written down
- [x] `cargo test --all-features`, `cargo test`, `cargo clippy
      --all-features --all-targets` pass from the repo root
      (`secp256k1` feature untouched by the cache changes)

## References

- `docs/plans/fuzzing.md` (Scope decisions table, Optional row;
  checklist item 12)
- `src/cache.rs` (the eviction path around `max_entries`)
- alktty `session_opseq` precedent (referenced in the plan)

## Notes

Option 2 picked (ticket/fix and defer). Code review of `src/cache.rs`
confirmed the claim: with `max_entries = 0`, `insert` always took the
`entries.len() >= max_entries` branch, but `evict_lru` found an empty
`order` and evicted nothing, so the cache permanently held one entry
against a zero-entry bound. A comparison toggle does not fix it (a `>`
swap would admit growth outright), so the fix is an explicit
zero-guard: `insert` returns early when `max_entries == 0`, the key is
dropped and zeroized. Regression tests added in `src/cache.rs`:
`test_cache_max_entries_zero_disables_cache` (deterministically failed
before the fix) and `test_cache_max_entries_one_holds_single_lru_entry`
(edge pin). No separate ticket file: the fix is completed, which the
acceptance criteria treat as satisfying the ticket requirement. The
deferral rationale (structural consistency of `order`/`entries`,
deterministic-suite coverage of the interleavings, low residual value
on this surface, alktty `session_opseq` named as the shape to revisit)
is recorded in `docs/plans/fuzzing.md` Scope decisions row, the
Targets section note, and session 4 notes; checklist item 12 checked.
The fuzz/shared replay and `secp256k1` feature are untouched.

## Summary

Verified the `max_entries = 0` bound bug by code reading, fixed it in
`src/cache.rs` with a zero-guard plus two regression tests, deferred
the `cache_opseq` fuzz target with written rationale in
`docs/plans/fuzzing.md` (Scope decisions row, Targets note, checklist
item 12, session 4 notes), and added a CHANGELOG 0.2.0 entry for the
fix. Verification: 114/107 tests (all-features/default) pass, clippy
clean, docs clean, shared fuzz replay 8/8 pass.