9a5c529a8e
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>
66 lines
2.5 KiB
TOML
66 lines
2.5 KiB
TOML
[workspace]
|
|
resolver = "2"
|
|
members = [
|
|
"core",
|
|
"storage",
|
|
"tests",
|
|
"desktop/daemon",
|
|
"desktop/ui",
|
|
"desktop/platform-linux",
|
|
"desktop/platform-windows",
|
|
]
|
|
|
|
# Release profile is workspace-wide (section 5 of CLAUDE.md). Cargo rejects
|
|
# [profile.*] in member manifests, so this lives here and nowhere else.
|
|
#
|
|
# The combination is deliberate: `z` optimises for size, LTO + codegen-units=1
|
|
# let the linker drop everything unreachable, and panic="abort" removes
|
|
# unwinding tables entirely. Together they are what make the 5 MB stripped
|
|
# daemon budget (section 5) reachable. scripts/ci.sh enforces the size gate.
|
|
# Workspace-wide lints. These apply to every member crate.
|
|
#
|
|
# They are set here rather than in clippy.toml because they must apply to the
|
|
# compilation — clippy.toml only accepts bare key=value pairs, and a `[lints]`
|
|
# table there is rejected outright. crates.io-wide intent is the same: these
|
|
# encode CLAUDE.md section 0 rule 5 (every fallible OS call handled, no
|
|
# unwrap/expect outside tests) and the section 1.5.3 arithmetic care.
|
|
[workspace.lints.rust]
|
|
# Rustc lints only. NOTE: `integer_conversions` is NOT a rustc lint name and
|
|
# cargo rejects it here as an unknown lint, so the u64 -> u32 truncation concern
|
|
# is expressed as the clippy lint below, which is the check that actually exists.
|
|
unsafe_code = "forbid"
|
|
|
|
[workspace.lints.clippy]
|
|
# Panicking in a release build leaves enforcement down. Section 1.5.4 requires
|
|
# failing open with a log line, not aborting.
|
|
panic = "deny"
|
|
# `unwrap`/`expect` on an OS or I/O result is forbidden outside tests (section 0
|
|
# rule 5).
|
|
unwrap_used = "deny"
|
|
expect_used = "deny"
|
|
# Silent u64 -> u32 truncation in usage counters and monotonic deltas is a real
|
|
# bug class in this codebase, not a style issue: an overflow there would corrupt
|
|
# a lock timer, which section 1.5.3 depends on.
|
|
cast_possible_truncation = "deny"
|
|
# Stack waste in the evaluator, which runs on every focus change.
|
|
large_enum_variant = "warn"
|
|
# Clarity over cleverness; section 5 budgets are measured, not guessed.
|
|
too_many_arguments = "warn"
|
|
|
|
[profile.release]
|
|
opt-level = "z"
|
|
lto = true
|
|
codegen-units = 1
|
|
panic = "abort"
|
|
strip = true
|
|
|
|
# Debug builds keep debug assertions and overflow checks — the engine does
|
|
# arithmetic on millisecond counters and monotonic deltas, and overflow in a
|
|
# release build would corrupt a lock timer (section 1.5.3).
|
|
[profile.dev]
|
|
opt-level = 0
|
|
debug = true
|
|
|
|
# Tests are compiled without panic=abort so that should_panic works.
|
|
[profile.test]
|
|
opt-level = 1 |