Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot] 8b788e5638 chore: update vendor submodules to latest upstream 2026-06-02 19:22:56 +00:00
10 changed files with 21 additions and 183 deletions
+9 -11
View File
@@ -108,18 +108,16 @@ jobs:
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
# Swatinem/rust-cache replaces a naive `actions/cache` of the whole
# `v2/target`. That manual cache of a 38-crate target dir (multi-GB) was an
# intermittent failure source — several CI runs this cycle died at the
# cache/setup step (after toolchain install, before "Run Rust tests"),
# needing a rerun. rust-cache is purpose-built for Rust: it caches the
# registry + git + a pruned target, evicts stale deps, and restores far more
# reliably (and faster) on large workspaces. `workspaces: v2` points it at
# the v2/ cargo workspace (keys on v2/Cargo.lock, caches v2/target).
- name: Cache cargo (Swatinem/rust-cache)
uses: Swatinem/rust-cache@v2
- name: Cache cargo
uses: actions/cache@v4
with:
workspaces: v2
path: |
~/.cargo/registry
~/.cargo/git
v2/target
key: ${{ runner.os }}-cargo-${{ hashFiles('v2/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Run Rust tests
working-directory: v2
+2 -2
View File
@@ -1048,7 +1048,7 @@ The Rust sensing server binary accepts the following flags:
| `--dataset` | (none) | Path to dataset directory (MM-Fi or Wi-Pose) |
| `--dataset-type` | `mmfi` | Dataset format: `mmfi` or `wipose` |
| `--epochs` | `100` | Training epochs |
| `--export-rvf` | (none) | Export a **placeholder** RVF container-format demo and exit — **not a trained model**. For a real model use `--train` (+ `--save-rvf`) or download a pretrained encoder. |
| `--export-rvf` | (none) | Export RVF model container and exit |
| `--save-rvf` | (none) | Save model state to RVF on shutdown |
| `--model` | (none) | Load a trained `.rvf` model for inference |
| `--load-rvf` | (none) | Load model config from RVF container |
@@ -1359,7 +1359,7 @@ docker run --rm \
-v $(pwd)/output:/output \
--entrypoint /app/sensing-server \
ruvnet/wifi-densepose:latest \
--train --dataset /data --epochs 100 --save-rvf /output/model.rvf
--train --dataset /data --epochs 100 --export-rvf /output/model.rvf
```
The pipeline runs 10 phases:
@@ -172,14 +172,6 @@ impl EnsembleClassifier {
let has_movement = reading.movement.movement_type != MovementType::None;
if !has_breathing && !has_movement {
// SAFETY: a detectable heartbeat means the survivor is ALIVE. No
// sensed breathing/movement *with* a pulse is respiratory arrest —
// the most time-critical savable state (Immediate), never Deceased.
// Only the total absence of breathing, movement AND heartbeat is
// reported Deceased.
if reading.heartbeat.is_some() {
return TriageStatus::Immediate;
}
return TriageStatus::Deceased;
}
@@ -303,27 +295,6 @@ mod tests {
assert_eq!(result.recommended_triage, TriageStatus::Deceased);
}
/// SAFETY regression: heartbeat present but no sensed breathing/movement is
/// respiratory arrest — Immediate, never Deceased. Only the *total* absence
/// of breathing, movement AND heartbeat (the test above) is Deceased.
#[test]
fn test_heartbeat_with_no_breathing_or_movement_is_immediate() {
// breathing: None, heartbeat: Some(72 bpm), movement: None
let reading = make_reading(None, Some(72.0), MovementType::None);
let classifier = EnsembleClassifier::new(EnsembleConfig {
min_ensemble_confidence: 0.0,
..EnsembleConfig::default()
});
let result = classifier.classify(&reading);
assert_eq!(
result.recommended_triage,
TriageStatus::Immediate,
"a survivor with a pulse must never be triaged Deceased"
);
}
#[test]
fn test_ensemble_confidence_weighting() {
let classifier = EnsembleClassifier::new(EnsembleConfig {
@@ -104,20 +104,7 @@ impl TriageCalculator {
let movement_status = Self::assess_movement(vitals);
// Step 4: Combine assessments
let status = Self::combine_assessments(breathing_status, movement_status);
// Step 5: SAFETY OVERRIDE — a detectable heartbeat means the survivor is
// ALIVE. `combine_assessments` only sees breathing + movement, so a
// person with a pulse but no *sensed* breathing/movement (respiratory
// arrest, or breathing too shallow for CSI to pick up) would otherwise
// be reported Deceased and deprioritized for rescue. No breathing + a
// pulse is the most time-critical *savable* state, so escalate to
// Immediate rather than ever calling a survivor with a heartbeat dead.
if status == TriageStatus::Deceased && vitals.heartbeat.is_some() {
return TriageStatus::Immediate;
}
status
Self::combine_assessments(breathing_status, movement_status)
}
/// Assess breathing status
@@ -230,9 +217,7 @@ enum MovementAssessment {
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{
BreathingPattern, ConfidenceScore, HeartbeatSignature, MovementProfile, SignalStrength,
};
use crate::domain::{BreathingPattern, ConfidenceScore, MovementProfile};
use chrono::Utc;
fn create_vitals(
@@ -248,29 +233,6 @@ mod tests {
}
}
/// SAFETY regression: a survivor with a detectable heartbeat but no sensed
/// breathing or movement is in respiratory arrest — Immediate (Red), and
/// must NEVER be reported Deceased. (Before the fix, `combine_assessments`
/// ignored heartbeat and returned Deceased; that path was in fact only
/// reachable *because* a heartbeat made `has_vitals()` true.)
#[test]
fn heartbeat_with_no_breathing_or_movement_is_immediate_not_deceased() {
let vitals = VitalSignsReading {
breathing: None,
heartbeat: Some(HeartbeatSignature {
rate_bpm: 72.0,
variability: 0.1,
strength: SignalStrength::Moderate,
}),
movement: MovementProfile::default(),
timestamp: Utc::now(),
confidence: ConfidenceScore::new(0.8),
};
let status = TriageCalculator::calculate(&vitals);
assert_eq!(status, TriageStatus::Immediate, "pulse present ⇒ alive");
assert_ne!(status, TriageStatus::Deceased);
}
#[test]
fn test_no_vitals_is_unknown() {
let vitals = create_vitals(None, MovementProfile::default());
@@ -100,17 +100,7 @@ pub async fn require_bearer(
.headers()
.get(AUTHORIZATION)
.and_then(|v| v.to_str().ok())
// RFC 6750 §2.1 / RFC 7235 §2.1: the auth-scheme ("Bearer") is
// case-insensitive. Match it as such (and tolerate extra leading
// whitespace before the token) so a correct token isn't rejected
// just because a client sent `bearer`/`BEARER`. The token compare
// below stays exact + constant-time.
.and_then(|s| {
let (scheme, token) = s.split_once(' ')?;
scheme
.eq_ignore_ascii_case("Bearer")
.then(|| token.trim_start())
});
.and_then(|s| s.strip_prefix("Bearer "));
let ok = supplied
.map(|s| ct_eq(s.as_bytes(), expected.as_bytes()))
.unwrap_or(false);
@@ -195,31 +185,6 @@ mod tests {
);
}
#[tokio::test]
async fn accepts_case_insensitive_bearer_scheme() {
// RFC 6750 §2.1 / RFC 7235 §2.1: the auth-scheme is case-insensitive.
// A correct token must authenticate regardless of scheme casing or
// extra whitespace; a wrong token must still be rejected.
async fn req_status(auth_value: &str) -> StatusCode {
let r = wrap(AuthState::from_token("s3cr3t"));
let mut req = Request::builder()
.method("GET")
.uri("/api/v1/info")
.body(Body::empty())
.unwrap();
req.headers_mut()
.insert(AUTHORIZATION, auth_value.parse().unwrap());
r.oneshot(req).await.unwrap().status()
}
assert_eq!(req_status("Bearer s3cr3t").await, StatusCode::OK);
assert_eq!(req_status("bearer s3cr3t").await, StatusCode::OK);
assert_eq!(req_status("BEARER s3cr3t").await, StatusCode::OK);
assert_eq!(req_status("Bearer s3cr3t").await, StatusCode::OK); // extra space
// Scheme leniency must NOT weaken the token check.
assert_eq!(req_status("bearer nope").await, StatusCode::UNAUTHORIZED);
assert_eq!(req_status("Basic s3cr3t").await, StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn enabled_blocks_api_with_wrong_bearer() {
let r = wrap(AuthState::from_token("s3cr3t"));
@@ -5619,16 +5619,6 @@ fn diagnose_model_load_error(path: &std::path::Path, data: &[u8], err: &str) ->
)
}
/// Whether `--export-rvf` should emit the placeholder container-format demo.
///
/// It must only do so **standalone**. Combined with `--train`/`--pretrain` the
/// real model is produced by the training pipeline, so short-circuiting here
/// would silently skip training and write placeholder weights — the #894 bug
/// where the documented `--train … --export-rvf` workflow produced a fake model.
fn export_emits_placeholder_demo(export_set: bool, train: bool, pretrain: bool) -> bool {
export_set && !train && !pretrain
}
// ── Main ─────────────────────────────────────────────────────────────────────
/// If `--ui-path` points nowhere (wrong cwd), try common repo layouts relative to cwd.
@@ -5672,24 +5662,9 @@ async fn main() {
return;
}
// Handle --export-rvf: writes a CONTAINER-FORMAT DEMO with placeholder
// weights — it is NOT a trained model. Only short-circuit when standalone:
// combined with --train/--pretrain the real model is exported by the
// training pipeline, and short-circuiting here would silently skip training
// and write placeholder weights (#894 — the documented `--train …
// --export-rvf` workflow produced a placeholder and never trained).
if export_emits_placeholder_demo(args.export_rvf.is_some(), args.train, args.pretrain) {
let rvf_path = args
.export_rvf
.as_ref()
.expect("export_emits_placeholder_demo implies export_rvf is set");
eprintln!(
"WARNING: --export-rvf writes a CONTAINER-FORMAT DEMO with placeholder \
weights it is NOT a trained model. Train one with \
`--train --dataset <DIR>` (which exports a calibrated .rvf to the \
models/ directory), or download a pretrained encoder. See issue #894."
);
eprintln!("Exporting RVF container package (placeholder weights)...");
// Handle --export-rvf mode: build an RVF container package and exit
if let Some(ref rvf_path) = args.export_rvf {
eprintln!("Exporting RVF container package...");
use rvf_pipeline::RvfModelBuilder;
let mut builder = RvfModelBuilder::new("wifi-densepose", "1.0.0");
@@ -5738,13 +5713,6 @@ async fn main() {
}
}
return;
} else if args.export_rvf.is_some() {
// --export-rvf alongside --train/--pretrain: don't emit a placeholder.
// Fall through so training runs; it exports the real calibrated model.
eprintln!(
"Note: --export-rvf is ignored in training mode — the trained model \
is exported by the training pipeline to the models/ directory."
);
}
// Handle --pretrain mode: self-supervised contrastive pretraining (ADR-024)
@@ -7342,29 +7310,3 @@ mod model_load_diagnostic_tests {
assert!(msg.contains("wifi-densepose-train"), "{msg}");
}
}
#[cfg(test)]
mod export_rvf_mode_tests {
use super::export_emits_placeholder_demo;
#[test]
fn standalone_export_emits_placeholder() {
// --export-rvf alone → the container-format demo (placeholder weights).
assert!(export_emits_placeholder_demo(true, false, false));
}
#[test]
fn export_with_train_does_not_short_circuit() {
// #894: `--train --export-rvf` must NOT emit a placeholder + skip
// training — it must fall through to the real training pipeline.
assert!(!export_emits_placeholder_demo(true, true, false));
assert!(!export_emits_placeholder_demo(true, false, true));
assert!(!export_emits_placeholder_demo(true, true, true));
}
#[test]
fn no_export_flag_never_emits() {
assert!(!export_emits_placeholder_demo(false, false, false));
assert!(!export_emits_placeholder_demo(false, true, false));
}
}
+1 -1