main
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a43a8e8231 |
docs: record the measured storage cost and the new crate boundary
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> |
||
|
|
610d6b4190 |
feat(storage): add SQLite persistence in a separate crate
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> |
||
|
|
da3ce160b4 |
docs: add Gitea workflows, issue/PR templates, README and licence notice
ci.yml calls `scripts/ci.sh` rather than restating the checks: a workflow that reimplements them becomes a second definition of "done", and the two drift. Android runs as a separate job. It fails when the SDK is absent while ci.sh skips it — a developer should not be blocked by a missing toolchain, but a silently skipped Android build in CI means regressions ship unnoticed. Every action is pinned to an exact version. LICENSE.md records that no licence is chosen yet, and why it is not a formality: the choice is upstream of the UI toolkit decision, since Slint is GPL-3.0. Co-Authored-By: Claude Code <noreply@anthropic.com> |
||
|
|
8a521e760e |
docs: add ADR log, architecture, limitations, permissions, perf, testing
The documentation set CLAUDE.md sections 2, 6, 8 and 10.1 call for, written as records of decisions actually made rather than as a description of intent. **DECISIONS.md** — 17 ADRs. Two are deliberately left open and say so: ADR-6 (the desktop UI toolkit, which section 3 requires be decided on measurement and which needs the owner's call, partly because Slint is GPL-3.0) and ADR-17 (the licence, which is a legal commitment nobody should make on the owner's behalf). Where CLAUDE.md gives a reason for a decision, that reason is recorded; where it does not, the file says the reason is unrecorded rather than inventing one. **LIMITATIONS.md** — all eight section 6 limits plus the section 1.5.4 safety guarantees, stated as facts about the operating systems. The words "unbypassable", "tamper-proof", "guaranteed" and "secure" appear nowhere, per sections 6.1 and 6.7. Ends with a separate list of shortfalls in the *current code* — storage has no I/O, the golden tables cannot yet be parsed, LockState always returns Normal — so a known gap in this implementation is never mistaken for a platform limit. **ARCHITECTURE.md** — the layering, the purity contract and its mechanical enforcement, why `next_wakeup` must be armed over inactive schedules too, the overnight weekday-of-start rule, and the end-to-end flow for one block event. **PERF.md** — targets and results as separate tables, so an aspiration is never read as a measurement. The one real number recorded (0.279 MB stripped daemon) is labelled as nearly meaningless, since the daemon is two print statements. Lists the common causes of idle-wakeup regressions. **PERMISSIONS.md** — per platform, what each permission is for and what degrades without it. Flags the two invasive ones (Android accessibility, extension host permissions) rather than burying them, and records that unprivileged eBPF is *assumed* unavailable here because the sysctls were never read. **TESTING.md** — the golden-table contract, the add-the-case-first rule, and why clock tampering needs code fixtures instead of data. Co-Authored-By: Claude Code <noreply@anthropic.com> |
||
|
|
5c50742a61 |
ci: add ci.sh, size gate, and shared script helpers
`scripts/ci.sh` is the source of truth for what must pass before a PR is mergeable. CLAUDE.md section 10.4 requires the Gitea workflow to run the same checks; the workflow calls this script rather than reimplementing the steps, so the two cannot drift apart. Runs: cargo fmt --check, clippy -D warnings, cargo test, the section 5 size gate, Android lint/assembleRelease, and a gitleaks scan. Verified behaviour, rather than assumed: - **Failures really fail.** Injected a syntax error into `core/src/lib.rs` and confirmed `ci.sh --keep-going` exits 1 and names all four dependent checks; then restored the file and confirmed a clean exit 0. `--keep-going` captures failures per step in a subshell instead of letting `set -e` abort the run, so the summary records what already passed. - **Skips are never passes.** Android and gitleaks both skip on this machine (no SDK, no gitleaks), and each SKIP names what was skipped and why, with the summary listing them separately from the passes. A skip that reads as a green tick is how CI quietly stops testing anything. - **Runs from any CWD.** `cargo` resolves a workspace from the CWD, not from the script's location, so `ci.sh` cds to the repo root. Verified by running it from `/tmp`. - **`cargo` is on PATH.** rustup adds `~/.cargo/bin` via `~/.bashrc`, which a non-interactive shell does not read — so a script invoked from CI or by absolute path failed with "command not found" until `lib.sh` prepends it. - **Size gate resolves absolute paths.** `CARGO_TARGET_DIR` is absolute by default, so joining it onto the repo root produced a doubled path; the default is now only joined when relative. Build artifacts go to `$HOME/.blocker-target`, outside the repo, so `target/` never appears in `git status` and can be deleted in one command. Co-Authored-By: Claude Code <noreply@anthropic.com> |
||
|
|
c723efb186 |
feat(core): add workspace skeleton and the pure decision engine
Establishes the cargo workspace and the shared `blocker-core` crate. The engine is deliberately written before any platform code, because CLAUDE.md section 2 makes it the single source of decision truth and M1 acceptance requires it to be pure. **Workspace** — five crates plus a test crate, zero external dependencies. Section 3 requires every crate to justify its binary-size cost, so M0 adds none. [profile.release] is workspace-only (cargo rejects it in members) and carries the section 5 settings that make the 5 MB stripped budget reachable. **Purity contract** — `evaluate(&EngineInput) -> Decision` reads no clock, no disk and no OS API; everything arrives in the input. `tests/purity.rs` enforces this mechanically by asserting blocker-core has zero dependencies, so a stray I/O reach-in fails the build rather than silently making the golden tables useless as a parity contract. Decisions worth calling out: - **Monotonic time is a separate type from wall time.** Section 1.5.3 requires a system date change never to shorten a lock. Keeping them apart makes that structural rather than a rule to remember. - **`start > end` means overnight.** No `crosses_midnight` flag: the comparison already encodes it and a second field is a second way to be wrong. The weekday for a window that started yesterday evening is yesterday's, which `overnight_window_survives_past_midnight` pins down. - **`next_wakeup` is armed over every non-paused schedule, not only active ones.** Filtering to active conditions first would let the daemon sleep straight through the moment a schedule is due to start. - **Allowlist needs the installed-app inventory.** `EngineInput` carries `installed_apps`; an empty slice fails open rather than guessing. - **Wall-clock correction extends, never shortens, and clamps to the 24h cap.** Extending unbounded would satisfy anti-tamper and violate section 1.5.4. - **`cast_possible_truncation` is denied workspace-wide.** Truncating a millisecond counter would corrupt a lock timer. Verified: 22 tests pass, `cargo fmt --check` clean, `cargo clippy -D warnings` clean, stripped release daemon 0.279 MB against the 5 MB budget. Co-Authored-By: Claude Code <noreply@anthropic.com> |
||
|
|
aa1c582f3c |
chore: add repo hygiene config and project spec
Bootstrap the repository with the shared configuration every other piece depends on: - .gitignore: strict. Keeps build output, cargo caches, Android keystores, signing keys, .env files, local SQLite databases, IDE state and any credential-shaped filename out of history. CLAUDE.md 10.1 requires these never enter the repo. - .editorconfig, rustfmt.toml, clippy.toml: formatting and lint policy. clippy.toml is written in the lints-file form so lints are not duplicated into every crate manifest. CLAUDE.md is committed verbatim; it is the project spec and the parity contract for every platform layer. Co-Authored-By: Claude Code <noreply@anthropic.com> |
||
|
|
81d5122e4b |
chore: initial empty commit
Establishes the main branch so feature branches have a base to merge into. The repository was created empty, so main did not exist and no pull request could target it. This commit is deliberately empty. Nothing lands on main without review (CLAUDE.md 10.2); it exists only so that m0/bootstrap has somewhere to be merged from. Co-Authored-By: Claude Code <noreply@anthropic.com> |