Skip to content

feat(user_dict): durable save + recovering open (LXUW hardening)#290

Merged
send merged 1 commit into
mainfrom
feat/lxuw-hardening
Jul 5, 2026
Merged

feat(user_dict): durable save + recovering open (LXUW hardening)#290
send merged 1 commit into
mainfrom
feat/lxuw-hardening

Conversation

@send

@send send commented Jul 4, 2026

Copy link
Copy Markdown
Owner

What

Harden the user dictionary (LXUW) with the same durability/recovery discipline
as the learning history (LXUD), using the shared crate::persist primitives
extracted in #289. This is the minimal, format-invariant hardening from
design §9 — deliberately no WAL/seq/migration (a few-dozen-entry store with
explicit save, where that machinery earns nothing).

  • savepersist::write_atomic (tmp → F_FULLFSYNC → rename → best-effort
    dir fsync). Fixes the unsynced write and the with_extension("tmp") stray
    user_dict.tmp — the two bugs LXUD had already fixed in its own copy.
  • from_bytespersist::bincode_reader_v1 (allow-trailing, matching the
    original plain bincode::deserialize, for this no-CRC/v1-class format) with an
    allocation cap so a corrupt length prefix can't trigger a giant allocation.
  • open_recovering — corruption (bad magic / version / bincode / length) is
    quarantined (.corrupt-<ts>, bytes preserved, newest 3 kept) and an empty
    dictionary is returned. Closes the path where a corrupt file re-threw every
    startup and registered words were lost forever. Err is reserved for
    environmental read failures (EACCES); quarantine-rename failure still yields an
    empty usable dict (loss still surfaced via the report). The strict,
    side-effect-free open stays for the CLI (an audit tool must never rename a
    live user's file — enforced by construction, pinned by a test).
  • FFI/SwiftLexUserDictionary::open no longer throws on corruption;
    open_report() surfaces data_loss_suspected (computed Rust-side).
    EngineContainer records it on the success path (not catch, where it
    would never fire) as a new .userDictionaryDataLoss case, distinct from
    .userDictionary (env failure → dict unavailable) — mirroring the merged
    .history / .historyDataLoss split. This supersedes design §10's older
    "reuse .userDictionary" note (recorded as settled in AGENTS.md).

Scope / compatibility

  • On-disk format unchanged (LXUW magic + version 1 + bincode) — to_bytes is
    untouched, output byte-identical, no migration owed.
  • Depends on refactor(persist): extract shared durable-write + quarantine primitives #289 (the crate::persist extraction); this is PR3b of that 2-PR
    split. Diff here is the user_dict hardening only.
  • SPEC.md gains the symmetric user_dict recovery / offline-tool lines; the
    version-bump policy (§7) is documented on the VERSION const.

Verification (local pre-push gate)

  • cargo fmt --all --check, clippy --workspace --all-features -D warnings
  • cargo test --workspace --all-features594 tests, 0 failed (7 new
    user_dict tests: quarantine, rotation, strict-open-no-side-effect,
    no-stray-tmp, oversized-length cap, clean/nonexistent)
  • mise run build (Swift bindings regenerated) + mise run test-swift
    139 Swift tests, 0 failed
  • msrv 1.88.0, supply-chain screen, cargo deny/vet/machete
  • Design gate: /simplify (4 agents) + /code-review high (correctness +
    conventions) + /lexime-review (Axis 3 structural / 4 UniFFI / 5 context) —
    all 0 findings

Test plan

  • Rust workspace + Swift suites green
  • Corrupt file → quarantined (.corrupt-*, bytes preserved) + empty usable dict + report flags loss
  • Strict open (CLI) still errors on corruption without renaming
  • Durable save leaves no stray .tmp; oversized length prefix rejected
  • CI green

🤖 Generated with Claude Code

Harden the user dictionary (LXUW) with the same durability/recovery
discipline as LXUD, using the shared `crate::persist` primitives — the
minimal, format-invariant hardening from design §9 (no WAL: a few-dozen
entries with explicit `save`, so the WAL machinery earns nothing here).

- **save**: `persist::write_atomic` (tmp → F_FULLFSYNC → rename → best-effort
  dir fsync). Fixes the unsynced write and the `with_extension("tmp")` stray
  `user_dict.tmp` — the two bugs LXUD had already fixed in its copy.
- **from_bytes**: bounded reader (`persist::bincode_reader_v1`, allow-trailing
  for this no-CRC / v1-class format) so a corrupt length prefix can't trigger
  a giant allocation.
- **open_recovering**: corruption (bad magic / version / bincode / length) is
  quarantined (`.corrupt-<ts>`, bytes preserved, newest 3 kept) and an empty
  dictionary is returned — closing the path where a corrupt file re-threw
  every startup and registered words were lost forever. `Err` is reserved for
  environmental read failures (EACCES); quarantine-rename failure still yields
  an empty usable dict. The strict, side-effect-free `open` stays for the CLI.
- **FFI/Swift**: `LexUserDictionary::open` no longer throws on corruption;
  `open_report()` surfaces `data_loss_suspected` (computed Rust-side).
  EngineContainer records it on the success path (not `catch`) as a new
  `.userDictionaryDataLoss` case, distinct from `.userDictionary` (env failure
  → dict unavailable) — mirroring the `.history` / `.historyDataLoss` split.
  This supersedes design §10's "reuse `.userDictionary`" note (settled in
  AGENTS.md).

Format unchanged (LXUW magic + version 1 + bincode) — no migration. SPEC.md
gains the symmetric user_dict recovery/offline-tool lines. Version-bump policy
(§7) documented on the VERSION const.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@send

send commented Jul 4, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 27fb856f74

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@send send merged commit 6d14a99 into main Jul 5, 2026
13 checks passed
@send send deleted the feat/lxuw-hardening branch July 5, 2026 01:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant