Commit Graph

2 Commits

Author SHA1 Message Date
SellPL f05836f528 feat(tests): add the golden decision tables, and fix two engine bugs they found
CI / ci (pull_request) Has been cancelled
CI / android (pull_request) Has been cancelled
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
SellPL 585fd30440 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>
2026-10-01 15:02:20 +03:00