mirror of
https://github.com/ruvnet/RuView
synced 2026-08-09 20:21:43 +00:00
feat(engine): per-room adapter provenance + drift-to-recalibration advisor
Closes the trust-chain gap where an ~11 KB per-room LoRA adapter (ADR-150
section 3.4) could silently change inference without the witness noticing:
provenance carried only "rfenc-v<N>" with no notion of adapter identity.
- StreamingEngine::set_room_adapter(AdapterInfo): pins the adapter's
content-derived id into provenance model_version
("rfenc-v1+adapter:<id>") — and therefore into the BLAKE3 witness — so
swapping or clearing adapter weights always shifts the witness. Engine test
proves base -> adapter -> other-adapter -> cleared all witness differently
and cleared == base.
- RecalibrationAdvisor: recommends re-running the ADR-135 empty-room baseline
/ refitting the room adapter on sustained low fusion coherence (streak
threshold, default 60 cycles ~ 3 s at 20 Hz) or an ADR-142 change-point.
Surfaced as TrustedOutput::recalibration_recommended, stored on the
sensing-server AppState alongside the witness at both live fusion sites.
- Bridge plumbing: EngineBridge::{set_room_adapter, clear_room_adapter} +
live-path test that the adapter id flows into the live witness.
Scope note (honest): this is the deployable provenance/trigger half of the
"retrained model" roadmap item. Fitting the adapter itself runs in the
existing external calibration service (aether-arena/calibration/); a trained
RF-encoder checkpoint still does not exist in-tree.
Engine 15 tests, bridge 7 tests. Workspace gate: 2,918 passed / 0 failed.
https://claude.ai/code/session_01MjBucx95K4BuUxZi8NWwRH
This commit is contained in:
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
- **Per-room adapter provenance + drift→recalibration advisor in the streaming engine.** Closes the trust-chain gap where an ~11 KB per-room LoRA adapter (ADR-150 §3.4) could silently change inference without the witness noticing. `StreamingEngine::set_room_adapter(AdapterInfo)` pins the adapter's content-derived id into provenance `model_version` (`rfenc-v1+adapter:<id>`) — and therefore into the BLAKE3 witness — so swapping or clearing adapter weights always shifts the witness (engine test proves base → adapter → other-adapter → cleared all witness differently, and cleared == base). New `RecalibrationAdvisor` recommends re-running the ADR-135 baseline / refitting the adapter on sustained low fusion coherence (streak threshold, default 60 cycles ≈ 3 s at 20 Hz) or an ADR-142 change-point; surfaced as `TrustedOutput::recalibration_recommended` and stored on the sensing-server `AppState` alongside the witness. Bridge plumbing: `EngineBridge::{set_room_adapter, clear_room_adapter}` + live-path test that the adapter id flows into the live witness. Engine 15 tests, bridge 7 tests. *Scope note: this is the deployable provenance/trigger half of the "retrained model" roadmap item — fitting the adapter itself runs in the existing external calibration service (`aether-arena/calibration/`), and a trained RF-encoder checkpoint still does not exist in-tree.*
|
||||||
- **RuView beyond-SOTA research series** (`docs/research/ruview-beyond-sota/`, 6 docs) — research-swarm output defining the beyond-SOTA bar and the path to it: system capability audit (role→crate maturity matrix, gap analysis, risk register), web-verified 2026 SOTA landscape per capability axis (incl. ratified IEEE 802.11bf-2025), 8-pillar target architecture on the ADR-136 contract spine (no rewrite), 6-layer benchmark/validation methodology (all 15 criterion bench targets inventoried; ADR-149 statistical protocol), and a determinism-safe optimization roadmap. Includes session validation evidence: 2,797 workspace tests / 0 failed, Python proof PASS (bit-exact), paired pre/post criterion runs.
|
- **RuView beyond-SOTA research series** (`docs/research/ruview-beyond-sota/`, 6 docs) — research-swarm output defining the beyond-SOTA bar and the path to it: system capability audit (role→crate maturity matrix, gap analysis, risk register), web-verified 2026 SOTA landscape per capability axis (incl. ratified IEEE 802.11bf-2025), 8-pillar target architecture on the ADR-136 contract spine (no rewrite), 6-layer benchmark/validation methodology (all 15 criterion bench targets inventoried; ADR-149 statistical protocol), and a determinism-safe optimization roadmap. Includes session validation evidence: 2,797 workspace tests / 0 failed, Python proof PASS (bit-exact), paired pre/post criterion runs.
|
||||||
|
|
||||||
### Performance
|
### Performance
|
||||||
|
|||||||
@@ -97,6 +97,10 @@ pub struct TrustedOutput {
|
|||||||
/// BLAKE3 witness over the trust decision (provenance ‖ class ‖ calibration)
|
/// BLAKE3 witness over the trust decision (provenance ‖ class ‖ calibration)
|
||||||
/// — a deterministic, signed-belief fingerprint (ADR-137 §2.7 / ADR-028).
|
/// — a deterministic, signed-belief fingerprint (ADR-137 §2.7 / ADR-028).
|
||||||
pub witness: [u8; 32],
|
pub witness: [u8; 32],
|
||||||
|
/// Whether the drift→recalibration advisor recommends re-running the
|
||||||
|
/// ADR-135 baseline / refitting the per-room adapter (ADR-150 §3.4):
|
||||||
|
/// sustained low coherence or an ADR-142 change-point this cycle.
|
||||||
|
pub recalibration_recommended: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Composition root for the RuView streaming engine.
|
/// Composition root for the RuView streaming engine.
|
||||||
@@ -120,6 +124,68 @@ pub struct StreamingEngine {
|
|||||||
// appends one belief per cycle (1.7M/day at 20 Hz); durable history is the
|
// appends one belief per cycle (1.7M/day at 20 Hz); durable history is the
|
||||||
// recorder's job, so old beliefs are evicted deterministically past this cap.
|
// recorder's job, so old beliefs are evicted deterministically past this cap.
|
||||||
semantic_retention: usize,
|
semantic_retention: usize,
|
||||||
|
// Per-room calibration adapter (ADR-150 §3.4: ~11 KB LoRA on a frozen
|
||||||
|
// base). Identity is part of the trust chain: when set, the adapter id is
|
||||||
|
// appended to the provenance model_version, so swapping adapters changes
|
||||||
|
// the witness. None = shared base model.
|
||||||
|
adapter: Option<AdapterInfo>,
|
||||||
|
// Drift→recalibration advisor (ADR-135 trigger for ADR-150 §3.4 refit).
|
||||||
|
recal: RecalibrationAdvisor,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Identity of an active per-room calibration adapter (ADR-150 §3.4). The id
|
||||||
|
/// must be content-derived (e.g. a hash prefix of the adapter file) so the
|
||||||
|
/// provenance/witness chain pins the exact weights that shaped inference.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct AdapterInfo {
|
||||||
|
/// Content-derived adapter identity (e.g. first 16 hex of its SHA-256).
|
||||||
|
pub adapter_id: String,
|
||||||
|
/// Number of in-room samples the adapter was fitted on (0 if unknown).
|
||||||
|
pub trained_samples: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recommends re-running calibration / adapter refit when the live signal
|
||||||
|
/// degrades persistently (ADR-135 drift → ADR-150 §3.4 few-shot recalibration).
|
||||||
|
///
|
||||||
|
/// Two triggers, both cheap and deterministic:
|
||||||
|
/// - `low_coherence_streak`: N consecutive cycles whose base coherence fell
|
||||||
|
/// below the floor (sustained degradation, not a single bad frame);
|
||||||
|
/// - any ADR-142 change-point this cycle (the environment itself changed).
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct RecalibrationAdvisor {
|
||||||
|
/// Coherence below this counts toward the streak.
|
||||||
|
pub coherence_floor: f32,
|
||||||
|
/// Consecutive low-coherence cycles required to recommend recalibration.
|
||||||
|
pub streak_threshold: u32,
|
||||||
|
streak: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RecalibrationAdvisor {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
coherence_floor: 0.5,
|
||||||
|
streak_threshold: 60, // ~3 s at 20 Hz of sustained degradation
|
||||||
|
streak: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RecalibrationAdvisor {
|
||||||
|
/// Feed one cycle's evidence; returns whether recalibration is recommended.
|
||||||
|
fn observe(&mut self, base_coherence: f32, change_point: bool) -> bool {
|
||||||
|
if base_coherence < self.coherence_floor {
|
||||||
|
self.streak = self.streak.saturating_add(1);
|
||||||
|
} else {
|
||||||
|
self.streak = 0;
|
||||||
|
}
|
||||||
|
change_point || self.streak >= self.streak_threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Current consecutive low-coherence cycle count.
|
||||||
|
#[must_use]
|
||||||
|
pub fn streak(&self) -> u32 {
|
||||||
|
self.streak
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StreamingEngine {
|
impl StreamingEngine {
|
||||||
@@ -140,9 +206,35 @@ impl StreamingEngine {
|
|||||||
slam: RfSlam::with_discovery(0.5, 5, 0.6),
|
slam: RfSlam::with_discovery(0.5, 5, 0.6),
|
||||||
person_tracks: BTreeMap::new(),
|
person_tracks: BTreeMap::new(),
|
||||||
semantic_retention: Self::DEFAULT_SEMANTIC_RETENTION,
|
semantic_retention: Self::DEFAULT_SEMANTIC_RETENTION,
|
||||||
|
adapter: None,
|
||||||
|
recal: RecalibrationAdvisor::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Activate a per-room calibration adapter (ADR-150 §3.4). From the next
|
||||||
|
/// cycle on, the adapter id is part of provenance `model_version` — and
|
||||||
|
/// therefore of the witness — so the exact weights shaping inference are
|
||||||
|
/// pinned in the trust chain. Pass the result of hashing the adapter file.
|
||||||
|
pub fn set_room_adapter(&mut self, info: AdapterInfo) {
|
||||||
|
self.adapter = Some(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deactivate the adapter (revert to the shared base model).
|
||||||
|
pub fn clear_room_adapter(&mut self) {
|
||||||
|
self.adapter = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The active adapter, if any.
|
||||||
|
#[must_use]
|
||||||
|
pub fn room_adapter(&self) -> Option<&AdapterInfo> {
|
||||||
|
self.adapter.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tune the drift→recalibration advisor (floor + streak threshold).
|
||||||
|
pub fn set_recalibration_advisor(&mut self, advisor: RecalibrationAdvisor) {
|
||||||
|
self.recal = advisor;
|
||||||
|
}
|
||||||
|
|
||||||
/// Default cap on live `SemanticState` beliefs in the WorldGraph
|
/// Default cap on live `SemanticState` beliefs in the WorldGraph
|
||||||
/// (~6 minutes of full-rate history at 20 Hz; older beliefs are evicted —
|
/// (~6 minutes of full-rate history at 20 Hz; older beliefs are evicted —
|
||||||
/// durable history belongs to the recorder).
|
/// durable history belongs to the recorder).
|
||||||
@@ -344,13 +436,20 @@ impl StreamingEngine {
|
|||||||
|
|
||||||
// 6. Semantic state with mandatory provenance (ADR-139/140). The
|
// 6. Semantic state with mandatory provenance (ADR-139/140). The
|
||||||
// calibration version comes from the *agreed* epoch (None on mismatch).
|
// calibration version comes from the *agreed* epoch (None on mismatch).
|
||||||
|
// When a per-room adapter is active (ADR-150 §3.4) its content-derived
|
||||||
|
// id is part of model_version — and therefore of the witness — so the
|
||||||
|
// exact weights shaping inference are pinned in the trust chain.
|
||||||
let calibration_version = match quality.calibration_id {
|
let calibration_version = match quality.calibration_id {
|
||||||
Some(c) => format!("cal:{:016x}", c.0),
|
Some(c) => format!("cal:{:016x}", c.0),
|
||||||
None => "cal:none".to_string(),
|
None => "cal:none".to_string(),
|
||||||
};
|
};
|
||||||
|
let model_version = match &self.adapter {
|
||||||
|
Some(a) => format!("rfenc-v{}+adapter:{}", self.model_version, a.adapter_id),
|
||||||
|
None => format!("rfenc-v{}", self.model_version),
|
||||||
|
};
|
||||||
let provenance = SemanticProvenance {
|
let provenance = SemanticProvenance {
|
||||||
evidence: quality.evidence_refs.iter().map(|e| format!("{e:?}")).collect(),
|
evidence: quality.evidence_refs.iter().map(|e| format!("{e:?}")).collect(),
|
||||||
model_version: format!("rfenc-v{}", self.model_version),
|
model_version,
|
||||||
calibration_version,
|
calibration_version,
|
||||||
privacy_decision: format!("{:?}/{:?}", self.privacy.active_mode(), effective_class),
|
privacy_decision: format!("{:?}/{:?}", self.privacy.active_mode(), effective_class),
|
||||||
};
|
};
|
||||||
@@ -373,6 +472,11 @@ impl StreamingEngine {
|
|||||||
// 7. Deterministic witness over the trust decision (ADR-137 §2.7).
|
// 7. Deterministic witness over the trust decision (ADR-137 §2.7).
|
||||||
let witness = witness_of(&provenance, effective_class);
|
let witness = witness_of(&provenance, effective_class);
|
||||||
|
|
||||||
|
// 8. Drift→recalibration advisor (ADR-135 → ADR-150 §3.4): sustained
|
||||||
|
// low coherence or an environment change-point recommends refit.
|
||||||
|
let recalibration_recommended =
|
||||||
|
self.recal.observe(quality.base_coherence, change_point.is_some());
|
||||||
|
|
||||||
self.cycle += 1;
|
self.cycle += 1;
|
||||||
Ok(TrustedOutput {
|
Ok(TrustedOutput {
|
||||||
semantic_id,
|
semantic_id,
|
||||||
@@ -383,6 +487,7 @@ impl StreamingEngine {
|
|||||||
directional,
|
directional,
|
||||||
change_point,
|
change_point,
|
||||||
witness,
|
witness,
|
||||||
|
recalibration_recommended,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -566,6 +671,93 @@ mod tests {
|
|||||||
assert_eq!(o1.quality.per_node_weights, o2.quality.per_node_weights);
|
assert_eq!(o1.quality.per_node_weights, o2.quality.per_node_weights);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ADR-150 §3.4 adapter provenance: activating a per-room adapter changes
|
||||||
|
/// the provenance model_version AND the witness — the exact weights shaping
|
||||||
|
/// inference are pinned in the trust chain, so an adapter can never swap
|
||||||
|
/// silently. Clearing it restores the base identity (and base witness).
|
||||||
|
#[test]
|
||||||
|
fn adapter_identity_is_witnessed() {
|
||||||
|
let cal = CalibrationId(9);
|
||||||
|
let frames = [node_frame(0, 1000, 56), node_frame(1, 1001, 56)];
|
||||||
|
|
||||||
|
let (mut e, room) = engine();
|
||||||
|
let base = e.process_cycle(&frames, cal, room, 1_000).unwrap();
|
||||||
|
assert_eq!(base.provenance.model_version, "rfenc-v1");
|
||||||
|
|
||||||
|
e.set_room_adapter(AdapterInfo {
|
||||||
|
adapter_id: "a1b2c3d4e5f60718".into(),
|
||||||
|
trained_samples: 150,
|
||||||
|
});
|
||||||
|
let adapted = e.process_cycle(&frames, cal, room, 2_000).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
adapted.provenance.model_version,
|
||||||
|
"rfenc-v1+adapter:a1b2c3d4e5f60718"
|
||||||
|
);
|
||||||
|
assert_ne!(adapted.witness, base.witness, "adapter must shift the witness");
|
||||||
|
|
||||||
|
// A different adapter id yields a different witness again.
|
||||||
|
e.set_room_adapter(AdapterInfo {
|
||||||
|
adapter_id: "ffffffffffffffff".into(),
|
||||||
|
trained_samples: 150,
|
||||||
|
});
|
||||||
|
let other = e.process_cycle(&frames, cal, room, 3_000).unwrap();
|
||||||
|
assert_ne!(other.witness, adapted.witness);
|
||||||
|
|
||||||
|
// Clearing restores the base identity and the base witness.
|
||||||
|
e.clear_room_adapter();
|
||||||
|
let back = e.process_cycle(&frames, cal, room, 4_000).unwrap();
|
||||||
|
assert_eq!(back.provenance.model_version, "rfenc-v1");
|
||||||
|
assert_eq!(back.witness, base.witness);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drift→recalibration advisor logic: a sustained low-coherence streak
|
||||||
|
/// recommends refit; a single healthy cycle resets the streak; a
|
||||||
|
/// change-point recommends immediately regardless of streak.
|
||||||
|
#[test]
|
||||||
|
fn recalibration_advisor_streak_and_change_point() {
|
||||||
|
let mut adv = RecalibrationAdvisor {
|
||||||
|
coherence_floor: 0.5,
|
||||||
|
streak_threshold: 3,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
// Healthy cycles never recommend and keep the streak at zero.
|
||||||
|
for _ in 0..5 {
|
||||||
|
assert!(!adv.observe(0.9, false));
|
||||||
|
}
|
||||||
|
assert_eq!(adv.streak(), 0);
|
||||||
|
// Two low cycles: not yet.
|
||||||
|
assert!(!adv.observe(0.2, false));
|
||||||
|
assert!(!adv.observe(0.2, false));
|
||||||
|
// Third consecutive low cycle: fire.
|
||||||
|
assert!(adv.observe(0.2, false));
|
||||||
|
// Recovery resets the streak.
|
||||||
|
assert!(!adv.observe(0.9, false));
|
||||||
|
assert_eq!(adv.streak(), 0);
|
||||||
|
// A change-point recommends immediately, even at full coherence.
|
||||||
|
assert!(adv.observe(0.9, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Engine-level: clean coherent cycles never recommend recalibration (the
|
||||||
|
/// advisor is wired into process_cycle and stays quiet on healthy input).
|
||||||
|
#[test]
|
||||||
|
fn healthy_cycles_do_not_recommend_recalibration() {
|
||||||
|
let (mut e, room) = engine();
|
||||||
|
e.set_recalibration_advisor(RecalibrationAdvisor {
|
||||||
|
coherence_floor: 0.5,
|
||||||
|
streak_threshold: 3,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
let cal = CalibrationId(2);
|
||||||
|
for i in 0..5u64 {
|
||||||
|
let frames = [
|
||||||
|
node_frame(0, 1_000 + i * 50_000, 56),
|
||||||
|
node_frame(1, 1_001 + i * 50_000, 56),
|
||||||
|
];
|
||||||
|
let out = e.process_cycle(&frames, cal, room, i as i64).unwrap();
|
||||||
|
assert!(!out.recalibration_recommended);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// WorldGraph belief retention: the live loop appends one SemanticState per
|
/// WorldGraph belief retention: the live loop appends one SemanticState per
|
||||||
/// cycle; past the cap the oldest beliefs are evicted so graph memory is
|
/// cycle; past the cap the oldest beliefs are evicted so graph memory is
|
||||||
/// bounded, while structural nodes and the newest belief always survive.
|
/// bounded, while structural nodes and the newest belief always survive.
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use wifi_densepose_bfld::PrivacyMode;
|
use wifi_densepose_bfld::PrivacyMode;
|
||||||
use wifi_densepose_engine::{EngineError, StreamingEngine, TrustedOutput};
|
use wifi_densepose_engine::{AdapterInfo, EngineError, StreamingEngine, TrustedOutput};
|
||||||
use wifi_densepose_geo::types::GeoRegistration;
|
use wifi_densepose_geo::types::GeoRegistration;
|
||||||
use wifi_densepose_signal::ruvsense::fusion_quality::CalibrationId;
|
use wifi_densepose_signal::ruvsense::fusion_quality::CalibrationId;
|
||||||
use wifi_densepose_worldgraph::WorldId;
|
use wifi_densepose_worldgraph::WorldId;
|
||||||
@@ -69,6 +69,18 @@ impl EngineBridge {
|
|||||||
self.engine.set_privacy_mode(mode);
|
self.engine.set_privacy_mode(mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Activate a per-room calibration adapter (ADR-150 §3.4). The adapter's
|
||||||
|
/// content-derived id becomes part of provenance/witness from the next
|
||||||
|
/// cycle — weights can never swap silently on the live path.
|
||||||
|
pub fn set_room_adapter(&mut self, info: AdapterInfo) {
|
||||||
|
self.engine.set_room_adapter(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deactivate the per-room adapter (revert to the shared base model).
|
||||||
|
pub fn clear_room_adapter(&mut self) {
|
||||||
|
self.engine.clear_room_adapter();
|
||||||
|
}
|
||||||
|
|
||||||
/// Borrow the engine (queries, WorldGraph snapshot, privacy audit).
|
/// Borrow the engine (queries, WorldGraph snapshot, privacy audit).
|
||||||
pub fn engine(&self) -> &StreamingEngine {
|
pub fn engine(&self) -> &StreamingEngine {
|
||||||
&self.engine
|
&self.engine
|
||||||
@@ -215,6 +227,36 @@ mod tests {
|
|||||||
assert!(bridge.engine().world().node_count() <= 3 + 5);
|
assert!(bridge.engine().world().node_count() <= 3 + 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn adapter_identity_flows_into_live_witness() {
|
||||||
|
let states = two_node_states_fixed();
|
||||||
|
let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R");
|
||||||
|
let base = bridge
|
||||||
|
.process_cycle_from_states(&states, 1_000)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
bridge.set_room_adapter(AdapterInfo {
|
||||||
|
adapter_id: "deadbeefcafef00d".into(),
|
||||||
|
trained_samples: 120,
|
||||||
|
});
|
||||||
|
let adapted = bridge
|
||||||
|
.process_cycle_from_states(&states, 2_000)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert!(adapted
|
||||||
|
.provenance
|
||||||
|
.model_version
|
||||||
|
.ends_with("+adapter:deadbeefcafef00d"));
|
||||||
|
assert_ne!(adapted.witness, base.witness);
|
||||||
|
// Clearing reverts to the base model identity.
|
||||||
|
bridge.clear_room_adapter();
|
||||||
|
let back = bridge
|
||||||
|
.process_cycle_from_states(&states, 3_000)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(back.provenance.model_version, "rfenc-v1");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn identity_strict_mode_is_carried_into_provenance() {
|
fn identity_strict_mode_is_carried_into_provenance() {
|
||||||
let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R");
|
let mut bridge = EngineBridge::new(PrivacyMode::PrivateHome, 1, "r", "R");
|
||||||
|
|||||||
@@ -1043,6 +1043,10 @@ struct AppStateInner {
|
|||||||
engine_bridge: engine_bridge::EngineBridge,
|
engine_bridge: engine_bridge::EngineBridge,
|
||||||
/// Witness of the most recent governed trust cycle (BLAKE3), for audit/UI.
|
/// Witness of the most recent governed trust cycle (BLAKE3), for audit/UI.
|
||||||
pub(crate) last_trust_witness: Option<[u8; 32]>,
|
pub(crate) last_trust_witness: Option<[u8; 32]>,
|
||||||
|
/// Latest drift→recalibration recommendation from the governed engine
|
||||||
|
/// (ADR-135 → ADR-150 §3.4): sustained low coherence or a change-point
|
||||||
|
/// suggests re-running the empty-room baseline / refitting the room adapter.
|
||||||
|
pub(crate) recalibration_recommended: bool,
|
||||||
/// SVD-based room field model for eigenvalue person counting (None until calibration).
|
/// SVD-based room field model for eigenvalue person counting (None until calibration).
|
||||||
field_model: Option<FieldModel>,
|
field_model: Option<FieldModel>,
|
||||||
// ── ADR-044 §5.2: adaptive rolling-p95 normalization ─────────────────────
|
// ── ADR-044 §5.2: adaptive rolling-p95 normalization ─────────────────────
|
||||||
@@ -5069,6 +5073,7 @@ async fn udp_receiver_task(state: SharedState, udp_port: u16) {
|
|||||||
.process_cycle_from_states(&sref.node_states, now_ms)
|
.process_cycle_from_states(&sref.node_states, now_ms)
|
||||||
{
|
{
|
||||||
sref.last_trust_witness = Some(trust.witness);
|
sref.last_trust_witness = Some(trust.witness);
|
||||||
|
sref.recalibration_recommended = trust.recalibration_recommended;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5538,6 +5543,7 @@ async fn udp_receiver_task(state: SharedState, udp_port: u16) {
|
|||||||
.process_cycle_from_states(&sref.node_states, now_ms)
|
.process_cycle_from_states(&sref.node_states, now_ms)
|
||||||
{
|
{
|
||||||
sref.last_trust_witness = Some(trust.witness);
|
sref.last_trust_witness = Some(trust.witness);
|
||||||
|
sref.recalibration_recommended = trust.recalibration_recommended;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6859,6 +6865,7 @@ async fn main() {
|
|||||||
"Default Room",
|
"Default Room",
|
||||||
),
|
),
|
||||||
last_trust_witness: None,
|
last_trust_witness: None,
|
||||||
|
recalibration_recommended: false,
|
||||||
field_model: if args.calibrate {
|
field_model: if args.calibrate {
|
||||||
info!("Field model calibration enabled — room should be empty during startup");
|
info!("Field model calibration enabled — room should be empty during startup");
|
||||||
FieldModel::new(field_bridge::single_link_config()).ok()
|
FieldModel::new(field_bridge::single_link_config()).ok()
|
||||||
|
|||||||
Reference in New Issue
Block a user