18 KiB
18 KiB
Acceptance criteria: the Fenris redesign
Status: Accepted — resolves Define cross-cutting acceptance criteria on the Wayfinder map. These criteria are the accepted definition of done for the finished redesign; the implementation-ready specification assembles them with ADRs 0001–0005 at handoff.
Framework
- Canonical term: acceptance criterion — one testable behavioral statement. "Behavioral gate" is avoided as a synonym. Wording follows the repository glossary (
CONTEXT.md). - Evidence classes — every criterion carries exactly one:
- A — automated test (unit/integration, fixture-driven).
- P — scripted system probe on a host with systemd, polkit, and the configured NVMe device.
- M — manual checklist, reserved for interactions a fixture cannot capture (live polkit agent prompts, TUI keyboard feel).
- Whatever can be automated must be; M only where automation cannot reach.
- Traceability-only: every number and behavior cites the ADR or ticket that fixed it. Nothing undecided enters here; new demands become new tickets, never criteria.
- Organization: criteria are grouped by subsystem, with cross-cutting invariants spanning them. Coverage spans all decided areas — observation store and migration, collector lifecycle and privileges, projection and confidence, controller identity, the Panes TUI, failure paths, and installation lifecycle.
- Placeholders: SLOT-A and SLOT-B are reserved for the open decisions Decide how degraded identity affects projection confidence and Choose the collector's NVMe acquisition path; their resolutions fill them.
- Test-plan boundary: Given/When/Then test specs are derived by the implementer at implementation time. This effort produces criteria only.
Cross-cutting invariants
- CI-1 (A; ADR 0002 §§6–8, ADR 0003 §10) Exhaustive state matrix: from a synthetic observation store, the TUI and
fenris statusrender every realizable combination of confidence state (Unavailable, Limited, Supported) × freshness grade (fresh, missed, stale, empty store) × endurance-baseline tier (verified override, unverified override, implied, none) exactly as the ADR 0002 rule table and ADR 0003 freshness constants dictate — headline number present only when the rules allow it, contributing facts always, never a percentage. - CI-2 (P/A; ADR 0003 §§4–9) TUI/CLI parity: every TUI action has a CLI twin — pause (
fenris monitor pause), resume (fenris monitor resume), collect-now (fenris sample), baseline set/clear, and the status fact set — with identical outcomes and wording. - CI-3 Prohibition set (A = test, P = probe; each cites its clause):
- No code path outside
fenris-collectinterrogates the device (ADR 0003 §7). - Polkit authorizes exactly one binary,
fenris-monitor, undercom.bongbetic.fenris.monitorauth_admin(ADR 0003 §5, ADR 0004 §3). - No absent hour is ever interpolated, estimated, or fabricated (ADR 0005 §2).
- No alerting, notification, or escalation machinery exists anywhere (ADR 0005 §§5–6).
/etc/fenris/fenris.confholds exactly one key — the device selector (ADR 0003 §3).- No synthetic or capacity-derived baseline is ever created, including for legacy history (ADR 0002, Consequences).
- Readers never partially interpret a newer-schema store (ADR 0001 §8, ADR 0005 §4).
- Projections are never stored; always recomputed on read (ADR 0001 §3, ADR 0002 §13).
- No code path outside
- CI-4 (A; ADR 0002 §§11–12) User-facing language: TUI and status render the endurance research's required wording and six disclosures as adopted; zero-rate and unavailable cases use their exact phrasing; the scenario range is the only spread shown anywhere.
Observation store and legacy migration (ADR 0001)
- ST-1 (P) One SQLite database in WAL mode at
/var/lib/fenris/observations.db, root-owned and group-readable through thefenrisread group; the TUI opens it read-only. - ST-2 (A) An unprivileged reader querying during a collector write sees a consistent snapshot.
- ST-3 (A) The schema carries
samples,hour_observations,day_aggregates,monitoring_periods,controller_segments, andendurance_baselinewith the ADR 0001 column sets as amended by Define endurance-baseline provenance and validation and Decide controller-segment metadata columns. - ST-4 (A) Hours and days are UTC-bounded; day derivation from hours is monotonic; no 23- or 25-hour days exist.
- ST-5 (A) Raw samples are pruned opportunistically to 14 days; hour observations and day aggregates are retained indefinitely.
- ST-6 (P) Legacy import is one transaction: a scripted kill mid-import leaves the store fully pre- or fully post-migration.
- ST-7 (A) Import is idempotent: a second run no-ops on the legacy-import marker.
- ST-8 (A) Legacy files are renamed
*.migratedonly after commit and never deleted. - ST-9 (A) Malformed legacy lines are quarantined with a logged count, never silently dropped.
- ST-10 (A)
hourly.jsonlis never trusted: mismatches against derived data are diffed and logged. - ST-11 (A) Migration opens one implicit monitoring period at the first legacy sample, closed
end_cause = migratedat the migration moment; pre-migration hours carry an unknown activity split except directly evidenced facts. - ST-12 (A) Schema versioning:
PRAGMA user_versionwith ordered, per-step transactional migrations; the collector refuses an unknown newer version.
Collector lifecycle and privilege boundaries (ADR 0003)
- LC-1 (P) Exactly two system units exist —
fenris-collect.timer(timers.target) andfenris-collect.service(Type=oneshot, root,ExecStart=/usr/libexec/fenris/fenris-collect); the TUI and CLI are ordinary unprivileged processes and never units. - LC-2 (P) Timer defaults ship as
OnBootSec=2min,OnUnitInactiveSec=5min,AccuracySec=30s,Persistent=no,TimeoutStartSec=90s; cadence changes are documented drop-ins and no interval key exists in configuration. - LC-3 (P) A hung device interrogation fails visibly within
TimeoutStartSec=90sas a bounded failed run retried next interval. - LC-4 (A/P)
/etc/fenris/fenris.confholds exactly the device selector (stable/dev/disk/by-id/…path; raw nodes warned), re-read every run; an invalid selector is a bounded failed run surfaced asconfiguration error: <reason>instatusand the TUI. - LC-5 (P) Two privileged binaries ship at
/usr/libexec/fenris/fenris-collectand/usr/libexec/fenris/fenris-monitor; the unprivilegedfenriswrapper opens the TUI with no arguments. - LC-6 (P+M) Pause =
fenris-monitor disable --nowasks for confirmation; Resume =enable --nowdoes not; both perform the systemctl operation and period bookkeeping in one step under polkitcom.bongbetic.fenris.monitor(auth_admin), failing cleanly with the printed root equivalent where no polkit agent exists. (M covers the live agent prompt.) - LC-7 (A) The period-row idempotent matrix of ADR 0003 §6 holds exactly: first-ever enable opens; resume with an open period changes nothing; resume without one opens anew; pause with an open period closes
user_disabled; pause otherwise no-ops; a raw systemctl stop/disable never recordsuser_disabled. - LC-8 (P)
fenris sampleand the TUI's collect-now route throughfenris-monitor→systemctl start fenris-collect.service, block until exit, and report the outcome (freshness line or journal hint) synchronously; the TUI never samples in-process. - LC-9 (A/P) CLI compatibility:
statusis a read-only composition (projection facts, enabled/active, last collect outcome,journalctlhint on failure or staleness) that never auto-samples and never prompts;sampleis retained via the helper;--deviceis rejected with a pointer to the configuration file;start,stop, andrunare rejected with one-line migration pointers;fenris.shis not shipped and is removed from the repository; the README maps its five menu options to successors. - LC-10 (A) Freshness constants are defined once and shared by TUI and CLI: fresh = newest sample within 2× cadence +
AccuracySec+ 60 s; missed between that and 48 h; stale ≥ 48 h; an empty store reads "no observations yet" with an enable hint; freshness derives from the newest sample timestamp, never a stored health flag.
Projection contract and confidence (ADR 0002)
- PR-1 (A) Exactly one projection, from the precedence-chosen baseline (verified override → unverified override → implied → unavailable); Percentage Used renders as a vendor-wear context line, with a note when it disagrees with the observed write rate by more than a factor of 2; the PU-slope regression and
capacity × 600synthesis are gone. - PR-2 (A) The headline rate is the sustained-regime rate (regime DUW bytes ÷ in-period wall-clock seconds), default regime = full history capped at 90 days; the 7/28/90-day scenario range is computed independently and shows only covered horizons, with no placeholders.
- PR-3 (A) Habit change: trailing 7-day mean ≥ 2× or ≤ 0.5× the preceding 28-day mean for 3 consecutive days starts a new regime at the first divergence day, adopted automatically and labeled "usage habit changed N days ago"; a regime younger than 7 days caps confidence at Limited evidence.
- PR-4 (A) Hour classification uses the named constants: powered-off below 90% of power-on-hours span; active at ≥ 256 MiB DUW; idle below it while powered on and sampled; unknown otherwise; disabled time is wall-clock outside monitoring periods, never an hour state.
- PR-5 (A) The denominator is wall-clock seconds inside monitoring periods including powered-off and unknown time; disabled periods are excluded from numerator and denominator; unexplained gaps keep the aggregate counter delta, remain as unknown seconds, and reduce coverage.
- PR-6 (A) Warming up until 14 distinct UTC day aggregates of which at most 2 fall below 50% coverage; the projection still renders with its facts while warming; every Unavailable condition renders no lifespan number.
- PR-7 (A) A newest day aggregate older than 48 h drops confidence one level and is shown as a contributing fact.
- PR-8 (A) The confidence rule table of ADR 0002 §8 holds verbatim, rendering state plus contributing facts and never a percentage.
- PR-9 (A) Segment breaks: a DUW decrease with unchanged identity keeps prior day aggregates as habit evidence with the projection Unavailable until re-warm; a controller-identity change quarantines prior history from projection entirely.
- PR-10 (A) The implied baseline is eligible only after ≥ 2 Percentage-Used increments within the current controller segment; until then, Unavailable with "vendor wear estimate too coarse to imply endurance".
- PR-11 (A) Zero rate renders "no finite projection from this history" — never infinity or zero; no statistical confidence interval appears anywhere.
- PR-12 (A) The projection contract hands the TUI exactly: confidence state, contributing facts, headline remaining time when one exists, scenario range, Percentage-Used context line, disclosure text — recomputed on read, never stored.
- PR-13 (A) Baseline provenance and validation per Define endurance-baseline provenance and validation: mandatory provenance (URL, revision, entry date, model, nominal capacity); one active row replaced on edit; verification derived at read (machine match or recorded attestation), never a stored boolean; incomplete provenance stores only behind explicit acknowledgment as the unverified tier; entry-time unprivileged sysfs validation (normalized model containment; capacity within ±1%; interactive confirm recorded as
validated_by = user); read-time applicability is a model match against the current controller segment, with a mismatch retained — never auto-deleted — leaving the projection Unavailable. - PR-14 (P)
baseline set/baseline clearpersist through the polkit-guardedfenris-monitorverb after CLI-side validation. - SLOT-A — pending Decide how degraded identity affects projection confidence: how a blank or degraded identity key moves confidence.
Controller identity (Verify the controller identity that segments observation history, Decide controller-segment metadata columns; ADR 0001 §3 as amended)
- ID-1 (A) The controller-segment identity key is the normalized kernel-exposed subsystem NQN, with the kernel composite then model|serial as fallbacks; FR is metadata only; identity change and DUW decrease act as independent axes.
- ID-2 (A) Segments freeze a fully nullable metadata snapshot at open — normalized
subnqn/sn/mn/frplusvid/ssvid/transportandidentity_degraded— immutable thereafter, withcntlidexcluded. - ID-3 (A) Legacy history imports under a labeled model-scoped legacy identity (mn-only segments).
Panes TUI (Prototype the TUI information architecture, Evaluate Python TUI frameworks; ADR 0003 §§8, 10; ADR 0004 §10)
- TUI-1 (A) Variant A "Panes": one dense keyboard-first screen; confidence rendered as evidence (state + contributing facts); boot enablement, runtime activity, last collect outcome, and freshness displayed as four separate facts.
- TUI-2 (M) Pause/resume asymmetry and polkit tty passthrough work in a live terminal: pause confirms, resume does not, and the platform agent prompts without breaking the TUI.
- TUI-3 (P) Textual runs on Python 3.9+, gated at install time, never a runtime crash.
Failure and recovery (ADR 0005)
- FL-1 (A) The collector validates every row against the store invariants (hour seconds sum to 3600; non-negative DUW delta within a controller segment; coverage consistent with sample count); a violating run writes nothing, logs the refused row, and fails visibly.
- FL-2 (A) Readers defensively exclude and count malformed rows as a contributing fact.
- FL-3 (A) No backfill ever: gaps remain unknown seconds; degradation flows only through coverage, freshness facts, and confidence categories.
- FL-4 (P/A) A store fault surfaces "observation store unreadable" with a journal hint and suppresses everything else store-dependent; the collector treats it as a bounded failed run and never recreates or overwrites the file; recovery is the documented human-sanctioned move-aside (with
history.jsonlre-import if the legacy import never completed); no built-in destructive command exists. - FL-5 (A) A newer-schema store renders "observation store written by a newer Fenris — upgrade Fenris" in TUI and status, with no partial interpretation.
- FL-6 (A/P) Repeated collector failures retry at flat cadence with no backoff or notification; persistence reads as stale exactly like any other gap.
- FL-7 (A)
critical_warning, media errors, and unsafe shutdowns render as ordinary facts in TUI and status and never affect the projection. - FL-8 (A) A collection run finding no open monitoring period opens one at the run moment, never backdated.
Installation, upgrade, and removal (ADR 0004)
- IN-1 (P)
sudo make installbuilds a wheel from the checkout and installs pinned dependencies into the dedicated venv at/opt/fenris, with a/usr/local/bin/fenriswrapper; after install nothing references the checkout. - IN-2 (P) The installer records every placed file in an explicit manifest consumed by upgrade and uninstall.
- IN-3 (P) The installer never enables or starts units: a fresh install is dormant (units disabled, nothing running, no monitoring period); the only opt-in is the sanctioned toggle —
fenris monitor resume [--now]or the first-run TUI prompt — enabling the timer and opening the first period in one step. - IN-4 (P) Install-time legacy import detects
./data/history.jsonl(or an explicit path), runs the idempotent single-transaction import, and reports imported counts;fenris import <path>remains available. - IN-5 (P)
sudo make upgradeinstalls into the same venv, syncs units and polkit against the manifest (daemon-reload; timer restarted only if unit contents changed and it is active), never kills an in-flight collection run, then applies forward-only schema migrations;/var/lib/fenrisis never rebuilt. - IN-6 (P) Before migrations,
observations.dbis snapshotted to a one-generation.bak; rollback is reinstall-previous plus restore; automatic schema downgrade does not exist. - IN-7 (P)
make uninstallperforms the sanctioned disable first (open period closesuser_disabled), then removes venv, helpers, units, polkit policy, and wrapper while keeping/etc/fenrisand the observation store;make purgeadditionally removes configuration and store. - IN-8 (P) Dependencies are exact pins in a committed lockfile installed by both install and upgrade; refreshing pins is an explicit
make update-depsstep, never an install side effect. - IN-9 (P) The installer verifies
python3 ≥ 3.9and fails cleanly otherwise;/var/lib/fenrisis created with root-written group-read permissions; the database file is created lazily by the first write.
Collector acquisition path
- SLOT-B — pending Choose the collector's NVMe acquisition path: dependency-weight, lockfile, privilege, and normalization-consistency criteria for the chosen path.