610d6b4190
Implements ADR-4 (CLAUDE.md section 3, `rusqlite` bundled). Schedules, lock state, disable attempts and usage buckets now persist across restarts. **Why a new crate rather than a module inside `blocker-core`.** The purity contract says the engine does no I/O, and `tests/purity.rs` enforces that mechanically by failing if `blocker-core` has any dependency at all. Putting rusqlite in `blocker-core` would have forced that check to degrade into a source-text grep — something a careful developer can route around and nobody reads in six months. `blocker-storage` depends on `blocker-core`; the reverse edge does not exist, so the zero-dependency proof stays automatic. Both crates sit in the same layer of the section 2 diagram: storage is shared by every platform and contains no platform code. **Schema is relational, not JSON-in-a-text-column.** `schedule_target` and `schedule_condition` are separate tables because target rows are queried by target, and an index on a JSON string is not a plan. Serialisation is hand-written in `storage/src/codec.rs` — `serde` is not a section 3 dependency, and section 0 rule 3 requires asking before adding one. **Fail open, verified end to end** (section 1.5.4). Corruption moves the damaged file aside and starts clean, keeping the original for diagnosis. Confirmed against the real binary, not only in tests: ``` $ printf 'SQLite format 3\x00 not a database' > .../blocker.db $ blocker-daemon blocker-daemon: database was corrupt and has been moved to .../blocker.db.corrupt; starting clean, so no schedules are active until you re-add them EXIT=0 ``` A truncated file is covered separately — that is what a power loss during a write actually produces, and it fails differently from garbage bytes. **Two bugs this PR's own tests caught:** 1. `prune_usage` anchored its cutoff on `MIN(day_index)`, the oldest row. Since that row is by definition the one being tested, the cutoff always sat before it and the delete matched nothing — the table grew forever, which is the slow disk leak the function exists to prevent. Now anchored on `MAX`. 2. An off-by-one in the same function: with a strict `<` against `newest - keep_days`, the boundary day was kept too, yielding `keep_days + 1` rows. Both are covered by `old_usage_is_pruned`. **Cost, measured rather than assumed:** the stripped daemon goes from 0.279 MB to **1.364 MB** with SQLite actually linked in — bundled SQLite is ~1.1 MB of the 5 MB budget, leaving 3.8 MB for D-Bus, X11 and a UI toolkit. That is a real margin rather than a comfortable one, and it makes ADR-4's bundled-vs-system trade-off a genuine open question rather than a settled one. The M0 figure of 0.279 MB excluded SQLite entirely, because dead-code elimination dropped a crate the placeholder binary never referenced. Co-Authored-By: Claude Code <noreply@anthropic.com>
35 lines
1.3 KiB
TOML
35 lines
1.3 KiB
TOML
[package]
|
|
name = "blocker-storage"
|
|
version = "0.1.0"
|
|
edition = "2021"
|
|
description = "SQLite persistence for Blocker: schedules, lock state, and usage counters."
|
|
license = "UNLICENSED"
|
|
|
|
[dependencies]
|
|
blocker-core = { path = "../core" }
|
|
|
|
# rusqlite with bundled SQLite, per CLAUDE.md section 3. Bundled rather than
|
|
# system-linked because the app ships to machines where libsqlite3 may be absent
|
|
# or a different major version, and a storage-format mismatch that only shows up
|
|
# on a user's machine is not a failure mode worth carrying.
|
|
#
|
|
# THIS CRATE IS NOT DEPENDENT ON BY blocker-core.
|
|
#
|
|
# That is the entire reason this crate exists separately. CLAUDE.md section 2
|
|
# requires the engine to be a pure function with no I/O, and the strongest
|
|
# mechanical check for that is "blocker-core has zero dependencies" — a test
|
|
# that reads Cargo.lock and fails on any entry. Keeping storage in its own crate
|
|
# lets that test stay strict and automatic, instead of degrading into a source
|
|
# grep that someone can route around.
|
|
#
|
|
# Both crates sit in the same layer: storage is shared by every platform and
|
|
# contains no platform code, so this is a refinement of the section 2 diagram,
|
|
# not a departure from it.
|
|
rusqlite = { version = "0.37", features = ["bundled"] }
|
|
|
|
[lints]
|
|
workspace = true
|
|
|
|
[lib]
|
|
name = "blocker_storage"
|
|
path = "src/lib.rs" |