M1: SQLite persistence in a separate crate (ADR-4) #2
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user