docs: record the measured storage cost and the new crate boundary
CI / ci (push) Has been cancelled
CI / android (push) Has been cancelled

Updates the ADR log, architecture, limitations, perf table and README to match
what now exists, rather than what was expected.

**The perf number was wrong and is now honest.** The M0 figure of 0.279 MB
excluded SQLite entirely — the placeholder binary never referenced the crate, so
dead-code elimination dropped it. With the daemon actually opening a database it
is **1.364 MB**, of which bundled SQLite is ~1.1 MB. That is 1.1 MB of the 5 MB
budget spoken for, leaving 3.8 MB for D-Bus, X11 and a UI toolkit.

This is the first number that makes the section 5 budget feel like a budget
rather than a formality, and it turns ADR-4's bundled-vs-system-linked choice
into a real trade-off with a measured price on both sides.

- ADR-4 marked accepted and implemented, with that trade-off left open.
- ARCHITECTURE.md documents why `blocker-storage` is a sibling crate and not a
  submodule: the dependency direction is what keeps the zero-dependency purity
  proof automatic rather than a grep someone can route around.
- LIMITATIONS.md drops the stale "storage has no I/O" shortfall and records the
  bundled-vs-system question instead, plus the fact that `LockState` still always
  evaluates to Normal.
- README status table and layout updated.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit was merged in pull request #1.
This commit is contained in:
2026-10-01 17:17:11 +03:00
parent 610d6b4190
commit a43a8e8231
5 changed files with 68 additions and 26 deletions
+6 -4
View File
@@ -16,7 +16,7 @@ enforces anything yet. No platform has a working build.
| Milestone | State | | Milestone | State |
|---|---| |---|---|
| M0 repo bootstrap | in progress | | M0 repo bootstrap | in progress |
| M1 core engine | engine, lock timers and safety guarantees written and tested; storage has no I/O yet | | M1 core engine | engine, lock timers and safety guarantees written and tested; SQLite persistence landed in `storage/` |
| M2-Linux | not started — daemon is a placeholder | | M2-Linux | not started — daemon is a placeholder |
| M3-Android | not started | | M3-Android | not started |
| M4 conditions | not started | | M4 conditions | not started |
@@ -74,6 +74,7 @@ summary as not-passes. A skip is never reported as a pass.
``` ```
core/ the decision engine — pure, no OS calls, no dependencies core/ the decision engine — pure, no OS calls, no dependencies
storage/ SQLite persistence (rusqlite, bundled), fail-open recovery
tests/ golden decision tables, and the engine purity check tests/ golden decision tables, and the engine purity check
desktop/daemon/ long-lived observer and enforcer (5 MB budget) desktop/daemon/ long-lived observer and enforcer (5 MB budget)
desktop/ui/ block-screen window, launched on demand desktop/ui/ block-screen window, launched on demand
@@ -106,9 +107,10 @@ scripts/ ci.sh, size-check.sh
| Daemon binary | ≤ 5 MB stripped | | Daemon binary | ≤ 5 MB stripped |
| APK size | ≤ 10 MB | | APK size | ≤ 10 MB |
Currently measured: **0.279 MB** stripped daemon — a placeholder that prints two Currently measured: **1.364 MB** stripped daemon, which includes bundled SQLite.
lines. The meaningful number comes in M2-Linux. Enforced in CI from the first Of the 5 MB budget that leaves 3.8 MB for D-Bus, X11 and a UI toolkit — a real
commit by `scripts/size-check.sh`. margin rather than a comfortable one. Enforced in CI from the first commit by
`scripts/size-check.sh`.
## Licence ## Licence
+10 -3
View File
@@ -182,10 +182,17 @@ APK size cost is unacceptable, which is a measurement before it is a decision.
| `usage` | foreground-time and launch-count accumulation into daily and hourly buckets | | `usage` | foreground-time and launch-count accumulation into daily and hourly buckets |
| `lock` | lock levels, the friction ladder's timing, monotonic anti-tamper | | `lock` | lock levels, the friction ladder's timing, monotonic anti-tamper |
| `never_block` | the section 1.5.4 safety list | | `never_block` | the section 1.5.4 safety list |
| `storage` | the SQLite schema and its fail-open recovery policy | | `storage` | *(separate crate: `blocker-storage`)* the SQLite schema and its fail-open recovery policy |
`storage` has no I/O behind it yet — see `LIMITATIONS.md`. The schema is written `blocker-storage` is a sibling crate, not a submodule. It depends on
and reviewed as text so it can be reviewed before it is compiled. `blocker-core`; `blocker-core` does not depend on it, and depends on nothing at
all. That direction is what lets `tests/purity.rs` keep checking "blocker-core has
zero dependencies" — an automatic proof that the engine cannot do I/O — instead of
the weaker and easily-routed-around alternative of grepping the source for
`rusqlite`.
Both crates occupy the same layer of the diagram above: storage is shared by every
platform and contains no platform code.
## Where the platform crates start and stop ## Where the platform crates start and stop
+33 -11
View File
@@ -25,7 +25,7 @@ which parts to trust.
| [1](#adr-1) | Rust for core and desktop, Kotlin for the Android shell | Accepted | | [1](#adr-1) | Rust for core and desktop, Kotlin for the Android shell | Accepted |
| [2](#adr-2) | One shared core; the engine is a pure function | Accepted | | [2](#adr-2) | One shared core; the engine is a pure function | Accepted |
| [3](#adr-3) | Event-driven with `next_wakeup`, never polling | Accepted | | [3](#adr-3) | Event-driven with `next_wakeup`, never polling | Accepted |
| [4](#adr-4) | SQLite via rusqlite, bundled | Proposed | | [4](#adr-4) | SQLite via rusqlite, in its own crate | Accepted, one trade-off open |
| [5](#adr-5) | uniffi for core-to-Kotlin bindings | Proposed | | [5](#adr-5) | uniffi for core-to-Kotlin bindings | Proposed |
| [6](#adr-6) | Desktop UI toolkit — Slint vs egui | **Proposed, needs owner** | | [6](#adr-6) | Desktop UI toolkit — Slint vs egui | **Proposed, needs owner** |
| [7](#adr-7) | Desktop website blocking mechanism | **Accepted — owner chose extension** | | [7](#adr-7) | Desktop website blocking mechanism | **Accepted — owner chose extension** |
@@ -108,24 +108,46 @@ it is supposed to start blocking.
--- ---
<a id="adr-4"></a> <a id="adr-4"></a>
## ADR-4: SQLite via rusqlite, bundled ## ADR-4: SQLite via rusqlite
**Status:** Proposed — dependency not yet added **Status:** Accepted and implemented, with one trade-off still open
**Context.** Section 3 specifies `rusqlite` with bundled SQLite. Section 1.5.3 **Context.** Section 3 specifies `rusqlite` with bundled SQLite. Section 1.5.3
requires lock and Strict state to live in the core database rather than in requires lock and Strict state to live in the core database rather than in
editable plaintext config. editable plaintext config.
**Decision.** `rusqlite` with `bundled`, storing schedules, lock state, and usage **Decision.** `rusqlite` with `bundled`, in a **separate `blocker-storage` crate**
counters in one file. that depends on `blocker-core`, never the reverse.
**Consequences.** The schema is written and reviewed in **Why the separate crate.** The purity contract says the engine does no I/O, and
`core/src/storage.rs` before the crate is added, so it can be reviewed as text. `tests/purity.rs` enforces that mechanically by failing if `blocker-core` has any
Bundling compiles SQLite from source, which costs build time and binary size but dependency at all. Putting `rusqlite` in `blocker-core` would have forced that
removes a system dependency — a real consideration on the machines this ships to. check to degrade into a source-text grep — something a careful developer can route
around, and something nobody reads in six months. Splitting keeps the check
automatic.
The binary-size cost has not yet been measured against the 5 MB budget. That is Both crates sit in the same layer of the section 2 diagram: storage is shared by
an open item, not a settled fact. every platform and contains no platform code. This is a refinement that makes the
guarantee checkable, not an architectural departure.
**Measured consequences.**
- Bundled SQLite costs **~1.1 MB** of the 5 MB stripped daemon budget (1.364 MB
total, from 0.279 MB without it). That leaves 3.8 MB for D-Bus, X11 and a UI
toolkit — a real margin, not a comfortable one.
- The build compiles SQLite from C source, which is a noticeable first-build cost.
- System-linking would reclaim most of the 1.1 MB at the price of a runtime
dependency on the host's libsqlite3, and a schema-format mismatch that only
appears on a user's machine.
**Still open:** bundled vs system-linked. Measured cost is now known; the decision
is the owner's.
**Schema notes.** Relational (`schedule_target`, `schedule_condition`) rather than
JSON blobs in a text column, 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 (ADR-13).
--- ---
+5 -3
View File
@@ -177,9 +177,9 @@ not mistaken for platform facts.
- **`ProcessBackend` is not implemented.** ADR-14 lists three backends; the KWin - **`ProcessBackend` is not implemented.** ADR-14 lists three backends; the KWin
and X11 paths land in M2-Linux, and the process fallback is weaker than the and X11 paths land in M2-Linux, and the process fallback is weaker than the
spec assumes because of the `/proc/connectors` gap above. spec assumes because of the `/proc/connectors` gap above.
- **`rusqlite` has not been added.** The schema is written and reviewed in - **`rusqlite` vs system-linked SQLite is unresolved** (ADR-4). Bundled is built
`core/src/storage.rs` (ADR-4) but storage has no I/O behind it yet, so nothing and costs ~1.1 MB of the 5 MB daemon budget. System-linking would reclaim most
persists. A restart loses all state. of that but adds a runtime dependency on the host's libsqlite3.
- **Lock state is not yet persisted or enforced.** `core/src/lock.rs` has the - **Lock state is not yet persisted or enforced.** `core/src/lock.rs` has the
timers, the escalation, and the wall-clock correction, all tested. The state timers, the escalation, and the wall-clock correction, all tested. The state
machine is not yet driven from a UI, and `LockState::evaluate` currently returns machine is not yet driven from a UI, and `LockState::evaluate` currently returns
@@ -188,6 +188,8 @@ not mistaken for platform facts.
`tests/src/golden.rs`; parsing needs serde, which is a dependency decision `tests/src/golden.rs`; parsing needs serde, which is a dependency decision
awaiting sign-off (ADR-13, ADR-16). The behavioural coverage that exists today awaiting sign-off (ADR-13, ADR-16). The behavioural coverage that exists today
is hand-written unit tests in the same spirit. is hand-written unit tests in the same spirit.
- **`LockState` still always evaluates to `Normal`** — persistence works, but no
UI drives the lock state machine yet.
- **`Condition::UsageLimit` and `LaunchCount` sum across all targets** rather - **`Condition::UsageLimit` and `LaunchCount` sum across all targets** rather
than the schedule's own target set, until targets carry usage keys. than the schedule's own target set, until targets carry usage keys.
- **Windows is entirely unimplemented** and cannot be compiled or tested on the - **Windows is entirely unimplemented** and cannot be compiled or tested on the
+14 -5
View File
@@ -29,17 +29,26 @@ Measured on: Ubuntu 26.04 LTS, KDE Plasma 6.6.6, 6 CPUs. Toolchain: rustc 1.98.1
| Metric | Target | Measured | When | | Metric | Target | Measured | When |
|---|---|---|---| |---|---|---|---|
| Daemon binary (stripped, release) | ≤ 5 MB | **0.279 MB** | 2026-10-01, M0 | | Daemon binary (stripped, release) | ≤ 5 MB | **1.364 MB** | 2026-10-01, M1 |
| Block latency | ≤ 150 ms | not measured | — | | Block latency | ≤ 150 ms | not measured | — |
| Idle CPU | ~0% | not measured | — | | Idle CPU | ~0% | not measured | — |
| Idle RAM (daemon) | ≤ 15 MB RSS | not measured | — | | Idle RAM (daemon) | ≤ 15 MB RSS | not measured | — |
| Idle wakeups | ≤ 1/min | not measured | — | | Idle wakeups | ≤ 1/min | not measured | — |
| APK size | ≤ 10 MB | not measured | — | | APK size | ≤ 10 MB | not measured | — |
The binary figure is real but nearly meaningless right now: the daemon is two **What the 1.364 MB contains, and why it matters.** That figure is with bundled
`println!` lines. The meaningful measurement comes in M2-Linux once enforcement SQLite linked in and the daemon actually opening a database — not the 0.279 MB
exists, and the real risk is at that point — D-Bus, X11, and a UI toolkit are placeholder from M0, which was two `println!` lines and excluded SQLite entirely
where 5 MB would actually be threatened. through dead-code elimination.
So the honest read is: **bundled SQLite costs roughly 1.1 MB of a 5 MB budget.**
That leaves 3.8 MB for D-Bus, X11, and the UI toolkit. It is a real constraint
rather than a comfortable margin, and it is the first number that makes the
section 5 budget feel like a budget instead of a formality.
It also makes ADR-4 (rusqlite bundled vs system-linked) a genuine trade-off rather
than a settled one: system-linking would recover most of that 1.1 MB at the cost
of a runtime dependency on the host's libsqlite3. Recorded as an open question.
## How to measure ## How to measure