SellPL f05836f528
CI / ci (pull_request) Has been cancelled
CI / android (pull_request) Has been cancelled
feat(tests): add the golden decision tables, and fix two engine bugs they found
The parity contract from CLAUDE.md section 7 now exists: 42 cases across 11
scenario groups, with a runner that asserts every case and a coverage check that
asserts every mandatory scenario is claimed.

**A design change, and why.** ADR-16 originally specified JSON tables, justified
by "serde_json will already be in the tree". That was wrong — `rusqlite` does not
pull `serde` by default and `uniffi` has not landed, so nothing provided it, and
`serde` is not a section 3 dependency (rule 0.3). The tables are therefore a Rust
builder DSL: the same declarations, but a typo in a field name is a compile error
rather than a test that silently asserts the wrong thing. For a document whose
entire job is to be a contract, compile-time checking beats diffability.
Converting to JSON later is mechanical if serde is approved.

**Two engine bugs the tables caught, both real:**

1. **Overnight windows woke the daemon immediately.** `time_window_boundary`
   computed the end as `end - minute` and clamped at zero. For a 22:00–06:00
   window active at 23:00, "06:00 today" is in the past, so the boundary became
   *now* — and the daemon would wake, re-evaluate, get the same answer, and wake
   again, forever. Not a missed block, but a busy loop burning the section 5 idle
   budget. Fixed by detecting the late half of an overnight window and crossing
   midnight properly.

2. **An inactive schedule woke a day early.** The next-start search iterated day
   offsets to test membership but computed the delta as `start - minute` only,
   wrapping by 1440 without advancing the day. A Monday–Friday schedule
   evaluated on Saturday reported Monday's start as 21 hours away — Sunday 09:00,
   a day not in the set. The block still fired, because the next evaluation
   recomputed it, but the daemon woke a day early for nothing. Fixed by folding
   the day offset into the delta.

**Four of my own table cases were wrong too**, which is the other half of why
they are worth having: a UTC+5 offset is 15:00 local and therefore *inside*
09:00–17:00 (I had asserted outside); the same for the DST cases. The engine was
right and the test was wrong in all four.

**Coverage bookkeeping is checked, not promised.** `coverage::Location` records
whether a scenario is covered by a table or by a code fixture — clock tampering
and the 24-hour cap are about the passage of time and a bound across lock levels,
not a decision at an instant, so they cannot be expressed as data. Both
directions fail: an uncovered scenario is an error, and so is a `covers` tag that
is not on the mandatory list, which is how a case quietly stops testing what its
name claims.

**`BlockReason` now carries its target.** A schedule may name twenty targets and
they do not all become blocked — some are paused, some belong to a condition that
is not currently active, some are on the never-block list. Without carrying which
target a reason is about, two blocked apps under one schedule produce two
identical reasons and the UI has to re-derive the pairing, which is exactly the
drift `BlockReason` exists to prevent. The tables could not assert per-app
attribution without it.

**The DST gap is recorded, not papered over.** `CivilTime` is an epoch plus an
offset with no timezone database behind it, so the tables pin that a fixed local
window stays fixed as the offset moves — including across a DST transition — but
cannot express a spring-forward time that does not exist or a fall-back time that
is ambiguous. Those are section 7 cases that are genuinely not covered, and
LIMITATIONS.md now says so.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-01 18:29:46 +03:00

Blocker

A cross-platform app and website blocker for Android, Windows and Linux.

One shared Rust core owns every decision. Platform code observes the OS and enforces what the core decides. That is what makes the same schedule behave identically on all three, and it is testable rather than aspirational — the golden decision tables in tests/ are the parity contract, so a platform disagreeing with the core fails the build instead of producing a bug report.

Status

M0 — repository bootstrap. The core engine exists and is tested; nothing enforces anything yet. No platform has a working build.

Milestone State
M0 repo bootstrap in progress
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
M3-Android not started
M4 conditions not started
M5 websites not started
M6 Strict Mode + friction not started
M7 statistics, settings, releases not started

Milestone order is Linux-first rather than Android-first. See ADR-15.

What it will do

  • Schedules of apps, websites and keywords, with time windows, usage limits, launch counts, Wi-Fi and location conditions
  • Blocklist and allowlist modes, with strictest-wins when schedules overlap
  • QuickBlock: block a chosen set for a chosen duration
  • Strict Mode and a configurable friction ladder, so turning blocking off takes deliberate effort in a weak moment while remaining always possible
  • Local usage statistics

What it will not do

This is a friction tool, not a wall. A user with root, ADB, safe mode, a factory reset, or desktop admin rights can always remove it, and that is intended. Factory reset, safe mode and OS recovery are never interfered with. The goal is to outlast an impulse, not to defeat a determined decision.

There is also a hardcoded list of things that can never be blocked — the phone and dialler, emergency calls and alerts, and on desktop the OS shell and the Settings basics needed to keep the machine usable. There is no setting that changes it.

See docs/LIMITATIONS.md for all eight platform limits, stated plainly, plus the shortfalls in the current code.

Build and test

Requires a stable Rust toolchain. The toolchain is pinned in rust-toolchain.toml.

scripts/ci.sh                  # everything, stops at the first failure
scripts/ci.sh --keep-going     # everything, summarises all failures
cargo test --workspace         # tests only
cargo build --workspace        # build only
scripts/size-check.sh          # the 5 MB stripped daemon gate

Build artifacts go to $HOME/.blocker-target, outside the repository, so target/ never shows up in git status and can be deleted in one command. Override with CARGO_TARGET_DIR.

Two checks skip on a machine without the relevant toolchain — Android without an SDK, the secret scan without gitleaks. Both skip loudly and are listed in the summary as not-passes. A skip is never reported as a pass.

Layout

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
desktop/daemon/       long-lived observer and enforcer (5 MB budget)
desktop/ui/           block-screen window, launched on demand
desktop/platform-linux/   ForegroundWatcher backends, enforcement
desktop/platform-windows/ same, Windows
android/              not started
docs/                 architecture, ADRs, limitations, permissions, perf, testing
scripts/              ci.sh, size-check.sh

Documentation

Document What it is for
docs/ARCHITECTURE.md The purity contract, next_wakeup, and why the engine has no dependencies
docs/DECISIONS.md 17 ADRs, including the two still awaiting a decision
docs/LIMITATIONS.md What the OS will not let us do, and what is not built yet
docs/PERMISSIONS.md Every permission, and what degrades without it
docs/PERF.md Section 5 budgets, and the measurements taken so far
docs/TESTING.md The golden-table contract and the manual checklists
CLAUDE.md The project specification. It governs.

Performance budgets

Metric Target
Idle CPU ~0%, no wakeups without events
Idle RAM (daemon) ≤ 15 MB RSS
Block latency ≤ 150 ms
Daemon binary ≤ 5 MB stripped
APK size ≤ 10 MB

Currently measured: 1.364 MB stripped daemon, which includes bundled SQLite. Of the 5 MB budget that leaves 3.8 MB for D-Bus, X11 and a UI toolkit — a real margin rather than a comfortable one. Enforced in CI from the first commit by scripts/size-check.sh.

Licence

Not yet chosen. See ADR-17. Choosing a licence is the owner's decision, and it interacts with the UI toolkit choice in ADR-6 (Slint is GPL-3.0 or commercial), so it is not something to settle by accident.

A note on origin

Blocker is a functional reimplementation of the behaviour of existing app blockers. It reuses no third-party name, logo, icon, copy, or screenshot — only behaviour.

S
Description
No description provided
Readme 1,024 KiB
Languages
Rust 92.9%
Shell 7.1%