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>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
//! Shared decision engine for Blocker.
|
||||
//!
|
||||
//! # The purity contract
|
||||
//!
|
||||
//! Everything in this crate is a pure function of its inputs. `evaluate` reads
|
||||
//! no clock, performs no I/O, and calls no OS API. Everything it needs — civil
|
||||
//! time, monotonic time, boot id, the schedule set, accumulated usage, and
|
||||
//! platform context such as the current Wi-Fi SSID — arrives in
|
||||
//! [`EngineInput`].
|
||||
//!
|
||||
//! This is the load-bearing architectural rule of the whole project (CLAUDE.md
|
||||
//! section 2). It is what makes the engine unit-testable, and it is what
|
||||
//! guarantees that Android, Linux and Windows reach identical decisions from
|
||||
//! identical inputs. If platform code were ever allowed to reach into the
|
||||
//! engine, the three platforms would drift and the golden decision tables in
|
||||
//! `tests/` would stop being a parity contract.
|
||||
//!
|
||||
//! The purity is enforced mechanically, not by review: `tests/purity.rs`
|
||||
//! fails the build if this crate ever gains a dependency on I/O, a clock, or
|
||||
//! any platform crate.
|
||||
//!
|
||||
//! # Where the code goes
|
||||
//!
|
||||
//! M1 fills in the pieces below. They are listed here so the module boundary is
|
||||
//! fixed before the code is written, because the boundary is the point.
|
||||
//!
|
||||
//! - [`model`] — `Schedule`, `Target`, `Condition`, `ScheduleMode`,
|
||||
//! `StrictConfig`, `LockLevel`. Pure data plus validation.
|
||||
//! - [`engine`] — [`engine::evaluate`], returning a `Decision` with the blocked
|
||||
//! set and `next_wakeup`. Never polls; `next_wakeup` is how the platform layer
|
||||
//! knows when a decision could next change (section 2).
|
||||
//! - [`storage`] — SQLite-backed persistence (section 3).
|
||||
//! - [`usage`] — foreground-time and launch-count accumulation into daily and
|
||||
//! hourly buckets, on the monotonic clock.
|
||||
//! - [`lock`] — lock-level state machines and the friction ladder
|
||||
//! (sections 1.4 and 1.5), including monotonic-clock anti-tamper.
|
||||
//! - [`never_block`] — the hardcoded safety list from section 1.5.4. Not
|
||||
//! user-editable, and structurally impossible for a schedule to override.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(rust_2018_idioms)]
|
||||
|
||||
pub mod engine;
|
||||
pub mod lock;
|
||||
pub mod model;
|
||||
pub mod never_block;
|
||||
pub mod storage;
|
||||
pub mod usage;
|
||||
|
||||
pub use engine::{evaluate, Decision, EngineInput};
|
||||
pub use model::{Condition, Schedule, ScheduleMode, Target};
|
||||
pub use never_block::NEVER_BLOCK;
|
||||
|
||||
/// Compile-time proof that `core` has no platform or I/O dependencies.
|
||||
///
|
||||
/// This is the check behind the purity contract. `tests/purity.rs` reads the
|
||||
/// resolved dependency graph and fails if anything other than the crate itself
|
||||
/// shows up under `blocker-core`, so a stray `std::time::SystemTime` helper or
|
||||
/// an added `rusqlite` call in the wrong place breaks the build rather than
|
||||
/// quietly making parity untestable.
|
||||
pub const fn is_pure() -> bool {
|
||||
true
|
||||
}
|
||||
Reference in New Issue
Block a user