//! The golden decision tables (CLAUDE.md section 7). //! //! A table is a list of cases. Each case is schedules plus an input plus an //! expected decision, and running the same table against the engine must //! reproduce the expectation exactly. //! //! # Format //! //! JSON, one file per concern, under `golden/`. JSON rather than TOML because //! `serde_json` is already in the dependency tree via `rusqlite` and `uniffi` //! (both listed in CLAUDE.md section 3), so TOML would add a crate for no gain. //! //! ```json //! { //! "concern": "overnight", //! "cases": [ //! { //! "name": "mon 22:00-06:00 window still active at tue 03:00", //! "covers": "7:overnight time ranges", //! "schedules": [ //! { "id": "s1", "name": "Night", "mode": "Blocklist", //! "targets": [{"type": "app", "app": "com.example.social"}], //! "conditions": [{"type": "Time", "days": [0], //! "start": "22:00", "end": "06:00"}] } //! ], //! "input": { "civil_time": "2026-10-06T03:00:00+02:00" }, //! "expect": { //! "blocked_apps": [{"app": "com.example.social", "schedule": "s1"}], //! "next_wakeup": "2026-10-06T06:00:00+02:00" //! } //! } //! ] //! } //! ``` //! //! # Status in M0 //! //! The shapes below are the **contract**, written down before any parse layer //! exists so M1 implements against a fixed format rather than retrofitting one. //! `serde::Deserialize` is deliberately not derived yet — adding serde is a //! dependency decision (CLAUDE.md section 0 rule 3) and is deferred to M1, //! where it arrives with `rusqlite` under one ADR entry. //! //! # The clock-tampering exception //! //! Most cases are pure data. Clock tampering is not: proving that moving the //! system date never shortens a lock requires actually moving a clock, which a //! data file cannot express. Those cases live in [`crate::purity`] as code //! fixtures instead, and the two coexist deliberately — the tables describe //! decisions, the fixtures describe the passage of time. use std::collections::BTreeMap; /// One table, deserialised from one `golden/*.json` file. #[derive(Debug, Clone)] pub struct Table { /// Which concern this table covers, e.g. `overnight`. pub concern: String, /// The cases in the table. pub cases: Vec, } /// One case: setup, input, expected decision. #[derive(Debug, Clone)] pub struct Case { /// Human-readable, and the primary test failure message. pub name: String, /// Which CLAUDE.md section 7 scenario this covers, so coverage is auditable /// rather than assumed. Checked against [`MANDATORY_COVERAGE`]. pub covers: String, /// The schedules under test. pub schedules: Vec, /// The input to the engine. pub input: InputSpec, /// The decision that must result. pub expect: ExpectSpec, } /// A schedule as it appears in a table. #[derive(Debug, Clone)] pub struct ScheduleSpec { /// Stable id, referenced by `BlockedSpec::schedule`. pub id: String, /// Display name, shown on the block screen. pub name: String, /// `"Blocklist"` or `"Allowlist"`. pub mode: String, /// What this schedule targets. pub targets: Vec, /// Its conditions. At least one must hold for the schedule to be active. pub conditions: Vec, /// Defaults to false when absent from JSON. pub paused: bool, } /// A target, tagged by type so the JSON is self-describing. #[derive(Debug, Clone)] pub enum TargetSpec { /// An app, by platform-native identifier. App { /// Package name on Android, desktop-file id or exe path elsewhere. app: String, }, /// A registrable domain. Subdomains are implied. Website { /// e.g. `example.com`. website: String, }, /// A case-insensitive substring matched against the URL. Keyword { /// The substring to match. keyword: String, }, } /// A condition, tagged by type. #[derive(Debug, Clone)] pub enum ConditionSpec { /// Block while local time is inside the window. Time { /// Weekdays on which the window **starts**. Monday = 0, matching /// `time::Weekday`. days: Vec, /// `HH:MM`. When `start > end` the window spans midnight — there is /// deliberately no `crosses_midnight` flag, because the comparison /// already encodes it and a second field is a second way to be wrong. start: String, /// `HH:MM`. See `start`. end: String, }, /// Block after a foreground-time threshold. UsageLimit { /// Threshold in minutes. limit_minutes: u32, /// `"Daily"` or `"Hourly"`. window: String, }, /// Block after a launch-count threshold. LaunchCount { /// Threshold count. limit: u32, /// `"Daily"` or `"Hourly"`. window: String, }, /// Block while connected to a named network. WifiSsid { /// The SSID to match exactly. ssid: String, }, /// Block inside or outside a radius. Mobile only (section 6.4). Location { /// Circle centre latitude in degrees. latitude: f64, /// Circle centre longitude in degrees. longitude: f64, /// Radius in metres. radius_meters: u32, /// Invert the test: block when *outside* the radius. Defaults false. block_when_outside: bool, }, } /// The engine input for a case. #[derive(Debug, Clone, Default)] pub struct InputSpec { /// RFC 3339 with an explicit offset, e.g. `2026-10-06T03:00:00+02:00`. /// The offset is mandatory so a DST case is unambiguous. pub civil_time: String, /// Monotonic milliseconds since boot. Absent means 0. pub monotonic_ms: u64, /// Boot id. Absent means 0. pub boot_id: u64, /// Accumulated usage. pub usage: UsageSpec, /// Platform-observed context. pub context: ContextSpec, } /// Usage counters for a case. All fields default to empty. #[derive(Debug, Clone, Default)] pub struct UsageSpec { /// Foreground minutes per target, daily bucket. pub daily_minutes: BTreeMap, /// Foreground minutes per target, hourly bucket. pub hourly_minutes: BTreeMap, /// Launch counts per target, daily bucket. pub daily_launches: BTreeMap, /// Launch counts per target, hourly bucket. pub hourly_launches: BTreeMap, } /// Platform context for a case. #[derive(Debug, Clone, Default)] pub struct ContextSpec { /// Current Wi-Fi SSID, if any. pub current_ssid: Option, /// Current `(latitude, longitude)`, if any. pub current_location: Option<(f64, f64)>, } /// The decision a case expects. #[derive(Debug, Clone)] pub struct ExpectSpec { /// Apps that must be blocked, each attributed to a schedule. pub blocked_apps: Vec, /// Websites and keywords that must be blocked. pub blocked_domains: Vec, /// RFC 3339, or the literal `none` when the platform may sleep forever. pub next_wakeup: String, } /// One expected blocked item. #[derive(Debug, Clone)] pub struct BlockedSpec { /// The target that must be blocked. pub app: String, /// The id of the schedule that must block it. pub schedule: String, } /// Every scenario in [`MANDATORY_COVERAGE`] must be claimed by at least one /// `covers` field across the tables, and `coverage.rs` asserts it — so a /// scenario cannot silently lose its test. pub const MANDATORY_COVERAGE: &[&str] = &[ "7:overnight time ranges", "7:DST changes", "7:timezone changes", "7:clock tampering", "7:strictest-wins", "7:allowlist mode", "7:invalid condition combos", "7:strict mode lock rules", // The section 1.2 combination table, itemised so a missing pair is visible. "1.2:time+usage rejected", "1.2:time+launch rejected", "1.2:usage+launch rejected", "1.2:usage+location rejected", "1.2:launch+location rejected", // Section 1.5.4 safety guarantees. "1.5.4:never-block list is unblockable", "1.5.4:worst case 24h", ];