Root cause (investigated, not assumed): the heavy deps in a [mat] wheel
were NOT on the survivor-detection/triage path the Python binding uses.
`wifi-densepose-mat` pulled `wifi-densepose-nn` as a NON-optional dep, and
nn's default `onnx` feature drags in `ort` (ONNX Runtime) + its
download/reqwest/hyper stack. But nn is used ONLY by mat's `src/ml/`
module (debris + vital-signs ONNX classifiers), which is an OPTIONAL
runtime enhancement: `DetectionPipeline.ml_pipeline` is `Option<_>`,
created only when `DetectionConfig::enable_ml` is set, and
`from_disaster_config` (the DisasterResponse path) never enables it. So
the ONNX stack was compiled-and-linked but never executed by the binding.
Fix = feature-gating (not a hoist — a hoist was unnecessary):
- `wifi-densepose-nn` made optional; new `ml` Cargo feature gates it.
- `default = [..., "ml"]` so existing consumers are unchanged; `api`
implies `ml` (the REST surface exposes `ml_ready`).
- `#[cfg(feature = "ml")]` on the `ml` module, its crate-root + prelude
re-exports, the `MatError::Ml` variant, and every ml touchpoint in
detection/pipeline.rs (ml_config/ml_pipeline fields, constructor,
process_zone enhancement block, enhance_with_ml/get_ml_results/
initialize_ml/ml_ready/ml_pipeline accessors, update_config).
- Fixed a latent feature-unification bug surfaced by dropping nn:
integration/hardware_adapter.rs uses `tokio::select!`, which needs
`tokio/macros`. That was only satisfied transitively via nn; now
declared explicitly on mat's own tokio dep so --no-default-features
builds compile.
The ADR-185 [mat] wheel already depended on mat with
`default-features = false, features = ["std"]`, so no python/ change was
needed — dropping `ml` from the default now actually removes ort.
Measured (maturin build --release --features mat --strip):
BEFORE (ml/ort in wheel): 8.4 MB (over the ADR-117 §5.4 <=5 MB budget)
AFTER (ml gated off): 2.0 MB (under budget) — 76% smaller
Verified:
cargo build -p wifi-densepose-mat (default+ml) OK
cargo build -p wifi-densepose-mat --no-default-features --features std OK
cargo test --features mat --test mat_parity 2/2 pass
maturin develop --features mat + pytest tests/test_mat.py 7/7 pass
(same detection result: 1 survivor, triage Delayed — behavior-transparent)
The comment claimed interpolation but the function returned the bin center,
capping breathing-rate resolution at +/-half a bin. Implemented quadratic
(3-point parabolic) peak interpolation: delta = 0.5*(yL-yR)/(yL-2y0+yR), clamped
to [-0.5,0.5], with an edge fallback to bin center. For a parabola-shaped peak the
recovery is exact (delta=0.4 for a true peak at bin 10.4). Test asserts the result
lands within half a bin of truth and strictly beats the old bin-center estimate.
Co-Authored-By: claude-flow <ruv@ruv.net>
The ensemble gate (EnsembleClassifier::determine_triage) and the survivor
record (Survivor::new -> TriageCalculator::calculate) used two different
START-protocol approximations with different rate bands and movement handling.
The pipeline gated on the ensemble triage then discarded it and recomputed via
TriageCalculator, so a survivor could be admitted as one priority and recorded
as another (e.g. 28 bpm + Tremor: gate said Delayed, record said Immediate).
In a mass-casualty tool that divergence is a life-safety defect.
determine_triage now delegates to TriageCalculator (the single source of truth),
retaining only the ensemble confidence gate (low confidence -> Unknown, except
Immediate which is never suppressed). Updated unit + integration tests to the
canonical expectations and added a divergent-boundary regression asserting
gate triage == survivor-record triage.
Co-Authored-By: claude-flow <ruv@ruv.net>
Both triage paths in the Mass Casualty Assessment tool classified a
survivor as Deceased (Black) on "no breathing + no movement" while
completely ignoring the heartbeat signal:
- domain `TriageCalculator::calculate` → `combine_assessments(Absent, None)`
returned Deceased. That branch is in fact only reachable *because* a
heartbeat makes `has_vitals()` true (breathing+movement absent alone →
Unknown) — so every "Deceased" was a live person with a pulse.
- detection `EnsembleClassifier::determine_triage` (the path used by
`classify()`) returned Deceased on `!has_breathing && !has_movement`,
also ignoring `reading.heartbeat`.
A survivor with a detectable pulse but no sensed breathing/movement is in
respiratory arrest — the most time-critical *savable* state. Reporting them
Deceased would deprioritize a rescuable person. WiFi-CSI also cannot confirm
death (no airway-repositioning step), so a pulse must override.
Fix: in both paths, if the result would be Deceased but a heartbeat is
present, return Immediate. Total absence of breathing, movement AND heartbeat
is unchanged (domain → Unknown, ensemble → Deceased).
2 safety regression tests added. Full MAT suite: 168 + 6 + 3 passed, 0 failed
(existing test_no_vitals_is_deceased still green — no heartbeat → Deceased).
The Rust port lived two directories deep (rust-port/wifi-densepose-rs/)
without any sibling under rust-port/ that warranted the extra level.
Move the whole workspace up to v2/ to match v1/ (Python) at the same
depth and shorten every cd / build command across the repo.
git mv preserves history for all tracked files. 60 files updated for
path references (CI workflows, ADRs, docs, scripts, READMEs, internal
.claude-flow state). Two manual fixes for relative-cd paths in
CLAUDE.md and ADR-043 that became wrong after the depth change
(cd ../.. → cd ..).
Validated:
- cargo check --workspace --no-default-features → clean (after target/
nuke; the gitignored target/ was carried by the OS rename and had
hard-coded old paths in build scripts)
- cargo test --workspace --no-default-features → 1,539 passed, 0 failed,
8 ignored (same totals as pre-rename)
- ESP32-S3 on COM7 → still streaming live CSI (cb #40300, RSSI -64 dBm)
After-merge follow-up: contributors should `rm -rf v2/target` once and
let cargo regenerate from the new path.