problem
MeasurementReport::ir_stats() (#346, PR #352) passes estimate_onset a causal lower bound — the earliest sample pure flight time admits — only when the report carries both a measured τ and position.distance_m:
// ac-core/src/measurement/report/ir_stats.rs, on PR #352's branch
let min_admissible_index = match (
&self.interface_latency,
self.position.as_ref().and_then(|p| p.distance_m),
) {
(Some(InterfaceLatency::Measured(tau)), Some(distance_m)) => { … Some(…) }
_ => None,
};
Nothing that runs outside a test ever sets distance_m. Read at 298f77dc (PR #352 tip, main merged in):
ac-daemon/src/handlers/audio/plot.rs builds PositionSnapshot at lines 224, 548 and 954 — all three from temperature_c alone, ..Default::default() for the rest.
- Every
distance_m: Some(…) in ac-core (report/provenance.rs:348, report/ir_stats.rs:673, report_html/mod.rs:443, report_pdf/mod.rs:313,523, report_layout/sections.rs:302) is inside that file's #[cfg(test)] module.
ac-cli has no distance argument, config key or plumbing for plot ir.
So on real hardware min_admissible_index is always None, and every capture takes estimate_onset's unbounded ONSET_SEARCH_WINDOW_S path. The bounded path — the one #346's AC1/AC3 causality rejection and PR #352's "on the rig geometry is recorded" reasoning rest on — is exercised by unit tests and by nothing else.
evidence from the rig
#378 AC6, pupu, 2026-09-15 (record on PR #352): 48 acoustic and 10 loopback ac plot ir captures, all unbounded. On the electrical loopback, where onset and peak should nearly coincide, the pick landed 358 samples before the peak on every capture — the unbounded window reaching into the band-limited main lobe's leading skirt. A causal bound is exactly what PR #352 says rejects that; it never ran.
why it matters
This is the silent-config shape: a correctness guard that exists, is tested, reads as active in the code and the PR description, and does nothing on any real measurement. No output says the bound was skipped for want of a distance — onset_rule records "no causal bound", but nothing tells an operator that a distance could have been supplied, because it cannot.
It also means no rig session can currently test the bounded estimator at all: AC6 scored the unbounded path because that is the only one reachable.
acceptance criteria
out of scope
related
#346, #352, #378 (AC6 record), #351 (τ rule on the other half of the same bound), #283.
problem
MeasurementReport::ir_stats()(#346, PR #352) passesestimate_onseta causal lower bound — the earliest sample pure flight time admits — only when the report carries both a measured τ andposition.distance_m:Nothing that runs outside a test ever sets
distance_m. Read at298f77dc(PR #352 tip, main merged in):ac-daemon/src/handlers/audio/plot.rsbuildsPositionSnapshotat lines 224, 548 and 954 — all three fromtemperature_calone,..Default::default()for the rest.distance_m: Some(…)inac-core(report/provenance.rs:348,report/ir_stats.rs:673,report_html/mod.rs:443,report_pdf/mod.rs:313,523,report_layout/sections.rs:302) is inside that file's#[cfg(test)]module.ac-clihas no distance argument, config key or plumbing forplot ir.So on real hardware
min_admissible_indexis alwaysNone, and every capture takesestimate_onset's unboundedONSET_SEARCH_WINDOW_Spath. The bounded path — the one #346's AC1/AC3 causality rejection and PR #352's "on the rig geometry is recorded" reasoning rest on — is exercised by unit tests and by nothing else.evidence from the rig
#378 AC6, pupu, 2026-09-15 (record on PR #352): 48 acoustic and 10 loopback
ac plot ircaptures, all unbounded. On the electrical loopback, where onset and peak should nearly coincide, the pick landed 358 samples before the peak on every capture — the unbounded window reaching into the band-limited main lobe's leading skirt. A causal bound is exactly what PR #352 says rejects that; it never ran.why it matters
This is the silent-config shape: a correctness guard that exists, is tested, reads as active in the code and the PR description, and does nothing on any real measurement. No output says the bound was skipped for want of a distance —
onset_rulerecords "no causal bound", but nothing tells an operator that a distance could have been supplied, because it cannot.It also means no rig session can currently test the bounded estimator at all: AC6 scored the unbounded path because that is the only one reachable.
acceptance criteria
plot ircapture, and it reaches the persisted report'sposition.distance_m.onset_ruleshows the causal bound enforced — verified on a real capture, not only a fixture.plot_irround trip through the daemon with a supplied distance produces a report whoseir_stats()hasmin_admissible_index.is_some(). A test that buildsPositionSnapshotby hand would pass while the producer still drops the field.out of scope
related
#346, #352, #378 (AC6 record), #351 (τ rule on the other half of the same bound), #283.