NODEDC_MISSION_CORE/docs/adr/0009-device-plugin-observat...

128 lines
6.0 KiB
Markdown

# ADR 0009: device-plugin observation runtime and K1 extraction
- Status: accepted
- Date: 2026-07-17
- Extends: ADR 0003, ADR 0004 and ADR 0008
## Context
The manifest/action boundary isolated the browser shell, but the host observation
service still imported K1 archive discovery, `.k1mqtt` preparation, MQTT timing
metadata and the K1 normalizer. A second sensor could not provide durable replay
without adding vendor branches to `SessionStore`, `SessionRecordingMaterializer`
and `app.py`.
## Decision
Every installed device plugin may contribute an optional observation runtime:
- one or more `ObservationArchiveSource` values with a plugin ID, archive ID,
confined root, discovery callback and recovery callback;
- exactly one recording exporter for that plugin ID;
- generic `ObservationSessionCandidate`, `SessionSource` and
`ObservationArtifactCandidate` values returned to the host.
The host owns SQLite lifecycle, path confinement, preparation scheduling,
cross-process cache locking, atomic publication, RRD delivery, recorded-media
admission and browser APIs. A plugin owns native evidence discovery, transport
format validation, recovery semantics, timeline-origin extraction and conversion
of its primary artifact into the canonical recorded Rerun artifact.
For new K1 sessions, the plugin also contributes confined clock artifacts. A
durable origin is published before camera production, MQTT shutdown creates a
transport-scoped provisional envelope, and the acquisition owner publishes a
content-addressed session envelope plus an atomic summary-pointer switch after
camera and MQTT/runtime shutdown. A provisional envelope cannot advertise
combined camera replay. Valid older sessions without this contract keep their
first/last-message compatibility fallback.
`ReplayCommand` is now vendor-neutral. It contains an owning `plugin_id`, an
opaque primary artifact ID, an ordered tuple of confined artifacts and a ready
timeline origin. It contains no filename suffix, MQTT topic or sidecar name.
The materializer selects the exporter by `plugin_id` and stages every validated
artifact prefix while preserving only its plugin-owned basename.
Derived RRD cache v9 binds the published RRD to:
- plugin ID and primary artifact ID;
- ordered artifact IDs and media types;
- full file identity and replay boundary for every artifact;
- per-artifact prefix digest;
- durable clock origin, active-envelope start/completion, selected clock
artifact identity and both digests when present;
- derived recording digest and zero-based timeline bounds whose origin and end
are both materialized as real RRD rows.
Cache v8 and older sidecars are rebuilt once. The native evidence is not
rewritten.
The K1 exporter also recognizes bounded `ModelingReport` messages. It writes the
reported route distance, speed and scan time as scalar time series below
`/metrics/device`; it does not infer them from pose integration or a UI timer.
Live observation consumes the same status through a plugin-injected raw-message
observer before the four-message visual preview queue. The generic
`VisualizationRuntime` still has no K1 decoder or topic knowledge.
The concrete implementation is physically located below:
```text
src/k1link/device_plugins/xgrids_k1/
ble/ mqtt/ protocol/ analyze/ net/ usb/
viewer/
facade.py camera.py archive.py observation.py rrd_export.py cli.py
```
The manifest entrypoint is
`k1link.device_plugins.xgrids_k1.facade:build_xgrids_k1_plugin`. Generic
`sessions`, `web/app.py`, `web/session_api.py` and the Rerun consumer do not
import this package or contain K1 formats, topics or identifiers.
Plugin SDK v0alpha2 additionally defines immutable
`RuntimeActionInvocation` and `RuntimeActionResult`. The host dispatcher creates
and validates them for every plugin action, including legacy compatibility
routes, before returning the path-free output document.
## Acceptance gate
The automated gate builds an unrelated synthetic spatial-sensor plugin which:
1. discovers its own native evidence extension;
2. reconciles into the unchanged host SQLite catalog;
3. produces a generic `ReplayCommand`;
4. materializes and reuses an RRD through its plugin-keyed exporter.
A dependency test also fails if generic observation hot-path files regain K1,
XGRIDS, topic, native-format or concrete-plugin imports. Adding a second device
therefore requires a manifest/runtime contribution and a plugin-owned reviewed
UI contribution, not edits to the session API, preparation queue, Rerun consumer
or main application shell. ADR 0010 defines that frontend boundary.
Additional acceptance tests bind the K1 capture clock into the ordered replay
artifacts, reject corrupt or provisional new-schema boundaries, materialize real
origin/end rows and retain archived device-metric samples without traversing the
lossy live-preview queue.
## Consequences
The semantic and physical vendor boundary is complete for the current
single-process architecture. Existing K1 raw evidence, camera archives and
operator workflows remain compatible. The `k1link` distribution name and CLI
command remain transitional compatibility names; their implementation is now
plugin-owned.
The operator project name is normalized and validated by the K1 contribution,
stored as display/catalog metadata and never used as a path component. This ADR
does not claim that it reaches the scanner: the inert modeling-control codec has
no publisher, and automatic K1 writes remain disabled pending legitimate OpenAPI
credential provisioning and durable post-stop save evidence.
ADR 0011 subsequently places the action control plane behind a versioned
descriptor/handshake/health transport seam. Observation discovery and export
remain host-called plugin contributions and are not yet portable process
transport contracts.
This ADR does not claim process isolation, crash containment, a durable operation
journal, signed plugin bundles, multi-device concurrency, portable SDK stream
envelopes or SDK `EvidenceStore` persistence. Those require a supervisor and a
new physical regression gate; they must not be inferred from module extraction.