diff --git a/docs/adr/0006-collector-acquisition-path.md b/docs/adr/0006-collector-acquisition-path.md new file mode 100644 index 0000000..3c781dc --- /dev/null +++ b/docs/adr/0006-collector-acquisition-path.md @@ -0,0 +1,28 @@ +# 6. Collector acquisition path: smartctl counters, sysfs identity + +## Status + +Accepted — resolves [Choose the collector's NVMe acquisition path](https://git.bongbetic.com/xavierk/Fenris/issues/16) on the [Wayfinder map](https://git.bongbetic.com/xavierk/Fenris/issues/1). + +## Context + +The collector ([ADR 0003](0003-service-lifecycle-and-sanctioned-toggle.md)) must acquire SMART/Health counters, thermal evidence, and controller identity each run. The [controller-identity research](https://git.bongbetic.com/xavierk/Fenris/src/branch/research/controller-identity/docs/research/controller-identity.md) fixed the identity key to the normalized, kernel-exposed subsystem NQN and warned that normalization must be specified once and applied at write time — or a collector implementation change can split a drive's own history. [ADR 0004](0004-install-upgrade-removal-lifecycle.md) pins exact Python dependencies in a dedicated venv, and the [segment-metadata decision](https://git.bongbetic.com/xavierk/Fenris/issues/14) froze nullable `vid`/`ssvid`/`transport` alongside the identity fields. Three first-party paths were candidates: the official libnvme Python bindings (SWIG; sysfs-backed attribute getters delivering normalized values), `nvme` CLI JSON output, and the incumbent `smartctl -j` plus sysfs reads. + +## Decision + +1. **Pin.** Every collection run acquires counters and thermal evidence solely from `smartctl -a -j ` and controller identity (`subnqn`, `sn`, `mn`, `fr`, `transport`) solely from sysfs (`/sys/class/nvme//`). No other acquisition path exists anywhere in the codebase. +2. **Hard pin, no fallback.** Any acquisition failure — missing binary, nonzero exit, malformed JSON, unreadable sysfs attribute — fails the whole collection run; [ADR 0005](0005-failure-detection-and-recovery.md)'s flat retry and freshness grading absorb the miss. A partial sample (identity without counters, or counters without identity) is never written: a transient read failure must not push a healthy drive down the degraded-identity path. +3. **Normalization once, at write time.** One collector-side function normalizes every identity field: trailing spaces and newlines stripped, no case folding, empty-after-strip stored blank. `smartctl` counter and thermal fields are consumed as-is (smartmontools already trims the strings it copies). Padded and unpadded renderings of the same field therefore yield byte-identical stored values. +4. **Segment metadata sourcing.** `transport` comes from the NVMe class sysfs directory; `vid`/`ssvid` from the PCI node (`/sys/class/nvme//device/{vendor,subsystem_vendor}`) when present, null otherwise — metadata only, never key components. +5. **Prerequisites.** `make install` verifies `smartctl` is present and fails cleanly otherwise. The acquisition path adds no Python dependency and no OS package beyond smartmontools; the [ADR 0004](0004-install-upgrade-removal-lifecycle.md) lockfile is untouched. + +## Considered options + +- **libnvme Python bindings** — the purest API and natively-normalized getters, but the SWIG module is not on PyPI: entering the venv requires the distro's `python3-libnvme` through `--system-site-packages` or a from-source build, coupling the exact-lockfile venv to the system Python and the distro's shipping choices. Rejected on dependency weight for one privileged five-minute oneshot. +- **`nvme` CLI JSON** — one binary covers counters and identity, but it adds an OS package for what smartmontools already provides, emits untrimmed strings, and reports `subnqn` from Identify data rather than the kernel: when a controller reports an empty NQN the kernel synthesizes one for sysfs while `id-ctrl` JSON omits the field, so the identity ladder would drop a rung depending on the drive. Rejected on packaging and identity-key consistency. + +## Consequences + +- The venv stays pure-Python; the two acquisition channels per run (subprocess JSON plus sysfs reads) hide behind one acquisition function, gated by acceptance criteria AC-1–AC-5. +- Identity is read from exactly the source the identity key names; libnvme's getters wrap the same sysfs attributes, so the values agree byte-for-byte where both exist. +- Switching acquisition path later is history-sensitive: a future path must deliver byte-identical normalized identity values, or the change itself forces a controller-segment boundary. diff --git a/docs/spec/acceptance-criteria.md b/docs/spec/acceptance-criteria.md index ec0fefc..9902422 100644 --- a/docs/spec/acceptance-criteria.md +++ b/docs/spec/acceptance-criteria.md @@ -1,6 +1,6 @@ # Acceptance criteria: the Fenris redesign -Status: Accepted — resolves [Define cross-cutting acceptance criteria](https://git.bongbetic.com/xavierk/Fenris/issues/13) on the [Wayfinder map](https://git.bongbetic.com/xavierk/Fenris/issues/1). These criteria are the accepted definition of done for the finished redesign; the implementation-ready specification assembles them with ADRs 0001–0005 at handoff. +Status: Accepted — resolves [Define cross-cutting acceptance criteria](https://git.bongbetic.com/xavierk/Fenris/issues/13) on the [Wayfinder map](https://git.bongbetic.com/xavierk/Fenris/issues/1). These criteria are the accepted definition of done for the finished redesign; the implementation-ready specification assembles them with ADRs 0001–0006 at handoff. ## Framework @@ -12,7 +12,7 @@ Status: Accepted — resolves [Define cross-cutting acceptance criteria](https:/ - 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-B is reserved for the open decision [Choose the collector's NVMe acquisition path](https://git.bongbetic.com/xavierk/Fenris/issues/16); its resolution fills it. SLOT-A was filled by [Decide how degraded identity affects projection confidence](https://git.bongbetic.com/xavierk/Fenris/issues/15) as PR-15, PR-16, and ID-4. +- **Placeholders**: none remain. SLOT-B was filled by [Choose the collector's NVMe acquisition path](https://git.bongbetic.com/xavierk/Fenris/issues/16) as AC-1–AC-5; SLOT-A was filled by [Decide how degraded identity affects projection confidence](https://git.bongbetic.com/xavierk/Fenris/issues/15) as PR-15, PR-16, and ID-4. - **Test-plan boundary**: Given/When/Then test specs are derived by the implementer at implementation time. This effort produces criteria only. ## Cross-cutting invariants @@ -113,6 +113,10 @@ Status: Accepted — resolves [Define cross-cutting acceptance criteria](https:/ - **IN-8** (P) Dependencies are exact pins in a committed lockfile installed by both install and upgrade; refreshing pins is an explicit `make update-deps` step, never an install side effect. - **IN-9** (P) The installer verifies `python3 ≥ 3.9` and fails cleanly otherwise; `/var/lib/fenris` is created with root-written group-read permissions; the database file is created lazily by the first write. -## Collector acquisition path +## Collector acquisition path (ADR 0006) -- **SLOT-B** — pending [Choose the collector's NVMe acquisition path](https://git.bongbetic.com/xavierk/Fenris/issues/16): dependency-weight, lockfile, privilege, and normalization-consistency criteria for the chosen path. +- **AC-1** (P) Each collection run acquires counters and thermal evidence solely from `smartctl -a -j ` and controller identity (`subnqn`, `sn`, `mn`, `fr`, `transport`) solely from sysfs; no other acquisition path exists anywhere in the codebase. +- **AC-2** (A) Identity normalization is applied exactly once, at write time — trailing spaces and newlines stripped, no case folding, empty-after-strip stored blank — so padded and unpadded renderings of the same field yield byte-identical stored values. +- **AC-3** (A) Any acquisition failure — missing binary, nonzero exit, malformed JSON, unreadable sysfs attribute — fails the whole collection run; no partial sample (identity without counters, or counters without identity) is ever written; the miss surfaces through ADR 0005 freshness, never as degraded identity. +- **AC-4** (P) `vid`/`ssvid` are read from the PCI sysfs node when present and stored null otherwise; they are segment metadata only, never key components. +- **AC-5** (P) `make install` verifies `smartctl` and fails cleanly otherwise; the acquisition path adds no Python dependency and no OS package beyond smartmontools (ADR 0004 §9).