Commit Graph

1019 Commits

Author SHA1 Message Date
rUv 4a083999e5
security(ruview-swarm): fail-closed on NaN/Inf at the swarm-comm trust boundary + ADR-176 (#1096)
* fix(ruview-swarm): fail-closed on NaN/Inf at swarm-comm trust boundary (ADR-148)

Beyond-SOTA security review of the ADR-148 drone swarm control plane found
four IEEE-754 NaN/Inf fail-open / DoS bugs on data crossing the untrusted
swarm-comm boundary (receive_peer_state / receive_peer_detection accept full
DroneState/CsiDetection whose f64/f32 fields deserialize with no finite-check).

- HIGH: failsafe::tick collision-avoidance + battery checks fail-open on NaN
  (NaN < threshold == false silently disabled collision avoidance / kept a
  NaN-battery drone Nominal). Now fails closed to EmergencyDiverge / RTH.
- MED: geofence::check NaN-altitude bypass returned Safe through the
  point-in-polygon path. Now leading non-finite-coordinate guard -> HardBreach.
- MED/DoS: antijamming FhssRadio panicked with "% 0" on an empty deserialized
  channels_mhz. Now len==0 early-returns (benign 0.0 sentinel).
- LOW: multiview::fuse propagated a NaN victim_position into the fused
  "confirmed victim" location. Now requires finite confidence + position.

Each fix pinned by a fails-on-old / passes-on-new test (MEASURED: old code
returned Nominal/Safe or panicked). cargo test -p ruview-swarm
--no-default-features: 117 -> 123 passed, 0 failed. Workspace green; Python
deterministic proof unchanged (f8e76f21...46f7a, off the signal path).

Documented-not-fixed (ADR slot 176): Raft AppendEntries lacks Log-Matching
consistency check (topology/raft.rs); MavlinkSigner::verify uses non-constant
-time tag compare + no replay-window rejection (already doc-flagged).

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr): ADR-176 — ruview-swarm NaN-fail-open safety review

Records the 4 MEASURED fail-open safety bugs fixed in f671000d7 (collision
avoidance, battery RTH, geofence, anti-jamming %0 panic — all NaN/Inf
defeating a safety comparison at the swarm-comm trust boundary) + 6 pins,
5 clean-with-evidence dimensions, and the 2 genuine issues deferred to a
focused follow-up (Raft AppendEntries log-matching; MAVLink signer
constant-time + replay window).

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-15 09:55:40 -04:00
rUv 0f64d23516
feat(bench): int8 quantization of WiFlow-STD half pose model — MEASURED trade-off (ADR-175, honest negative) (#1095)
Sub-deliverable 8.2 of the benchmark/optimization milestone. Quantizes the
843,834-param "half" WiFlow-STD pose model (half_best.pth) to int8 two ways and
MEASURES the accuracy/size trade-off vs fp32 under ONE locked normalization
(ADR-173 torso-diameter PCK, upstream calculate_pck use_torso_norm=True), on the
same seed-42 file-level 70/15/15 test split that produced the fp32 sweep numbers.

MEASURED on ruvultra (RTX 5080, torch 2.11.0+cu128, fbgemm; clean test, torso-PCK):
  fp32             96.62% pck@20  99.47% pck@50  0.008981 mpjpe  3.351 MB
  int8 PTQ static  40.98% pck@20  94.98% pck@50  0.038262 mpjpe  1.046 MB  (-55.64pp)
  int8 QAT (3 ep)  67.48% pck@20  98.69% pck@50  0.026548 mpjpe  1.043 MB  (-29.15pp)

Verdict (honest no): int8 is NOT a win at the strict PCK@20 edge target. Static
PTQ collapses; QAT recovers a large share but still loses 29 pp @20 for a 3.2x
size win — keep fp32/fp16 on the edge. Disclosed: QAT fake-quant val pck@20 was
83.45% but converted int8 scores 67.48% (~16pp convert_fx gap, reported honestly).

Deliverables:
- v2/crates/wifi-densepose-train/scripts/quantize_half_int8.py (reproducible:
  header carries the exact ssh command + run date; QAT primary, static PTQ fallback)
- docs/adr/ADR-175-int8-quantization-half-pose-model-measured.md (MEASURED table,
  locked normalization, QAT-vs-PTQ labeling, verdict, reproduction, limitations)
- CHANGELOG [Unreleased] ### Added entry

No production Rust or signal-pipeline change. Python deterministic proof unchanged
(f8e76f21a0f9852b70b6d9dd5318239f6b20cbcb4cdd995863263cecdc446f7a, bit-exact).
2026-06-15 09:16:22 -04:00
rUv b209b8b778
ci(bench): compile-verify regression gate for v2 criterion benches + ADR-174 (#1094)
* ci(bench): wire v2 criterion benches into CI as a compile-verify regression gate

Sub-deliverable 8.3 of the benchmark/optimization milestone (needs ADR slot 174).

The v2/ workspace ships 26 criterion benches across 18 crates, but benches are
not part of `cargo test`, so nothing in CI compiled them and they silently rot
when a public API they call changes.

Add `.github/workflows/bench-regression.yml`:
  - bench-compile (HARD GATE): `cargo bench --workspace --no-default-features
    --no-run` compiles + links every default-feature bench (no measurement) plus
    the cir-gated cir_bench — a real, deterministic regression guard against
    bench bit-rot.
  - bench-fast-run (INFORMATIONAL, continue-on-error, never gates): runs a
    curated pure-CPU subset (nvsim, ruvector sketch/fusion) in criterion
    quick-mode and uploads logs as an artifact.

No timing-regression gate, by design: wall-clock on shared GitHub runners varies
2-3x run-to-run, so a hard threshold or cross-runner `criterion --baseline`
compare would manufacture false failures. The honest scope is compile-verify +
informational-run; the workflow header documents the self-hosted-runner
condition under which true timing-gating becomes honest. The crv-gated crv_bench
is excluded because its crates.io dep ruvector-crv 0.1.1 fails to build upstream.

Running the gate immediately caught one already-bit-rotted bench:
wifi-densepose-mat/detection_bench failed to compile (E0063: missing field
last_rssi in SensorPosition). Fixed (last_rssi: None) and re-verified.

Validation (MEASURED): mat detection_bench + cir_bench + nvsim + ruvector +
vitals + swarm benches compile under --no-default-features; fast subset runs;
`cargo test -p wifi-densepose-mat --no-default-features` 174 passed / 0 failed;
Python proof PASS, hash f8e76f21...46f7a unchanged.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr): ADR-174 — CI bench-regression compile-verify gate

Records sub-deliverable 8.3 (bench-regression.yml, committed c4c59e085):
a hard compile-verify gate over all 26 v2 criterion benches (caught + fixed
one real bit-rotted bench, mat/detection_bench E0063) + an informational
fast-run. Documents the honest scope — no timing-regression gate, since
shared-runner wall-clock varies 2-3x; states the self-hosted-runner condition
under which timing gating becomes honest.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-15 08:26:38 -04:00
rUv 90a88ada9a
feat(train): metric-locked PCK/MPJPE accuracy harness + ADR-173 (resolve PCK-definition ambiguity) (#1092)
* feat(train): metric-locked PCK/MPJPE accuracy harness — resolve PCK-definition ambiguity

The SOTA brief (docs/research/sota-nn-train-benchmark-brief.md §1/§3.1/§4)
identifies metric ambiguity as the single biggest threat to any beyond-SOTA
claim: three PCK@20 numbers (96.09% WiFlow-STD image-normalized, 81.63%
AetherArena torso-PCK, 61.1% GraphPose-Fi standard PCK) cannot be lined up
because each silently uses a different normalization. The project was retracted
twice over this (a withdrawn 92.9% used absolute pixels, not torso).

New src/accuracy.rs makes the normalizer explicit, selectable, and carried with
every reported number:
- PckNormalization enum: TorsoDiameter (standard MM-Fi/GraphPose-Fi hip↔hip),
  BoundingBoxDiagonal (looser WiFlow-STD image-normalized), AbsolutePixels(t)
  (retracted convention, reproducible + clearly non-comparable).
- pck_at(pred, gt, vis, k, normalization) — one canonical PCK reusing the
  metrics_core geometric primitives (no duplicate kernel).
- mpjpe(pred, gt, vis) — 2D/3D, mm.
- PoseAccuracy { pck_at: BTreeMap<u8,f32>, mpjpe, normalization, n_keypoints,
  n_frames } via accuracy_report(frames, ks, normalization) — an unlabeled PCK
  number is structurally impossible.

17 hand-computed deterministic tests (no GPU, no datasets) prove the harness
arithmetic, including the key proof that identical predictions score
0.50 / 1.00 / 0.75 under the three normalizations, plus graceful degenerate
handling (zero torso, empty frames, NaN coords — no panic, never false-perfect).

This is measurement infrastructure, NOT an accuracy claim. Public API worth an
ADR — needs ADR slot 173 (parent to write).

wifi-densepose-train lib 191→206, test_metrics 12→14, 0 failed; full workspace
green (exit 0); Python deterministic proof unchanged
(f8e76f21a0f9852b70b6d9dd5318239f6b20cbcb4cdd995863263cecdc446f7a).

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr): ADR-173 — metric-locked PCK/MPJPE accuracy harness

Documents the accuracy harness (committed 3a8b2ed13) that resolves the
PCK-definition ambiguity flagged as the #1 beyond-SOTA risk in the SOTA brief
(#1090): three historical numbers (96/81.6/61) used three unstated
normalizations. The harness makes normalization explicit + selectable
(PckNormalization enum) and every reported number carries its definition.
Key proof: identical predictions → 0.50/1.00/0.75 under torso/bbox/abs.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-15 00:41:02 -04:00
rUv cfd0ad76cf
security(core,cli): pin CSI-deserialiser DoS-resistance + ADR-172 (clean-with-evidence) (#1091)
* test(core,cli): pin DoS-resistance of CSI deserialisers (ADR-127 security review)

Beyond-SOTA security review of wifi-densepose-core + wifi-densepose-cli.
Load-bearing-question verdict: the NaN-state-poisoning bug class does NOT
originate in core — core exposes no stateful accumulator (no Welford,
von-Mises, IIR, voxel grid, running mean); each downstream crate rolls its
own, so each fix is correctly local. Both crates confirmed clean on every
reviewed dimension (panic-on-adversarial-input, NaN handling, unbounded
memory, path traversal, secrets) — no production code changed.

Adds 4 regression pins locking in two existing-but-untested DoS guards:
- core: from_canonical_bytes shape guard (Vec::with_capacity bound) — proven
  to fail with `capacity overflow` when the saturating-mul guard is removed.
- core: canonical decoder never panics on arbitrary/truncated bytes.
- cli: parse_csi_packet rejects an oversized n_antennas*n_subcarriers claim
  before Array2 allocation (33 MB claim in a 2 KB datagram -> None).
- cli: parse_csi_packet never panics on arbitrary UDP bytes.

core: 35 -> 37 lib tests; cli: 24 -> 26 tests; 0 failed. Python proof
unchanged (f8e76f21…46f7a — off the signal path).

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr): ADR-172 — wifi-densepose-cli + core CSI-deserialiser security review

Records the clean-with-evidence verdict + 4 DoS-resistance regression pins
(test-only, committed in a1051607d). Documents the load-bearing finding:
the NaN-state-poisoning bug class does NOT originate in a shared core
primitive (core exposes no stateful accumulator — MEASURED via grep), so
the 3 prior downstream-local fixes are complete. Gives the wifi-densepose-cli
review its own ADR slot (core portion cross-refs ADR-127 §9).

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-14 23:58:09 -04:00
rUv 71e8756051
docs(research): SOTA evidence brief for nn/train benchmark ADR (#1090) 2026-06-14 23:32:58 -04:00
rUv 5287497a4a
security(homecore-migrate): redact secret value from malformed secrets.yaml error (#1089)
* fix(homecore-migrate): redact secret value from malformed secrets.yaml error (secret-leak)

`read_secrets` wrapped serde_yaml's parse error into `MigrateError::YamlParse {
source }`. serde_yaml's message for a typed-tag coercion failure embeds the
offending scalar verbatim, e.g. `invalid value: string "<the-secret-value>"`.
That error propagates out of `read_secrets`, is `?`-returned by the
`InspectSecrets` CLI path in main.rs, and printed to stderr by anyhow — leaking
a secret value despite the CLI's deliberate `<redacted>` design.

Fix: secrets.yaml parse failures now map to a new redacting variant
`MigrateError::SecretsParse { path, line, column }` that carries only the file
path and a coarse location (from `serde_yaml::Error::location()`), never the
scalar content. Other (non-secret) YAML files keep `YamlParse`.

Pinned by `secrets::tests::malformed_secrets_error_never_contains_secret_value`
(asserts the rendered error AND its full #[source] chain never contain the
secret value; fails on the old `YamlParse` path) plus
`malformed_secrets_error_reports_location` (still fail-closed + locatable).

ADR-165 secret-handling rule: a secret value must never appear in output.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(homecore-migrate): record secret-leak fix in ADR-165 + CHANGELOG

Note the secrets.yaml error-redaction fix and the review's clean dimensions
(read-only source / no traversal / no panic / fail-closed versioning / no
injection) in ADR-165 §2.4, bump the test-evidence count 19→21 in §2.6, and add
an [Unreleased] Security entry to CHANGELOG.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-14 23:09:55 -04:00
rUv bf1dfe79fd
fix(homecore core): TOCTOU race dropped/reordered state_changed events under concurrent writers (~93k→0) + 2 fail-closed hardenings (#1087)
* fix(homecore): atomic state set — close TOCTOU lost/reordered state_changed events

StateMachine::set did get() (release shard lock) → compute next + no-op
decision → insert() (re-acquire lock) → send(). The read-modify-write was
not atomic w.r.t. a concurrent writer on the same entity: a writer that
read a stale `old` could mis-classify a real transition as a no-op and drop
its state_changed event (a missed automation trigger) or fire an event whose
new_state duplicated the previously delivered one (a spurious trigger for any
automation keyed on old_state != new_state). ADR-127 §2.1 promises "writer
atomically replaces the map entry"; the implementation did not.

Fix: hold the DashMap shard write-lock across the whole read→decide→insert→
fire sequence via entry()/insert_entry(). tx.send is non-blocking, non-async,
and never re-enters the map, so firing under the shard lock cannot deadlock
and keeps global event order in lock-step with global commit order.

Pinned by concurrent_set_fires_no_duplicate_adjacent_events: 4 writers
toggling one entity A/B; asserts no two consecutive fired events carry the
same new_state (impossible under correct serialisation). Fails reliably on
the old code (~365-476 duplicate-adjacent events on the first trial), passes
on the fix across repeated runs.

Co-Authored-By: claude-flow <ruv@ruv.net>

* harden(homecore): bound entity_id length — close memory-DoS at the REST boundary

homecore-api/src/rest.rs parses untrusted path segments straight through
EntityId::parse (get/delete/set_state). With no length cap, an otherwise-valid
id like "a." + many MB of [a-z0-9_] was accepted; a POST /api/states/<giant>
would persist it into the DashMap state store, permanently growing memory
(amplification across distinct ids).

Fix: reject ids longer than MAX_ENTITY_ID_LEN (255, HA-compatible) up front in
parse(), before any per-char scan, with a new EntityIdError::TooLong. Fails
closed at the boundary type so every caller (REST, registry deserialize,
automation) is protected.

Pinned by entity_id_length_boundary: exactly-MAX accepted, MAX+1 rejected,
4 MiB id rejected as TooLong. Fails on old code (oversized parses Ok).

Co-Authored-By: claude-flow <ruv@ruv.net>

* harden(homecore): isolate panicking service handlers (catch_unwind)

ServiceRegistry::call already ran handlers outside the registry lock (the
Arc<dyn ServiceHandler> is cloned out of the read guard first), so a panic
could never poison the RwLock or block other callers — good. But a panicking
handler unwound through call() into the caller's task; the task driving the
engine (e.g. an axum request handler invoking a service) could be aborted by
one buggy integration.

Fix: wrap the handler future in AssertUnwindSafe + FutureExt::catch_unwind and
convert a panic into ServiceError::HandlerPanicked. Mirrors HA isolating
service-handler exceptions. The registry stays fully usable afterwards.

Pinned by panicking_handler_is_isolated_and_registry_survives: the panicking
call returns HandlerPanicked (not an unwind), a sibling healthy service still
returns its value, and the bad service remains registered. Fails on old code
(the await point panics instead of returning Err).

Co-Authored-By: claude-flow <ruv@ruv.net>

* test(homecore): pin event-bus lag safety (bounded broadcast, no DoS)

Documents-with-evidence that the core EventBus does NOT have the homecore-api
WS broadcast-lag failure: with EVENT_CHANNEL_CAPACITY=4096, firing 3x capacity
while a subscriber never drains keeps fire_* non-blocking (publisher never
waits on slow receivers), gives the slow receiver a recoverable Lagged(n)
(drop-oldest + re-sync) rather than a closed channel, and leaves the bus live
for a fresh fast subscriber. No code change — pins the clean dimension.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(homecore): record ADR-127 §9 security+concurrency review + CHANGELOG

Documents the three pinned fixes (HC-RACE-01 state-set TOCTOU, HC-EID-LEN-01
entity_id memory-DoS, HC-SVC-PANIC-01 service-handler isolation) and the
clean dimensions (bounded event-bus lag handling, lock discipline / no
lock-across-await, no panic-on-input) with their evidence.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-14 22:28:05 -04:00
rUv 9b126e927e
harden(assist security): bound untrusted utterance (DoS); cmd-injection/ReDoS/NaN/fail-open all proven clean with evidence (#1086)
* fix(homecore-assist): bound untrusted utterance length, fail closed (ADR-133 security)

The intent recognizers accept utterances from untrusted callers (voice
transcripts, the WebSocket `assist` command). Neither the regex nor the
semantic path bounded utterance length, so a pathological multi-megabyte
utterance forced an unbounded `to_lowercase()` clone plus a per-registered-
pattern scan (and, in the semantic path, full tokenisation + feature-hash
embedding) — an allocation/CPU amplification on attacker-controlled input.
The `regex` crate is linear-time (no catastrophic backtracking), so this was
a throughput/memory DoS rather than a hang, but it was still unbounded.

Fix: introduce MAX_UTTERANCE_BYTES (4 KiB — far above any real spoken
command) and check it at both recognizer boundaries BEFORE any allocation or
scan. An over-length utterance fails closed: Ok(None) (no intent, no action),
identical to an unrecognised phrase. No legitimate command is affected.

Pinned by fails-on-old tests:
  - recognizer::over_length_utterance_fails_closed — an over-length utterance
    that contains a valid command resolves to None (would have matched before)
  - semantic_recognizer::over_length_utterance_fails_closed_semantic

Co-Authored-By: claude-flow <ruv@ruv.net>

* test(homecore-assist): pin clean security dimensions with evidence (ADR-133)

Adds regression tests documenting the dimensions reviewed and found clean,
so the properties cannot silently regress:

  - runner: no subprocess surface exists. RufloRunnerOpts.{script_path,env}
    are inert and never executed; even a hostile script_path/env spawns
    nothing. And the entity_id capture class [a-z0-9_ .] strips every shell
    metacharacter, so a resolved slot can never carry ; | & $ ` / etc into a
    (future) argv — sanitisation by construction.
    (shell_metachars_never_survive_into_a_resolved_slot,
     runner_opts_are_inert_no_process_spawned)
  - recognizer: the regex crate is a linear-time finite automaton; a classic
    catastrophic-backtracking shape (a+)+$ on adversarial input completes in
    bounded time — no ReDoS.
    (pathological_backtracking_pattern_completes_in_bounded_time)
  - embedding: embeddings are structurally finite (FNV feature-hash + guarded
    L2 normalise, no external float input, no unguarded division), so a crafted
    utterance cannot inject NaN/Inf to poison cosine k-NN; cosine against the
    zero vector is a finite 0.0, never NaN.
    (embeddings_are_structurally_finite, cosine_with_zero_vector_is_finite_not_nan,
     empty_utterance_against_empty_index_no_panic_no_match)
  - pipeline: injection-shaped utterances never deliver a metacharacter into a
    service call; the worst case resolves to a clean entity token, and an
    unrecognised utterance fails closed to not_understood (no action).
    (pipeline_injection_shaped_utterance_carries_no_metachars_to_service)

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(homecore-assist): record ADR-133 security review (HC-ASSIST-01 + clean dims)

CHANGELOG [Unreleased] Security entry + ADR-133 section 6 review notes for the
homecore-assist voice/intent pipeline review.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-14 21:34:38 -04:00
rUv 41bee64593
fix(recorder): bound history query (memory-DoS) + add missing transactional purge (disk-DoS); SQL-injection & NaN dims clean (#1084)
* fix(homecore-recorder): bound history query + add transactional purge (memory-DoS + disk-DoS)

Security review of the HA-compat state recorder (ADR-132) found two real
bounding bugs; SQL-injection and NaN-index dimensions confirmed clean.

(1) Memory-DoS: get_state_history carried no LIMIT — a wide [since,until]
    window over a high-frequency entity loaded an unbounded row set into a
    single in-memory Vec. Added LIMIT MAX_HISTORY_ROWS (1,000,000); the
    sibling search paths were already k-bounded.

(2) Disk-DoS / documented-but-missing purge: README advertised
    Recorder::purge(older_than) but no retention path existed -> unbounded
    disk growth. Added a transactional purge with an EXCLUSIVE cutoff
    (idempotent, no off-by-one) that deletes old states+events and
    garbage-collects orphaned state_attributes blobs (dedup-shared blobs
    are kept until their last referencing state is gone). All three deletes
    run in one transaction so a mid-purge failure rolls back cleanly.

Pinning tests (homecore-recorder 19->25 no-default / 25->31 ruvector, 0 failed):
- malicious_entity_id_is_stored_literally_not_executed (SQL injection)
- like_metacharacters_in_query_are_literal_not_wildcards (LIKE escape)
- history_query_carries_a_limit_clause (memory-DoS bound)
- purge_keeps_boundary_row_and_drops_older (exclusive-cutoff, true pin)
- purge_gcs_orphaned_attributes_but_keeps_shared (dedup-safe GC)
- purge_also_removes_old_events

No behaviour change beyond the two fixes. Python deterministic proof
unchanged (recorder is off the signal proof path).

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(homecore-recorder): record ADR-132 security review findings

Add a "3a. Security review" section to ADR-132 and a CHANGELOG [Unreleased]
Security entry covering the homecore-recorder review: SQL-injection and
NaN-index dimensions confirmed clean with evidence (every query bound; LIKE
pattern bound+escaped; SHA-256->i32->f32 embeddings always finite, empty
index/k=0 probed no-panic), plus the two fixes (unbounded history LIMIT,
transactional exclusive-cutoff purge with orphan-attribute GC).

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-14 21:00:52 -04:00
rUv 5bc3b634b7
fix(automation security): template-bomb DoS (100MB/11s render → fuel-bounded, HIGH) + delay panic-on-config (MEDIUM) (#1083)
* fix(homecore-automation): bound template render to stop unbounded-expansion DoS (HC-SEC-01)

A `template:` condition / value_template comes straight from user
automation config and was rendered with MiniJinja's default (no
instruction budget, no output cap). A single condition such as
`{% for i in range(5000) %}{% for j in range(5000) %}xxxx{% endfor %}{% endfor %}`
rendered a 100 MB string over ~11 s on one render call (proven
empirically) — a CPU/memory denial of service, the bfld-class
"unbounded expansion".

Fix:
- Enable MiniJinja's `fuel` feature and set a per-render instruction
  budget (`set_fuel(Some(1_000_000))`). A nested loop burns one unit
  per iteration, so the budget caps total work regardless of nesting;
  the attack now fails fast (~90 ms) with "engine ran out of fuel".
- Reject template sources over 64 KiB before compilation (defense in
  depth so a pathological literal can neither compile nor emit verbatim).

Legitimate HA templates (a few dozen instructions) are unaffected.

Tests (fail on old — unbounded render / no rejection):
- nested_loop_template_is_bounded_not_unbounded_dos
- single_huge_repeat_template_is_bounded
- oversized_template_source_is_rejected
- legitimate_template_still_renders_within_fuel (no regression)

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix(homecore-automation): stop crafted delay/timeout from panicking the run task (HC-SEC-02)

`Action::Delay { seconds }` and `Action::WaitForTrigger { timeout_seconds }`
fed the user-supplied float straight into `Duration::from_secs_f64`, which
PANICS on negative, NaN, infinite, or overflowing inputs. All of those are
reachable from a crafted (or simply typo'd) automation YAML —
`delay: {seconds: -1}`, `.nan`, `.inf`, `1e308` — so one hostile config
aborts the spawned automation task with a panic
("cannot convert float seconds to Duration: value is negative", proven
empirically).

Fix: a `safe_duration_from_secs` guard that saturates instead of panicking,
matching Home Assistant's lenient "non-positive delay = no delay":
- NaN / ±inf / negative -> Duration::ZERO
- absurdly large (would overflow) -> clamped to ~100 years (MAX_DELAY_SECS)

Tests (fail on old — panic = failure):
- delay_negative_seconds_does_not_panic
- delay_nan_seconds_does_not_panic
- delay_infinite_seconds_does_not_panic
- wait_for_trigger_negative_timeout_does_not_panic
- safe_duration_saturates_hostile_values (incl. overflow clamp)

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(homecore-automation): record HC-SEC-01/02 security review (CHANGELOG + ADR-129 §8a)

Document the two DoS findings (template unbounded-expansion HC-SEC-01,
delay panic-on-config HC-SEC-02) and the dimensions probed clean
(condition fail-closed, bounded run-modes, sandboxed read-only templates).

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-14 20:22:07 -04:00
rUv e1f4897269
fix(geo numerical): parse_hgt underflow/inf-grid (HIGH) + haversine asin-NaN; pointcloud confirmed-robust (NaN-poisoning class, 3rd find) (#1081)
* fix(geo numerical robustness): parse_hgt underflow panic + haversine asin-domain NaN

Targeted numerical-robustness audit of wifi-densepose-geo (ADR-154-class sweep).

Two real bugs, each pinned by a fails-on-old test:

1. terrain.rs parse_hgt — usize underflow panic on degenerate input.
   `side = sqrt(n_samples)`; for empty / sub-2x2 buffers side <= 1, so
   `1.0 / (side - 1)` underflows `usize` (panic "attempt to subtract with
   overflow" in debug; wraps to a huge value in release → garbage/inf
   cell_size_deg that poisons every ElevationGrid::get). A truncated HTTP
   body or a 404 HTML page reaches parse_hgt. Now bails with a clear error
   when side < 2.

2. coord.rs haversine — asin domain overflow → NaN for (near-)antipodal
   points. Floating rounding can push `h.sqrt()` to 1.0 + ~4e-16, and
   `asin(>1)` is NaN (verified: pair (-44.4994,-178.95722)→(44.49939999,
   1.04278001) yields h=1.0000000000000004). A NaN distance silently breaks
   all downstream `<`/`>` comparisons. Clamp into [0,1] before asin.

Also pins the ±90° pole-singularity (cos(lat)=0 division) as no-panic; the
ENU transform itself is unchanged (no behavior change for valid inputs).

Tests: wifi-densepose-geo 9→15 lib (6 new), 8 integration unchanged. 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>

* test(pointcloud robustness): pin NaN-state-poisoning resistance + degenerate voxel fusion

Numerical-robustness audit of wifi-densepose-pointcloud. No bug found — the
crate is confirmed-robust against the proven NaN-state-poisoning class that bit
calibration/vitals. This adds regression pins documenting why:

1. csi_pipeline.rs — persistent auto-accumulating state (occupancy EMA,
   vitals) is provably self-healing. The UDP parser only emits finite
   amplitudes/phases (sqrt/atan2 of i8), and even an adversarial hand-built
   CsiFrame with NaN/inf amplitudes+phases cannot latch non-finite state:
   motion_score = (NaN/100).min(1.0) → 1.0; breathing path → 0 → clamp(5,40)
   → 5.0; tomography EMA uses only integer rssi. The new test injects 40
   poisoned frames and asserts occupancy/vitals stay finite AND the pipeline
   recovers to an in-range estimate afterward — so a future refactor that drops
   a `.min`/`.clamp` self-heal would fail this pin.

2. fusion.rs — fuse_clouds voxel averaging is div-by-zero-safe (per-voxel
   count >= 1 by construction). Pins empty / single-point / all-coincident
   inputs as no-panic with finite output.

No behavior change. Tests: wifi-densepose-pointcloud 18→22 (4 new), 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(geo/pointcloud robustness): CHANGELOG + ADR-154 sibling-crate sweep note

Record the wifi-densepose-geo + wifi-densepose-pointcloud numerical-robustness
audit under CHANGELOG [Unreleased] → Fixed, and a sibling-crate-extension note
on the ADR-154 horizon ledger (these crates are outside ADR-154's signal scope
but the sweep is the same ADR-154 class).

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-14 19:37:08 -04:00
rUv 9f80b66ae3
harden(cog-ha-matter crypto): domain-separate witness signing + verify_strict (signing chain otherwise sound — P2 crypto core verified) (#1080)
* fix(cog-ha-matter): domain-separate witness signing chain + verify_strict (ADR-116 §2.2)

Crypto review of the SHA-256 + Ed25519 witness chain that ADR-262 P2
reuses. The sibling wifi-densepose-engine bug class (unframed
concatenation of operator-influenceable strings into a signed digest)
is ABSENT here — canonical_bytes already length-prefixes kind/payload.
Two real hardening gaps fixed:

- CHM-WIT-01: add a versioned domain-separation tag
  (WITNESS_DOMAIN_TAG = b"cog-ha-matter/witness-event/v1\0") to
  canonical_bytes so the witness SHA-256 preimage / Ed25519 message
  cannot be replayed as a message for another signing context that
  shares key infrastructure (notably the manifest binary_signature).
  Completes the engine review's "domain-tag + length-prefix" rule.
  Witness bytes change by design (prior on-disk hashes/sigs invalidated);
  no in-repo crate consumes these bytes programmatically.

- CHM-WIT-02: verify_signature uses VerifyingKey::verify_strict (rejects
  non-canonical encodings + small-order keys) for the audit-uniqueness
  property. Key stays caller-pinned (not read from the event).

Pinned by fails-on-old tests: canonical_bytes_is_domain_separated,
canonical_bytes_starts_with_domain_tag_then_prev_hash,
witness_preimage_cannot_collide_with_a_bare_manifest_digest,
signature_commits_to_domain_tag_not_bare_fields; key-pinning guarded by
verify_uses_strict_path_and_pins_caller_key. cog-ha-matter 64 -> 68
tests, 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(cog-ha-matter): record ADR-116 crypto review findings (CHM-WIT-01/02)

CHANGELOG [Unreleased] Security entry + ADR-116 §4.1 review notes:
engine-class signed-digest collision confirmed ABSENT (length-prefixing
already correct), domain-separation tag added, verify_strict hardening,
and the clean dimensions (verify-before-trust, key-handling,
determinism, fail-closed parsing) with byte-layout evidence.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-14 19:04:09 -04:00
rUv 02cb84e0bb
fix(vitals safety): non-finite CSI frame permanently froze breathing+HR via IIR-state poisoning (self-heal) + noise-never-Valid pin (#1079)
* fix(vitals): self-heal IIR filters after non-finite CSI frame (ADR-021/ADR-158 §A1)

The 2nd-order resonator bandpass_filter in BreathingExtractor and
HeartRateExtractor latches each output y[n] into the filter state
(y1/y2). A single non-finite amplitude residual from a corrupt CSI
frame produced a NaN output that was written into the state. The
existing extract() is_finite() guard dropped that one sample from the
history buffer but never sanitized the poisoned filter state, so every
subsequent output stayed NaN, was rejected too, and the sliding-window
history never refilled: breathing AND heart-rate extraction went
silently dead (returning None forever) until reset().

On the vitals alert path this is a safety-relevant denial of service —
one bad frame stops monitoring with no error surfaced. Same class as the
calibration NaN bug (ADR-154 §3) and the firmware vitals fixes
(#998/#996/#987): prior hardening guarded the history boundary but not
the filter-state boundary.

Fix: when bandpass_filter computes a non-finite output it resets the IIR
state to default and returns 0.0, so the resonator recovers on the next
clean frame (the 0.0 is still dropped by the caller's finite-check, so no
spurious sample enters history).

Also de-magic the safety-critical HR physiological plausibility band into
named HR_PLAUSIBLE_MIN_BPM/HR_PLAUSIBLE_MAX_BPM consts (value-identical
40/180 BPM).

Pinned by:
- breathing::tests::nan_frame_does_not_permanently_poison_filter (FAILS pre-fix)
- breathing::tests::inf_mid_stream_does_not_freeze_history (FAILS pre-fix)
- heartrate::tests::nan_frame_does_not_permanently_poison_filter (FAILS pre-fix)
- heartrate::tests::pure_noise_is_never_reported_valid (fabricated-vital negative)
- heartrate::tests::plausibility_band_constants_pinned (de-magic value pin)

wifi-densepose-vitals --no-default-features: 55->60 lib tests, 0 failed.
Workspace green (3370 passed, 0 failed). Python proof unchanged (vitals
off the deterministic proof's signal path).

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(vitals): record IIR NaN/inf self-heal fix (ADR-021, CHANGELOG)

Document the wifi-densepose-vitals filter-state poisoning fix in ADR-021
Implementation Notes (parallel to the firmware #998/#996/#987 robustness
class) and add a CHANGELOG [Unreleased] Fixed entry. Notes the confirmed
clean dimensions with evidence (flat -> None; noise -> low-confidence
Unreliable, never Valid; harmonic-rich breathing -> not a confident false
HR; out-of-band BPM clamped).

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-14 18:01:47 -04:00
rUv ebfaee4437
fix(calibration): NaN-poisoning silently disabled presence specialist (Features::from_series unguarded) + de-magic (#1077)
* fix(calibration): drop non-finite samples in Features::from_series (ADR-151)

A single NaN/inf scalar sample (corrupt CSI frame) poisoned mean/variance
into NaN, which — baked into a persisted PresenceSpecialist::threshold —
silently disabled presence detection (every `f.variance > NaN` is false),
no error raised. extract.rs is the live-inference + training feature path,
yet (unlike geometry_embedding.rs) had no non-finite guard.

Fix at the production boundary: filter non-finite samples before computing
any statistic; an all-non-finite series degrades to Features::ZERO, same as
the empty series. Value-identical for all-finite input (full_loop + existing
extract tests unchanged). Pinned by two fails-on-old tests.

Co-Authored-By: claude-flow <ruv@ruv.net>

* refactor(calibration): de-magic specialist thresholds to named consts (ADR-151)

Promote the bare default min-score literals (breathing 0.25, heartbeat 0.3)
and the anomaly score scale / label cutoff (2.0× spread, > 0.5) to documented
named consts. Value-identical — pinned by characterization tests asserting the
consts equal the prior literals and the gate boundary (score >= floor).

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(calibration): record ADR-151 review — NaN fix + clean dimensions

CHANGELOG [Unreleased] Security entry and ADR-151 §6.1 review note for the
beyond-SOTA correctness+security review: NaN-poisoning fail-closed fix,
file/path (no I/O in crate), untrusted-load, receipt/hash (absent), and the
clean numerical paths — all with evidence.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-14 17:22:20 -04:00
rUv db3d94a313
fix(homecore-api security): auth-gate GET /api/ (was unauthenticated) + recover WS subscription on broadcast lag (#1076)
* fix(homecore-api security): auth-gate GET /api/ (HC-API-AUTH-01, ADR-161)

`rest::api_root` took no headers and unconditionally returned
`200 {"message":"API running."}`, while every sibling REST route gates
on `BearerAuth::from_headers`. HA's `APIStatusView` inherits
`requires_auth = True`, so `/api/` must return 401 for a missing/wrong
bearer — HA clients use it as a token-validation probe, so a 200 told a
bad-token client its token was valid and let an unauthenticated party
confirm a live endpoint. LOW severity (static body, no data leak),
reported at true severity.

Fix: `api_root(headers, State)` validates the bearer like `get_config`.

Pinned by fails-on-old tests (200 -> assert 401):
- api_root_rejects_missing_bearer
- api_root_rejects_wrong_bearer
guarded by api_root_accepts_correct_bearer (still 200 with valid token).

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix(homecore-api security): recover WS subscription on broadcast lag (HC-WS-LAG-01, ADR-161)

`subscribe_events`'s per-subscription task matched `Err(_) => break` on
both broadcast `recv()` arms. `RecvError::Lagged(n)` (a slow consumer
falling >EVENT_CHANNEL_CAPACITY=4,096 events behind) is recoverable —
the bus doc says "Lagged receivers must re-sync" and HA keeps the
subscription alive across a lag. The old code treated the first lag as
fatal, so after an event burst the client's stream went permanently
silent with no error frame — a self-inflicted event-delivery DoS under
load. LOW severity.

Fix: `Lagged(_) => continue` (skip dropped window, re-sync),
`Closed => break`, on both the system and domain arms.

Pinned by subscription_survives_broadcast_lag: subscribes, floods 6,000
filtered events past the 4,096 capacity to force a Lagged, then asserts
a subsequent subscribed event is still delivered (old code: 5s timeout).

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(homecore-api security): record HC-API-AUTH-01 + HC-WS-LAG-01 review (ADR-161)

CHANGELOG [Unreleased] Security entry + ADR-161 addendum documenting the
beyond-SOTA network-API review: two LOW bugs fixed (unauthenticated
GET /api/; WS subscription killed on broadcast lag) and the
auth/traversal/injection/info-leak/CORS dimensions confirmed clean with
evidence (no traversal surface — in-memory DashMap + EntityId allowlist;
HashSet token compare, not a byte-== timing oracle).

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-14 16:48:57 -04:00
rUv a369fbe66e
fix(bfld security): close HIGH privacy-bypass in process_to_frame (identity surface leaked despite restrictive class) + JSON-injection (#1075)
* fix(bfld): route process_to_frame payload through PrivacyGate (ADR-141 privacy bypass)

BfldPipeline::process_to_frame stamped the frame header with the active
privacy class but serialized the caller-supplied BfldPayload UNCHANGED via
BfldFrame::from_payload. This let a frame labeled Anonymous(2) or
Restricted(3) carry the full identity-leaky compressed_angle_matrix
(+ amplitude/phase proxies, csi_delta) that PrivacyGate::demote is documented
and tested (privacy_gate_demote.rs) to strip at exactly those classes.

A NetworkSink accepts class >= Derived(1), so such a frame would publish the
beamforming angle matrix — the identity surface — across the node boundary
despite its restrictive class byte. The class byte lied about payload content.

Fix: after building the frame at the active class, apply PrivacyGate::demote to
the same class. demote() strips sections by target-class threshold (independent
of any class transition), so a same-class demote performs no class change but
brings the payload into policy compliance. Research classes (Raw/Derived) keep
the full payload — demote is a no-op there.

Pinned by three fails-on-old tests in pipeline_to_frame.rs:
- process_to_frame_at_anonymous_strips_identity_leaky_sections (FAILED pre-fix)
- process_to_frame_in_privacy_mode_strips_amplitude_and_phase (FAILED pre-fix)
- process_to_frame_at_derived_preserves_full_payload (guards against over-strip)
The pre-existing round-trip test is updated to assert the gated payload.

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix(bfld): JSON-escape zone_id in MQTT state-topic payload

render_events emitted the zone_activity payload as format!("\"{zone}\"") with no
escaping, while ha_discovery.rs already escapes operator-controlled strings via
push_str_field. A zone name containing a double-quote or backslash therefore
produced malformed / injectable JSON on the state topic that Home Assistant
parses (e.g. zone `a"b` -> payload `"a"b"`).

Fix: add json_string_literal() mirroring ha_discovery's escaping (", \, \n, \r,
\t, control chars) and use it for the zone payload. Value-identical for normal
zone names (living_room etc.).

Pinned by zone_payload_escapes_json_metacharacters (FAILED pre-fix); the
existing zone_payload_is_json_string_with_quotes still passes unchanged.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr-141): record bfld privacy+security review findings + CHANGELOG

Document the two fixed bugs (process_to_frame privacy-bypass; zone_id JSON
injection) and the dimensions confirmed clean (event-field gating, witness/hash
framing, fail-closed) in ADR-141, plus CHANGELOG [Unreleased] Security/Fixed
entries.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-14 16:15:42 -04:00
rUv d2089c342a
fix(engine security): close witness domain-separation collision in governed-trust cycle + prove privacy monotonicity (#1074)
* fix(engine): length-prefix witness fields to close domain-separation collision

The BLAKE3 trust witness concatenated model_version, calibration_version,
and privacy_decision boundary-to-boundary, with the variable-length evidence
list lacking an explicit count. A string straddling a field boundary (e.g. a
per-room adapter id absorbing the leading bytes of the calibration epoch, or a
model_version absorbing a trailing evidence ref) collided with a different
trust decision — silently un-distinguishing two distinct privacy-relevant
inputs and defeating the ADR-137 tamper/drift audit guarantee. model_version
is operator-influenceable via the adapter id (ADR-150 §3.4), so the ambiguity
was reachable.

Fix: domain-tag the hash and length-prefix every field (8-byte LE length),
plus an explicit evidence count. Pinned by two fails-on-old tests:
witness_distinguishes_model_calibration_boundary and
witness_distinguishes_evidence_model_boundary.

Co-Authored-By: claude-flow <ruv@ruv.net>

* test(engine): pin privacy monotonicity, fail-closed boundaries; de-magic constants

Review hardening for the governed-trust cycle (no behavior change):

- forced_contradiction_never_relaxes_class: property test over all 5 privacy
  modes proving a forced contradiction only ever raises the emitted class byte
  (more restrictive) and a clean cycle emits exactly the base class — the
  ADR-141/120 information-only-removed invariant.
- empty_cycle_fails_closed: a zero-frame cycle errors (fusion NoFrames),
  emits no SemanticState, and does not advance the cycle counter.
- single_node_cycle_is_well_formed: characterizes the n=1 boundary (no mesh,
  no directional, base class, witness still emitted) — documents single-node
  sensing as a valid non-demoting mode, not a bypass.
- De-magicked the engine-construction literals (coherence accept gate, ADR-143
  SLAM discovery + static-anchor thresholds) into named documented consts,
  value-identical, pinned by engine_constants_match_prior_values.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(engine-review): record witness domain-separation fix + monotonicity clean bill

CHANGELOG [Unreleased] Security entry and review notes appended to ADR-137
(witness domain-separation fix) and ADR-141 (privacy monotonicity confirmed
clean over all 5 modes, fail-closed boundaries pinned).

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-14 15:32:24 -04:00
rUv 306d009e72
feat(rufield): rufield-viewer live-ingest mode (submodule bump) (#1072)
Bumps vendor/rufield to add --source live --upstream: the dashboard ingests
RuView's /ws/field events, verifies each ed25519 receipt on ingest (forged
events flagged, never fused), and renders real RuView FieldEvents through the
same display path. Honest SYNTHETIC/LIVE/DISCONNECTED banner, mutually
exclusive, never mislabeled (409 on /api/run in live mode). Closes the
RuView↔RuField visual loop (ADR-262 surfaces). 26 tests, 0 failed.

Co-authored-by: ruv <ruvnet@gmail.com>
2026-06-14 14:24:13 -04:00
rUv df617145d6
feat(ADR-262 P3): live /api/field + /ws/field — RuView sensing speaks RuField (fail-closed egress) (#1071)
* feat(ADR-262 P3): live RuField surface — RuView sensing speaks RuField on /api/field + /ws/field

Wire the P1 `wifi-densepose-rufield` bridge into the live
`wifi-densepose-sensing-server` so the governed sensing cycle emits real
signed RuField `FieldEvent`s on two additive endpoints.

- Cargo: add the `wifi-densepose-rufield` path dep (the single coupling
  point, ADR-262 §5.4 — no new RuView-internal coupling).
- New `src/rufield_surface.rs` (kept out of the 8k-line main.rs):
  `FieldSurface` holds a dedicated ed25519 `Signer` + a bounded ring of
  recent events + the `/ws/field` broadcast topic; `GET /api/field` and
  `GET /ws/field` handlers; a standalone `router()` for isolated testing.
- Signer (defers the P2 key decision, ADR-262 §8 Q1): a STANDALONE
  dev/sensing key from `WDP_RUFIELD_SIGNING_SEED`, else a deterministic
  dev default with a logged WARN. Reusing the `cog-ha-matter` Ed25519
  key is the deferred P2 call — P3 does not pre-empt it.
- Tap: at the ESP32 governed-trust cycle (`main.rs` ~5886 observe_cycle
  / ~5938 SensingUpdate build), `emit_rufield_event` joins the cycle's
  features/classification/signal_field with the engine's
  effective_class/demoted trust state into a `SensingSnapshot` and
  surfaces it via the bridge. Existing endpoints (`/ws/sensing` etc.)
  are unchanged — purely additive.
- Privacy egress: `network_egress_allowed` is fail-closed for an
  unattended live surface — only P1/P2 leave the box; P0 raw and
  P3/P4/P5 (identity/biometric/aggregate) are held edge-local. A
  `Derived` cycle maps to P4/P5 and never surfaces.
- No-phantom: `emit` drops no-presence cycles (no fabricated events).

Gates (tests/rufield_surface_test.rs, tower::oneshot, 4/0): well-formed
signed event (WifiCsi, P2 not P1, is_fusable, real timestamp); empty
cycle → no phantom; Derived trust never surfaces; mixed stream surfaces
only egress-safe events.

Honesty (ADR-262 §0/§6): real plumbing on a live endpoint, NOT accuracy.
Single-link CSI with its existing caveats (no validated room-coordinate
accuracy); dedicated dev signing key pending the P2 ownership decision;
no accuracy claim.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(ADR-262 P3): mark P1+P3 implemented; document /api/field + /ws/field; CHANGELOG

- ADR-262 Status → "P1 + P3 implemented"; add a P3 implementation-status
  block (tap site, endpoints, dedicated dev signer deferring the §8 Q1
  key decision, fail-closed egress, gates). Keep the honesty framing:
  real plumbing on a live endpoint, not accuracy.
- CHANGELOG [Unreleased]: add the ADR-262 P3 entry.
- user-guide: add `/api/field` to the REST table + a "RuField surface
  (ADR-262 P3)" section covering `/api/field` + `/ws/field`, the
  fail-closed P1/P2-only egress, the WDP_RUFIELD_SIGNING_SEED dev key,
  and the no-accuracy honesty note.

Co-Authored-By: claude-flow <ruv@ruv.net>

* ci: checkout submodules everywhere + Dockerfile copies vendor/rufield

Making wifi-densepose-rufield (ADR-262 bridge) a v2 workspace member means
EVERY cargo-on-workspace context must have the vendor/rufield submodule
present (cargo loads all member manifests). P1 only fixed the rust-tests
job; this adds `submodules: recursive` to all workflow checkouts that run
cargo (mqtt-integration was failing on the missing submodule manifest), and
makes Dockerfile.rust COPY vendor/rufield/ to /vendor/rufield (matches the
bridge's ../../../vendor/rufield path-dep under the collapsed Docker layout).
update-submodules.yml left alone (it manages submodules itself).

Co-Authored-By: claude-flow <ruv@ruv.net>

---------

Co-authored-by: ruv <ruvnet@gmail.com>
2026-06-14 13:55:41 -04:00
rUv f250149e94
feat(ADR-262 P1): wifi-densepose-rufield bridge — RuView sensing → signed RuField FieldEvents (fail-closed privacy map) (#1070)
* feat(rufield): ADR-262 P1 — wifi-densepose-rufield anti-corruption bridge

New v2 workspace member that converts RuView WiFi-CSI sensing output into
signed RuField FieldEvents. Path-deps the vendor/rufield submodule crates
(rufield-core/-provenance/-privacy/-fusion); single coupling point between
RuView and the standalone RuField MFS spec (ADR-262 §5.4).

- SensingSnapshot: owned primitives mirroring SensingUpdate + TrustedOutput
  (no dependency on wifi-densepose-sensing-server).
- snapshot_to_field_event(): builds a WifiCsi FieldTensor + Observation,
  derives a real position from the signal-field peak (never fabricated),
  real sha256 provenance + ed25519 signature (synthetic=false).
- map_privacy() (§3.3 crux): maps by information content, NEVER byte value —
  Derived (byte 1) → P4/P5, never P1; fail-closed demotion floor to P2.

P1 gates (tests/p1_gates.rs): round-trip serde, is_fusable verified receipt,
RuFieldFusion::ingest accept + infer runs, privacy-safety (Derived never P1),
full §3.3 table, fail-closed demotion, determinism, no-fabricated-position.
15 tests pass (5 unit + 9 integration + 1 doc), 0 failed.

Honesty: P1 plumbing (tested conversion + safe privacy mapping), NOT wired
into the live server (P3) and NOT an accuracy claim.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr-262): mark P1 implemented + CI submodules:recursive + CHANGELOG/CLAUDE

- ADR-262 Status → "Proposed — P1 implemented"; add §0.1 Implementation
  status (the bridge crate + the five P1 gates that pass; defers the
  provenance-carrier reuse, P3 live wiring, and P4 multi-modality).
- ci.yml: add `submodules: recursive` to the rust-tests checkout so the new
  crate's `vendor/rufield` path-deps resolve in CI (they fail otherwise even
  though the workspace build passes locally with the submodule present).
- CHANGELOG [Unreleased]: P1 bridge entry (kept alongside the upstream
  ADR-262 research entry).
- CLAUDE.md: crate table row for `wifi-densepose-rufield`.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-14 12:46:58 -04:00
rUv faca0530de
docs(adr-262): RuField↔RuView integration design (Proposed) (#1069)
Researched integration ADR: thin wifi-densepose-rufield bridge crate
(rvcsi pattern), live SensingServerAdapter emitting signed FieldEvents,
vertical fusion composition (ruvsense within-WiFi → rufield cross-modal),
and ONE canonical privacy/provenance model (RuView effective_class →
RuField P0-P5 at egress; reuse cog-ha-matter SHA-256+Ed25519 receipt).
Key finding: RuView has 2 privacy enums + 3 witness mechanisms; the
Derived(byte=1)<Anonymous(byte=2)-but-carries-identity trap means the
bridge must map by information content, not byte value. Plumbing
architecture, not accuracy (real-CSI is unlabeled replay today).

Co-authored-by: ruv <ruvnet@gmail.com>
2026-06-14 12:03:16 -04:00
rUv 6f6c867629
feat(rufield): CsiReplayAdapter — first real WiFi-CSI adapter (submodule bump) (#1068)
Bumps vendor/rufield to include CsiReplayAdapter: RuField now ingests real
captured WiFi CSI (.csi.jsonl) → FieldTensor → CSI-variance motion/presence
proxy → signed FieldEvents → fusion. Measured on 199 real frames: 182 fused
inferences (115 breathing, 67 person_present) from real signal. Replay-from-file,
unlabeled (proxy not validated accuracy) — live streaming + labeled accuracy
remain roadmap; mmWave/thermal stay synthetic.

Co-authored-by: ruv <ruvnet@gmail.com>
2026-06-14 11:45:50 -04:00
rUv 95a5ecc746
feat(rufield): rufield-viewer dashboard — completes ADR-260 §27.9 (#1067)
Bumps the vendor/rufield submodule to include the new rufield-viewer crate
(Axum + vanilla JS read-only dashboard streaming the deterministic
SyntheticSim→fusion camera-free room-intelligence demo: live room state,
P0–P5 privacy-badged event log, fusion graph, signed-receipt viewer, behind
a permanent SYNTHETIC banner). All ADR-260 §27 criteria 1–10 now PASS.
Read-only demo viewer, not device management (real-adapter milestone later).
rufield repo now 7 crates / 72 tests.

Co-authored-by: ruv <ruvnet@gmail.com>
2026-06-14 11:10:02 -04:00
rUv 1f05456588
feat(ADR-261 M2): multi-bit + large-N ANN scaling study — measured, no crossover (refutes M1 prediction) (#1066)
* feat(ADR-261): multi-bit (b∈{1,2,4}) quantized HNSW traversal + scaling harness

Generalize the SymphonyQG-style quantized-traversal HNSW from 1-bit Hamming to a
b-bit-per-dimension code (b ∈ {1,2,4}), mirroring ADR-156 §10's multi-bit RaBitQ
scheme (rotate via FHT Pass-2, uniform mid-rise scalar quantizer over [-3,3],
ranked by per-dim L1). b=1 is byte-for-byte the original construction (codes in
{0,1} ⇒ L1 == Hamming), pinned by one_bit_build_bits_matches_legacy_build.
Bytes/node scales linearly: 128-d → 16/32/64 B for b=1/2/4.

- hnsw_quantized.rs: QuantizedHnswIndex::build_bits(...,bits,...), bits()/
  bytes_per_node() accessors, code-L1 greedy+beam traversal. build(...) kept as
  the b=1 backward-compatible entry point. +4 tests (multi-bit recall regression,
  bits clamp, bytes/node, legacy parity).
- ann_measure.rs: build_indices_bits / build_quant_bits / run_scaling_study +
  best_float_op / best_quant_op; scaling_report (#[ignore], --release) and a
  CI-safe scaling_study_small_is_consistent.
- ann_bench.rs: 2-bit and 4-bit quant criterion benches over the shared graph.

ruvector lib 151 → 156 passed, 0 failed, 1 ignored (scaling_report).

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr-261): record M2 multi-bit scaling study — measured, no crossover (refutes M1 prediction)

Multi-bit (b∈{1,2,4}) quantized HNSW traversal + N∈{10k,100k,250k} scaling study,
measured on this box. No crossover at any (N,b): at 10k more bits help (ratio
0.19→0.48×, b≥2 reaches 0.90 recall) but quant stays slower than float HNSW at
equal recall; at 100k/250k quant recall collapses (b=4: 1.0→0.788→0.624, never
≥0.90) while float holds ≥0.92. The predicted large-N crossover moved the wrong
way. Published negative with the mechanism explained. ADR-261 §11.

Co-Authored-By: claude-flow <ruv@ruv.net>

---------

Co-authored-by: ruv <ruvnet@gmail.com>
2026-06-14 10:31:00 -04:00
rUv f756a8af49
feat(ADR-261): ruvector HNSW graph-ANN (25x measured vs linear) + honest SymphonyQG-direction refutation (#1063)
* feat(ruvector): real float HNSW + SymphonyQG-style quantized-traversal index (ADR-261)

Adds the graph-ANN index the ruvector retrieval path was missing (ADR-156
§5 #1 noted there was no HNSW baseline to measure SymphonyQG against).

- hnsw.rs: correct float HNSW (Malkov & Yashunin) — multi-layer NSW graph,
  ef_construction/ef_search, Algorithm-4 neighbour selection, seeded-
  deterministic level assignment (SplitMix64, reused from rotation.rs),
  L2 + cosine, brute-force ground truth, full degenerate-case guards.
  recall@10 correctness gate >=0.95 vs brute force (L2 + cosine).
- hnsw_quantized.rs: SymphonyQG-style variant — same graph, traversal scored
  by cheap 1-bit Hamming over the RaBitQ Pass-2 rotated sign code, final
  exact-float rerank.
- ann_measure.rs: shared deterministic planted-cluster fixture + recall/QPS
  measurement (ann_bench_report is the ADR source of truth).

Fixes an index-out-of-bounds bug the recall gate caught: insert wired
bidirectional edges before pushing the node's own link row. +20 tests,
ruvector lib 131->151, 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>

* bench(ruvector): criterion ann_bench for HNSW vs quantized vs linear (ADR-261)

Times the same shared ann_measure fixture/indices through criterion so the
bench and the report test can never measure different graphs.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr-261): graph-ANN index ADR with MEASURED HNSW vs quantized verdict

ADR-261 (Accepted): float HNSW ~25x QPS over linear scan at recall >=0.99
(the baseline ADR-156 said was missing). Honest negative: the 1-bit
quantized traversal is too coarse to beat float HNSW at equal recall at
N=10k (best recall 0.738, no >=0.90 equal-recall point) — the SymphonyQG
3.5-17x is NOT reproduced by our 1-bit construction; expected crossover at
large N + a multi-bit code. Caveat: our HNSW + our quant, not SymphonyQG's
system — direction tested, not a 1:1 reproduction.

ADR-156 §5 #1 + §8 backlog: CLAIMED -> MEASURED-direction-tested.
CHANGELOG [Unreleased] entry.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-14 02:33:32 -04:00
rUv 261ce80a72
feat(adr-260): RuField MFS spec + vendor/rufield submodule (#1061)
ADR-260 (Accepted — v0.1 reference stack): RuField, the open specification
for camera-free multimodal field sensing — one FieldEvent/FieldTensor/
FusionGraph/PrivacyClass/ProvenanceReceipt model above WiFi CSI/CIR/BFLD,
UWB, BLE Channel Sounding, mmWave radar, ultrasound, subsonic, infrared,
and quantum sensors.

Published standalone as github.com/ruvnet/rufield and vendored here as the
vendor/rufield submodule (the vendor/rvcsi pattern — not a v2/ workspace
member). v0.1 reference stack: 6 crates, 60 tests/0 failed, clippy-clean.
All benchmark metrics SYNTHETIC (simulator ground truth, no hardware).

Co-authored-by: ruv <ruvnet@gmail.com>
2026-06-14 01:17:11 -04:00
rUv 0c2b1c16cc
fix: ESP32 vitals over-count + presence flicker (#998/#996) + Observatory per-person position/motion (#1050) (#1060)
* fix(firmware): gate phantom persons + add presence hysteresis (#998, #996)

Two ESP32 edge-vitals logic bugs in edge_processing.c. Both are
robustness/logic fixes — NOT validated-accuracy claims. True count/PCK
vs labelled ground truth remains hardware/data-gated (COM9 ESP32-S3).

#998 — n_persons over-counted (reported 4 for one person):
update_multi_person_vitals() split top-K subcarriers into top_k_count/2
groups and marked EVERY group active, so one body's multipath always
read the full EDGE_MAX_PERSONS. Added two pure, host-testable helpers:
  - count_distinct_persons(): per-group energy gate
    (EDGE_PERSON_MIN_ENERGY_RATIO) + spatial dedup
    (EDGE_PERSON_MIN_SC_SEP) so weak/adjacent multipath groups don't
    count as separate bodies. Strongest group always counts (>=1).
  - person_count_debounce(): a gated count must hold
    EDGE_PERSON_PERSIST_FRAMES consecutive frames before it's emitted,
    so a single noisy frame can't promote a phantom.
The active flags now mark only the strongest stable_count groups.

#996 — presence flag flickered at ~50cm despite high presence_score:
the bare `score > threshold` compare chattered on a noisy score
(field-observed 2.6-26.7 frame-to-frame). Replaced with a Schmitt
trigger + clear-debounce (presence_flag_update): assert above
threshold, hold in the dead band down to threshold *
EDGE_PRESENCE_HYST_RATIO, clear only after EDGE_PRESENCE_CLEAR_FRAMES
consecutive sub-low frames. presence_score itself is unchanged and
still emitted for consumer-side thresholding.

All thresholds are named, documented constants in edge_processing.h.
Firmware builds clean for esp32s3 (idf.py build RC=0).

Co-Authored-By: claude-flow <ruv@ruv.net>

* test(firmware): host C99 tests for vitals count + presence logic (#998, #996)

test/test_vitals_count_presence.c pins the two fixes with deterministic
host-buildable tests (no ESP-IDF needed). 13 cases / 22 assertions, all
passing under gcc 13 -Wall -Wextra:

  #998 count gate: single strong signature + multipath -> count==1;
  two well-separated -> 2; two strong-but-adjacent -> 1 (dedup);
  no signal -> 0; three well-separated -> 3.
  #998 debounce: transient spike rejected; sustained change accepted;
  flapping count stays stable.
  #996 presence: dithering trace -> stable flag (no flicker); brief dips
  held by clear-debounce; genuine departure clears within hold window;
  dead-band holds state.

The named tuning constants are #include'd from the real
edge_processing.h so the test and firmware can never disagree on
thresholds. `make run_vitals` / `make host_tests` added; binaries
gitignored.

Hardware-gated caveat documented in the test header: these pin the
decision LOGIC; the exact energy/separation/hysteresis values that best
match a real room vs labelled occupancy remain on-device tuning.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs: record ESP32 vitals count/presence fixes (#998, #996)

CHANGELOG [Unreleased] Fixed: root cause + fix + named constants + test
+ explicit hardware/data-gated caveat for both bugs.

ADR-021 Implementation Notes: dated 2026-06 entry noting the edge-path
person-count + presence-flicker fixes are boolean/count emission-logic
fixes, not a validated-accuracy claim; thresholds pending on-device
calibration.

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix(sensing-server): emit real field-derived person position/motion to /ws/sensing (#1050)

The Observatory 3D figure never animated because the sensing_update WS
frame carried no per-person position/motion_score/pose — only image-space
keypoints. The FigurePool/PoseSystem (and demo-data.js's own contract)
animate each figure from persons[i].position (room-world), .motion_score
(0..100), and .pose; none were on the live stream.

Honest scope (Case 2): the pipeline has no calibrated per-person room
localizer or per-person skeletal pose. New field_localize module extracts
the strongest peak(s) from the real signal_field grid (subcarrier
variances x motion-band power) and maps the peak cell to Observatory world
coords with the exact _buildSignalField transform. motion_score is the
measured motion_band_power passed through; pose is set only from a real
aggregate posture estimate, else None (never a fabricated skeleton).
Empty/below-threshold field -> persons: [] (no phantom); present person
with no resolvable peak keeps position [0,0,0], not invented coords.

attach_field_positions runs after the tracker step at all five broadcast
sites. New position/motion_score/pose fields added to both PersonDetection
structs. No UI change needed — the Observatory already reads these fields.

Tests: field_localize peak/coordinate/empty/separation units +
observatory_persons_field_position_tests (known-peak -> emitted position,
empty-room -> no phantom, pose real-or-None, below-threshold honesty).
sensing-server bin 441->451, 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(changelog): record #1050 Observatory persons position/motion fix

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-14 00:31:30 -04:00
rUv 1d12e8831a
refactor(beyond-sota): ADR-155 M2 — host-verifiable §8 closeout (7 de-magic, 9 boundary tests, native-conv honest-null) (#1059)
* refactor(train): ADR-155 M2 §8 — de-magic train non-tch tuning constants + boundary tests

Lift bare numeric literals used as thresholds / guard epsilons in the
non-tch (host-verifiable) train surface into named, documented consts and
pin each set with a *_consts_unchanged_from_literals test. Values are
bit-identical to the prior inline literals — cleanup, no behaviour change.

De-magicked (const + pin test):
- metrics_core.rs: VISIBILITY_THRESHOLD (0.5), MIN_REFERENCE_EXTENT (1e-6),
  OKS_FALLBACK_SIGMA (0.07)
- ruview_metrics.rs: NUM_KEYPOINTS (17), VISIBILITY_THRESHOLD (0.5),
  PCK_THRESHOLD (0.2), MIN_BBOX_DIAG (1e-3), MIN_DURATION_MINUTES (1e-6)
- subcarrier.rs: SPARSE_BASIS_SIGMA (0.15), SPARSE_BASIS_THRESHOLD (1e-4),
  SPARSE_REGULARIZATION_LAMBDA (0.1), SPARSE_COO_PRUNE_EPS (1e-8),
  SPARSE_SOLVER_TOL (1e-5 f64), SPARSE_SOLVER_MAX_ITERS (500)
- eval.rs: MIN_POSITIVE_MPJPE (1e-10)
- domain.rs: LAYER_NORM_EPS (1e-5)
- virtual_aug.rs: BOX_MULLER_U1_FLOOR (1e-10), MIN_ROOM_SCALE (1e-10)

Boundary / characterization tests (pin CURRENT behaviour):
- visibility_threshold_boundary_is_inclusive (>= 0.5 at the edge)
- degenerate_extent_below_floor_is_unscoreable ((0,0,0.0)/0.0, not perfect)
- tracking_zero_duration_does_not_divide_by_zero
- oks_short_array_is_bounded_at_keypoint_count (16 rows, no panic)
- compute_interp_weights_single_target_is_index_zero (target_sc==1)
- sparse_interp_single_target_is_finite
- domain_gap_infinite_when_in_domain_perfect_but_cross_nonzero
- domain_gap_unity_when_everything_perfect
- augment_frame_zero_room_scale_passes_amplitude_finite

Doc-only (no behaviour change):
- rapid_adapt.rs: correct module-doc O(eps) -> O(eps^2) for central differences
- geometry.rs: add # Panics to DeepSets::encode (documents existing assert!)

train --no-default-features: 191 lib (was 176), 303 total (was 288), 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(nn): ADR-155 M2 §3 — pure-Rust LinearHead::try_new input guard + de-magic softplus threshold

ADR-155 §3 found rf_encoder.rs has no adversarial checkpoint-deserialization
assert — its assert_eq!s in LinearHead::new are construction-time API contracts
on programmer-supplied vectors. This adds the honest, in-scope improvement the
M2 task allows: a pure-Rust *fallible* constructor so weights from an untrusted /
deserialized checkpoint can be shape-validated without panicking.

- Add RfHeadError (WeightShape / BiasShape / VarWeightShape) + Display + Error.
- Add LinearHead::try_new returning Result<Self, RfHeadError>; on success the
  head is byte-identical to LinearHead::new. new() is unchanged (still asserts;
  now documents # Panics and points to try_new) — no behaviour change for
  existing callers.
- De-magic softplus's bare 20.0 overflow threshold into
  SOFTPLUS_LINEAR_THRESHOLD (value unchanged) + pin test.

Tests: try_new_accepts_valid_and_rejects_each_bad_shape (valid == new forward;
each bad shape → typed error), softplus_threshold_unchanged_from_literal.

nn --no-default-features lib: 37 passed (was 35), 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>

* perf(nn): ADR-155 M2 §4 — native-conv bench-first → MEASURED-INCONCLUSIVE (no perf change shipped)

The §8 "native-conv naive-loop rewrite" backlog item: DensePoseHead::
apply_conv_layer is a pure-Rust 6-nested-loop conv (benchable on this host, not
tch/ort-gated). Bench-first per the §0 PROOF discipline.

- Add committed criterion bench benches/native_conv_bench.rs measuring forward()
  through the naive conv on representative single-layer configs (--no-default-
  features; no ort download).
- Prototyped a bit-identical range-clamped variant (hoist the per-tap in-bounds
  branch by pre-clamping kh/kw ranges; same ic→kh→kw MAC order ⇒ bit-identical).
  MEASURED before/after on this host: ~35% faster on padding-heavy small-channel
  maps (4.40→2.84 ms) but a ~3% *regression* on channel-heavy maps (11.09→11.48
  ms), all inside a ±20% run-to-run noise floor. Verdict: INCONCLUSIVE — the
  benefit is not robustly positive, so the rewrite is NOT shipped and NOT a
  fabricated speedup. Reverted to the naive loop; honestly deferred (ADR-155 §8).
- Add native_conv_matches_reference: a hand-computed characterization anchor
  (1×1 = scalar MAC; same-padded 3×3 ones = truncated-window sums 9/6/4) pinning
  CURRENT conv behaviour for any future rewrite.

nn --no-default-features lib: 38 passed (was 37), 0 failed. No behaviour change.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr-155): M2 §8.2 — enumerated host-verifiable P3 backlog clearance + CHANGELOG

Replace the §8 bulk "~40 lower-severity findings" line with the real, enumerated
M2 resolution (§8.2): 7 de-magicked (const + pin == prior literal), 9 boundary
tests, 1 input guard (rf_encoder try_new), 2 doc-only, 1 perf bench-first
MEASURED-INCONCLUSIVE (not shipped). Mark native-conv + rf_encoder RESOLVED;
state which §8 items stay data-gated (GraphPose-Fi/INT4/CSI-JEPA) or tch-gated
(proof/trainer/model panic sites, metrics *_v2 dead code) and ONNX read-lock
upstream-gated — blocked, not dropped. Declare the non-tch-verifiable subset of
§8 cleared.

Validation: train --no-default-features 303 passed (was 288); nn lib 38 (was 35);
workspace --no-default-features 3,293 passed, 0 failed; Python proof VERDICT PASS,
hash f8e76f21…46f7a UNCHANGED bit-exact.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-14 00:07:56 -04:00
rUv 8c24b8bdfe
refactor(beyond-sota): ADR-154 M3 — clear §7.4 P3 backlog (22 de-magic + 6 boundary tests, backlog 36→0) (#1057)
* refactor(signal): de-magic motion.rs tuning constants (ADR-154 §7.4 #18)

Lift the bare fusion weights, normalization scales, confidence-indicator
weights, and adaptive-threshold clamp bounds in motion.rs out of the
scoring functions into named, documented EMPIRICAL-DEFAULT consts. Values
are bit-identical to the prior literals — this is cleanup, no behaviour
change.

Adds boundary/characterization tests pinning current behaviour:
- motion_tuning_consts_unchanged_from_literals (consts == old literals)
- doppler_component_saturates_at_full_scale (/100 then clamp(0,1))
- correlation_score_zero_below_n2_boundary (n<2 guard)
- temporal_variance_zero_below_two_history (len<2 guard)
- adaptive_threshold_engages_at_history_boundary (history 9 vs 10)

Co-Authored-By: claude-flow <ruv@ruv.net>

* refactor(signal): gesture.rs euclidean length guard + de-magic (ADR-154 §7.4 #12)

- Add a debug_assert! to euclidean_distance documenting the same-dimension
  caller contract: zip() silently truncates on a length mismatch, so a
  mismatch is now loud in debug builds while the release operating path and
  output are unchanged.
- De-magic the bare 1e-10 confidence epsilon into a documented const
  CONFIDENCE_SECOND_BEST_EPSILON (value unchanged).

Tests pinning current behaviour:
- confidence_epsilon_unchanged_from_literal
- dtw_empty_sequence_is_infinite (n=0/m=0 boundary)
- euclidean_distance_equal_length_is_l2 (same-dim contract)

Co-Authored-By: claude-flow <ruv@ruv.net>

* refactor(signal): de-magic longitudinal.rs drift thresholds (ADR-154 §7.4)

Lift the bare drift-detection literals (7-day baseline, 2-sigma z-score,
3-day sustained, 7-day escalation, EMA alpha, cosine epsilon) into named,
documented EMPIRICAL-DEFAULT consts encoding the module's Key Invariants.
The duplicated `>= 7` in is_ready/is_ready_at now share one const. EMA alpha
kept as the exact 0.05 literal (1.0 - 0.95_f32 is not bit-identical in f32).
Values unchanged.

Tests:
- drift_consts_unchanged_from_literals
- is_ready_at_day_boundary (day 6 vs 7)
- cosine_similarity_zero_vector_is_zero (zero-norm guard)

Co-Authored-By: claude-flow <ruv@ruv.net>

* refactor(signal): de-magic division/zero-norm epsilons + boundary tests (ADR-154 §7.4)

De-magic the bare division-guard epsilons in four modules into named,
documented consts (values unchanged) and pin the previously-untested
zero-norm / zero-variance / degenerate boundaries:

- cross_room.rs: COSINE_SIMILARITY_EPSILON (1e-9) + test_cosine_similarity_zero_vector
- multiband.rs: PEARSON_DENOMINATOR_EPSILON (1e-12) + pearson_correlation_zero_variance
- intention.rs: LEAD_TIME_MIN_ACCEL (1e-10) + lead_time_zero_for_static_stream
- hampel.rs: ZERO_MAD_EPSILON (1e-15) + test_zero_half_window_error
  + test_zero_mad_constant_window; documented hampel_filter # Errors

Each module also gets a *_unchanged_from_literal const-pin test.

Co-Authored-By: claude-flow <ruv@ruv.net>

* refactor(signal): de-magic rf_slam + attractor_drift constants (ADR-154 §7.4)

rf_slam.rs:
- NS_PER_DAY (86_400_000_000_000.0), MIGRATION_MIN_SPAN_DAYS (1e-9), and the
  fixed-map defaults (FIXED_MAP_ASSOC_RADIUS_M/MIN_SIGHTINGS/MIN_COHERENCE)
  lifted out of inline literals (values unchanged).
- migration_zero_span_is_zero_rate pins the single-sighting zero-span guard.

attractor_drift.rs:
- METRIC_BUFFER_CAPACITY (365), STABLE_CENTER_WINDOW (10) de-magicked.
- Documented the implicit recent.len()>=1 divide-safety in the PointAttractor
  branch (guaranteed by the count < min_observations guard).
- analyze_min_observations_boundary pins the off-by-one boundary.

Each module gets a *_consts_unchanged_from_literals pin test.

Co-Authored-By: claude-flow <ruv@ruv.net>

* refactor(signal): de-magic coherence.rs variance floor + default decay (ADR-154 §7.4)

Completes the M1 #9 de-magic for coherence.rs: the four bare 1e-6 variance-floor
literals (update_reference floor + coherence_score/per_subcarrier_zscores epsilon)
collapse to one VARIANCE_FLOOR const, and the inline 0.95 default decay becomes
DEFAULT_EMA_DECAY. Values unchanged.

Tests:
- drift_consts_unchanged_from_literals extended (VARIANCE_FLOOR, DEFAULT_EMA_DECAY)
- coherence_score_finite_with_zero_variance pins the floor's effect

Co-Authored-By: claude-flow <ruv@ruv.net>

* refactor(signal): de-magic calibration.rs thresholds + min-frames default (ADR-154 §7.4 #2)

Lift the bare calibration literals into named EMPIRICAL-DEFAULT consts (values
unchanged, bit-identical; calibration is off the Python proof path):
- DEFAULT_MIN_FRAMES (600) — was repeated across all four tier constructors
- AMP_STD_FLOOR (1e-12) z-score divisor floor
- MOTION_AMP_Z_THRESHOLD (2.0) / MOTION_PHASE_DRIFT_THRESHOLD (π/6) — the two
  motion_flagged sites now share one definition
- SUBTRACT_MIN_NORM (1e-30) baseline-subtraction guard

Test calibration_consts_unchanged_from_literals pins all five and asserts every
tier constructor shares DEFAULT_MIN_FRAMES.

Co-Authored-By: claude-flow <ruv@ruv.net>

* refactor(signal): de-magic fusion_quality + temporal_gesture constants (ADR-154 §7.4)

fusion_quality.rs:
- CONTRADICTION_PENALTY (0.8) and CONTRADICTION_BOUND_HALFWIDTH (0.1) named.
- no_contradiction_is_identity pins the n=0 boundary (penalty 0.8^0 = 1.0,
  zero-width bounds).

temporal_gesture.rs:
- CONFIDENCE_SECOND_BEST_EPSILON (1e-10, mirrors gesture.rs) and
  NORM_QUANTIZATION_SCALE (1000.0) named.

Each module gets a *_consts_unchanged_from_literals pin test. Values unchanged.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr-154): record Milestone-3 — §7.4 row #21-45 P3 backlog cleared

Replace the lumped #21-45 backlog row with the enumerated M3 resolution: 22
magic constants de-magicked into named EMPIRICAL-DEFAULT consts (each pinned ==
prior literal), 6 boundary/characterization tests, ~4 doc-only, across 11
modules; not-real findings reported + skipped (unreachable attractor_drift
div0, non-existent gesture thresholds, proof-path features.rs). Update residual
P3 rows #2/#12/#17/#18 to RESOLVED, the deferred count (36 -> 0), the scope
field, and the Horizon-ledger one-liner. §7.4 backlog fully cleared across
M0-M3. CHANGELOG [Unreleased] entry added.

Validation: signal lib --no-default-features 476/0/1; --features cir 476/0;
workspace 3,275/0; Python proof PASS, hash f8e76f21...46f7a UNCHANGED.

Co-Authored-By: claude-flow <ruv@ruv.net>

---------

Co-authored-by: ruv <ruvnet@gmail.com>
2026-06-13 19:36:05 -04:00
rUv 91248536bc
feat(beyond-sota): ADR-156 M2 — RaBitQ unbiased distance estimator (rigorous published negative on strict-K) (#1056)
* feat(ruvector): RaBitQ unbiased distance estimator (ADR-156 M2)

Implement the real Gao & Long (SIGMOD 2024) RaBitQ contribution on top of
the existing Pass-2 rotation: an unbiased estimator of the inner product /
squared distance recovered from the 1-bit code plus 8 B/vec per-vector side
info (residual_norm + x_dot_o), used to rerank the candidate set instead of
raw Hamming.

- src/estimator.rs (new): EstimatorSketch, SideInfo, EstimatorQuery,
  DistanceEstimator (estimate_inner_product / estimate_sq_distance /
  ranking_key / cosine_ranking_key), EstimatorBank (topk_estimated[_cosine],
  with_centroid). Zero-centroid simplification documented; paper-faithful
  centroid path also built.
- src/rotation.rs: extract apply_padded() (full padded FHT frame the code
  lives in); apply() now truncates apply_padded(). No behaviour change.
- lib.rs: export estimator types.

Additive + backward-compatible: Pass-1 Sketch / Pass-2 SketchBank / WireSketch
wire format unchanged; all external callers use Pass-1 and are unaffected.

Co-Authored-By: claude-flow <ruv@ruv.net>

* test(ruvector): estimator strict-K coverage harness (ADR-156 M2)

Add measure_estimator (cosine rerank) + measure_estimator_euclidean to the
coverage harness, on the BIT-IDENTICAL fixture / cluster centres / query
stream / cosine ground truth as measure_pass1/measure_pass2 — apples-to-apples
sign-Hamming vs unbiased-estimator-rerank.

Regression tests:
- estimator_rerank_not_worse_than_sign (>= sign-only Pass-2 on a fixed fixture)
- estimator_coverage_is_deterministic
- estimator_coverage_report (--nocapture prints the strict-K table)

MEASURED strict-K (candidate_k=K=8): Pass-1 36.13% -> Pass-2-sign 46.39% ->
estimator-cosine 49.71%. Still short of the ADR-084 90% strict bar; estimator
reaches 95.12% at candidate_k=24 (vs sign 91.60%). Published negative.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(ruvector): record RaBitQ estimator measured negative (ADR-156 §11, ADR-084)

- sketch_bench: estimator cosine/euclid columns in the coverage table.
- ADR-156 §11 (new): estimator formula + zero-centroid simplification stated
  honestly; strict-K coverage table; RESOLVED-NEGATIVE verdict (49.71% strict,
  short of 90%); pinning test names. §5 #2 + §10.5 updated.
- ADR-084 'Pass 2b' (new): estimator landed + measured strict-K vs the bar.
- CHANGELOG [Unreleased]: ADR-156 §11 Milestone-2 entry.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-13 18:24:40 -04:00
rUv 865f9dee77
perf(beyond-sota): ADR-154 M2 — FFT planner hoist (1.84x, bit-identical) + 3 honest perf nulls + boundary tests (#1055)
* perf(signal): hoist FFT planner across subcarriers (ADR-154 §7.4 #20)

compute_multi_subcarrier_spectrogram called compute_spectrogram once per
subcarrier, and each call built a fresh FftPlanner + re-planned the same
length-window_size FFT. Hoist the plan + window out of the per-subcarrier
loop via a new compute_spectrogram_with_plan core that takes a pre-planned
Arc<dyn Fft> and pre-built window. compute_spectrogram delegates to it
(unchanged behaviour); the multi-subcarrier path plans once and reuses.

MEASURED-HOT (dsp_perf_bench, this box): at 56 subcarriers, window 128,
fresh-planner-per-subcarrier 467.88 µs -> hoisted-plan 254.75 µs = 1.84x;
window 256: 627.27 µs -> 448.39 µs = 1.40x. Plan-forward cost alone is
~1.86 µs (w128), x56 subcarriers ~= the removed delta.

Output is bit-identical: multi_subcarrier_hoisted_plan_bit_identical
compares f64::to_bits of every spectrogram value + freq/time resolution
against the per-call fresh-planner path across all 4 window functions x
{power,magnitude} on a 56-subcarrier matrix. The numeric STFT body is the
old loop verbatim; only plan/window construction is lifted.

Co-Authored-By: claude-flow <ruv@ruv.net>

* test(signal): boundary/tolerance tests for ADR-154 §7.4 #14 #16 #19

Three "+ test" backlog gaps closed — pure additions, no behaviour change
(phase_align refactor is internal: estimate_phase_offsets still returns the
identical offset vector; a counted core is split out only to observe the
iteration count).

#14 cir.rs fft_operator — fft_operator_within_tolerance_of_dense_canonical56:
  the opt-in FFT Φ/Φᴴ path changes the witness hash, so pin it numerically
  CLOSE to the dense path (not silently divergent). Asserts the full Cir
  output (every tap within 1e-2·dominant, dominant idx/ratio, active_tap_count,
  ranging_valid, rms_delay_spread) on the production canonical-56 config
  across τ ∈ {20,50,90} ns. Extends the existing HT20/single-τ test.

#16 phase_align.rs — refinement_terminates_at_iteration_cap_when_not_converging:
  forces non-convergence (tolerance=0.0, unreachable) and asserts the loop
  runs exactly max_iterations then returns — proving the cap, not convergence,
  bounds the loop (no infinite spin). Companion
  refinement_converges_before_cap_on_easy_input proves the cap is an upper
  bound, not the only exit.

#19 csi_ratio.rs — ratio_finite_at_and_below_1e_12_epsilon: the module
  implements the CSI ratio as the conjugate product H_i·conj(H_j) (no
  division), so it is finite even at/below the 1e-12 magnitude boundary a
  naive H_i/H_j division would need an epsilon to guard. Pins finiteness +
  bit-exact conjugate product at the boundary (zero target → zero, never
  inf/NaN), through the amplitude/phase extraction.

cargo test -p wifi-densepose-signal --no-default-features --lib: 447 passed,
0 failed; --features cir --lib: 447 passed, 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr-154): record Milestone-2 P2-perf verdicts + boundary tests (§7.4)

§7.4: #20 MEASURED-HOT (1.40–1.84× spectrogram FFT-plan hoist, bit-identical);
#5/#6/#7 MEASURED-NULL (benched, not hot, left as-is — sub-µs / stack-only /
alloc-once); #8 MEASUREMENT-ONLY (per-call 56×56 eigh cost; eigenvalue/BLAS
backend un-buildable on this Windows host, number deferred to a BLAS box, NOT
fabricated; also corrects the finding — extract_perturbation reuses cached
modes, the recompute is in estimate_occupancy). #14/#16/#19 RESOLVED (tolerance
/ convergence-cap / epsilon-boundary tests). Updated §7.4 intro + Horizon-ledger
(deferred count 41→36). CHANGELOG [Unreleased] entry added.

Co-Authored-By: claude-flow <ruv@ruv.net>

* bench(signal): committed P2 bench-first benches (ADR-154 §7.4 #5/#6/#7/#8/#20)

New dsp_perf_bench.rs backs every Milestone-2 perf verdict with a committed
criterion bench — no speedup claimed without a before/after number here, and
a benched NULL is the proof a micro-opt was unnecessary (the §5.x "already
amortized" pattern). Registered in Cargo.toml [[bench]].

MEASURED (this box, criterion medians):
  #20 spectrogram_multi_subcarrier (fresh vs hoisted plan):
      MEASURED-HOT — 467.88→254.75 µs (1.84x) @ sc56/w128; 627.27→448.39 µs
      (1.40x) @ sc56/w256. Optimized in the prior commit.
  #5 multistatic_attention/weights: MEASURED-NULL — 181 ns (2 nodes) ..
      848 ns (8 nodes); sub-µs, no hot-path alloc — left as-is.
  #6 tomography_reconstruct/solve: MEASURED-NULL — 47.5 µs (16 links) /
      60.4 µs (32 links) for a full 50-iter ISTA solve; the 2 per-solve voxel
      buffers (~4 KB) are negligible vs O(iters·links·voxels) compute, and
      reconstruct(&self) reuses them across iterations already — left as-is.
  #7 pose_kalman_update/cycles: MEASURED-NULL — 150 ns (17 kpts) / 2.82 µs
      (170); the Kalman "gain matrices" are fixed-size STACK arrays
      ([[f32;3];6]), zero heap — nothing to reuse — left as-is.
  #8 field_model_occupancy (eigenvalue feature): MEASUREMENT-ONLY — quantifies
      the per-call n×n eigendecomposition cost; incremental SVD is a sized
      future project, not attempted (number recorded in ADR-154 §7.4).

Reproduce:
  cargo bench -p wifi-densepose-signal --no-default-features --bench dsp_perf_bench
  cargo bench -p wifi-densepose-signal --bench dsp_perf_bench  # adds #8

Cargo.lock: dev-dep (criterion/clap) graph + crate version bumps from the
build; no runtime-dependency change.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-13 17:34:37 -04:00
rUv cf2a85db66
feat(beyond-sota): ADR-157 M1 — constant-time HMAC compare + MEASURED 5.57x native wlanapi scan (#1054)
* fix(hardware): constant-time HMAC sync-beacon tag compare (ADR-157 §B4)

AuthenticatedBeacon::verify compared the 8-byte HMAC-SHA256 tag with
`self.hmac_tag == expected`, which short-circuits on the first differing
byte and leaks, via verification latency, how many leading bytes a forged
tag matched — a byte-by-byte tag-recovery oracle (~256·N trials vs 256^N).

Replace with a hand-rolled branch-free `constant_time_tag_eq`: XOR-accumulate
every byte difference into a single u8 with no early exit, compare to zero
once. `#[inline(never)]` + `core::hint::black_box(diff)` resist the optimizer
reintroducing a short-circuit or a non-constant-time memcmp; length mismatch
returns false without inspecting contents. No new dependency — ADR-157 had
deferred this only to avoid the `subtle` crate; a fixed 8-byte compare needs
none.

Test (hard gate): tag_compare_is_constant_time_shape — equal / first-differ /
last-differ / all-differ / length-mismatch + end-to-end verify() last-byte
tamper. Proven to fail on a last-byte-skipping constant-time bug. A coarse
timing smoke check (tag_compare_timing_invariance_smoke) is #[ignore]d to
avoid CI flakiness. Grade MEASURED (constant-time construction).

ADR-157 §8 §B4 → RESOLVED. wifi-densepose-hardware: 164 passed / 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(wifiscan): MEASURE native wlanapi.dll vs netsh throughput (ADR-157 §5 #4)

ADR-157 §5 #4 recorded the native wlanapi.dll multi-BSSID fast path as
"asserted but NOT implemented; live scanner is the ~2 Hz netsh shim". Audit
finding: that status is stale — wlanapi_native::scan_native already implements
the real WlanOpenHandle → WlanEnumInterfaces → WlanGetNetworkBssList →
WlanFreeMemory/WlanCloseHandle FFI (handle cleanup on all exits, length-bounded
buffer walks, #[cfg(windows)] with typed Unsupported off-Windows), and
WlanApiScanner::scan_instrumented already wires it native-first with a netsh
fallback. The missing piece was an honest MEASUREMENT.

Add benchmark_backend(backend, window): drives one specific backend over a
fixed wall-clock window so netsh is timed independently (the existing
benchmark() picks native-first and so never measures netsh on a box where
native works). Returns None for an unavailable native path (honest negative,
not a fabricated number).

MEASURED on this box (Intel Wi-Fi 7 BE201 320MHz, 2026-06-13), 10 s window:
  native 21.42 Hz vs netsh 3.84 Hz = 5.57× (mean 5.0 BSSIDs/scan each).
  native-only run: 18.0 Hz. 50/50 back-to-back native scans, no handle leak.
A real positive result — NOT a fabricated 10×. Achieved 21.4 Hz is in the
asserted >2 Hz regime, below the asserted 10–20 Hz upper bound.

Tests (live-WLAN, #[ignore] for CI, RUN here):
  measure_native_vs_netsh_throughput, native_scans_dont_leak_handles,
  measure_native_scan_rate. Non-ignored pin native_scan_runs_real_ffi_on_windows
  (pre-existing) stays green. wifi-densepose-wifiscan: 94 passed / 0 failed.

ADR-157 §5 #4 + §8 → MEASURED (was ACCEPTED-FUTURE / CLAIMED-unmeasured).

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-13 16:32:34 -04:00
rUv 9b07dff298
feat(beyond-sota): ADR-155 metric unification + ADR-156 RaBitQ Pass-2 (honest negative + latent topk bugfix) (#1053)
* refactor(train): hoist canonical PCK/OKS to un-gated metrics_core; fold test_metrics onto production (ADR-155 M1 §8)

ADR-155 §8 deferred item: test_metrics.rs reference kernels validated
production against their OWN reimplementation — a test that cannot catch a
canonical-impl bug (both could be wrong the same way).

- Extract canonical_torso_size / pck_canonical / oks_canonical / sigmas /
  bounding_box_diagonal into a new NON-tch-gated `metrics_core` module, so
  the single metric definition is reachable under
  `cargo test --no-default-features` (the `metrics` module is tch-gated).
  `metrics` re-exports every item → still exactly ONE implementation.
- Rewrite tests/test_metrics.rs to assert the PRODUCTION pck_canonical /
  oks_canonical equal hand-computed fixtures (not a reimplementation):
  canonical_pck_matches_hand_computed_fixture (corr=3/total=4/pck=0.75),
  hip↔hip normalizer pin, zero-visible⇒0.0, OKS perfect⇒1.0, fake-Gold pin.
- Keep an INDEPENDENT raw-threshold reference kernel only as a differential
  cross-check: test_kernel_agrees_with_canonical asserts it AGREES with
  canonical where torso==1.0 (genuine cross-check, not duplication).

Grade: MEASURED. test_metrics 10→12 tests, 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix(sensing-server): relabel divergent live PCK/OKS so they're never conflated with canonical (ADR-155 M1 §2.1/§8 Goal C)

Goal C named training_api.rs:804 (torso-HEIGHT PCK). Auditing it surfaced
TWO findings the ADR-155 §1 table missed:

1. training_api.rs is an ORPHAN file — not declared `mod` in lib.rs OR main.rs,
   so it does NOT compile into the crate. It does not drive the live server.
2. The REAL live `best_pck`/`best_oks` (main.rs training path → RVF metadata
   JSON read by model_manager.rs) come from trainer.rs:
   - `pck_at_threshold` = RAW-threshold PCK, NO torso normalization (the most
     divergent kind), printed/serialized as bare "PCK@0.2".
   - `oks_map` calls `oks_single(area=1.0)` = the EXACT fake-Gold pattern
     ADR-155 §2.1 claimed closed elsewhere — still live here, inflating best_oks.

Resolution = RELABEL (torso/raw math is load-bearing on different data; the
pub fns can't be renamed without breaking API; sensing-server has no train/
ndarray dep). Honest unify is a tracked §8 backlog item.

- training_api.rs: `compute_pck` → `compute_pck_torso_height` + divergence doc;
  val_pck/best_pck/val_oks struct fields documented as torso-HEIGHT proxies;
  logs say `pck_torso_h@0.2`. Test torso_pck_is_labelled_distinctly_from_canonical.
- trainer.rs (LIVE): `pck_at_threshold` documented raw-unnormalized; `oks_map`
  area=1.0 flagged fake-Gold; test pck_at_threshold_is_raw_unnormalized_not_canonical.
- main.rs: live print relabelled `pck_raw@0.2` / `oks_map(area=1.0 proxy)`.

No wire-format field renames (back-compat); no pub-API rename (no silent break).
Grade: MEASURED (relabel + divergence pinned). sensing-server 450→451 lib tests, 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr-155): mark §8 metric items RESOLVED + audit map + honest §1 under-count correction (M1b Goals A/D)

- §8.1: full PCK/OKS audit map (every def: file:line, basis, canonical/
  legacy/distinct), the two §8 items marked RESOLVED with resolution+why.
- Honest finding: §1's "seven divergent metrics" was an UNDER-count —
  sensing-server's LIVE trainer.rs has a raw-unnormalized PCK and an
  area=1.0 fake-Gold OKS the table omitted, and the file §8 named
  (training_api.rs) is orphaned dead code. §9 honest-limits updated.
- Goal D: metrics.rs *_v2 variants confirmed caller-less + deprecated;
  noted for future cleanup, NOT deleted (public API, tch-gated).
- CHANGELOG [Unreleased] Fixed entry.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(ruvector): RaBitQ Pass-2 randomized rotation + topk bugfix (ADR-156 §8)

Implements the deferred "Multi-bit / Extended RaBitQ Pass 2" backlog item
from ADR-156 §8: a deterministic randomized orthogonal rotation applied
before sign-quantization, the published RaBitQ construction (Gao & Long,
SIGMOD 2024).

Rotation construction: Fast Hadamard Transform + seeded ±1 sign flips
("HD" / randomized Hadamard), O(d log d) time and O(d) memory — a dense
d×d rotation is O(d²) and infeasible at the 65,535-d the wire format
provisions for. Pads to the next power of two; SplitMix64 seeds the sign
stream so index-time and query-time rotations are bit-identical.

API is additive and backward-compatible: Pass 1 (`from_embedding`) is
untouched; Pass 2 is opt-in via `Sketch::from_embedding_rotated` and
`SketchBank::with_rotation` (+ `insert_embedding` / `topk_embedding` /
`novelty_embedding` helpers that rotate consistently). Default behaviour
is unchanged.

While building the Pass-2 coverage harness, found and fixed a PRE-EXISTING
correctness bug in `SketchBank::topk`: the n>k heap path used
`BinaryHeap<Reverse<(d,id)>>` (a min-heap) but treated its peek as the
max, so it returned the k FARTHEST sketches as "nearest". The shipped unit
tests only exercised the n≤k fast path, so it went unnoticed. Fixed to a
plain max-heap; pinned by `topk_heap_path_returns_nearest` and
`tight_clusters_give_high_coverage_with_overfetch` (the latter measured
0.072 on the old code).

New tests (+17, 100→117 in the crate): rotation determinism/norm-preservation
(`rotation_is_deterministic_for_seed`, `rotation_preserves_norm`), Pass-2
shape-compatibility, `pass2_coverage_not_worse_than_pass1`, and a
deterministic coverage report.

MEASURED top-K coverage (anisotropic planted-cluster fixture, cosine ground
truth; dim=128 N=2048 K=8 64 clusters noise=0.35 128 queries):
  candidate_k=K=8 : Pass1 36.13% -> Pass2 46.39%  (both << 90% bar)
  candidate_k=24  : Pass1 83.89% -> Pass2 91.60%  (Pass2 clears 90%)
  candidate_k=32  : Pass1/Pass2 100%
Honest result: rotation consistently helps (+10pp at strict K), but neither
pass clears the ADR-084 90% bar at candidate_k==K on this distribution.
Pass 2 reaches 90% only with ~3x over-fetch (the ADR-084 "candidate set"
deployment pattern). Multi-bit Pass 3 evaluated separately.

Co-Authored-By: claude-flow <ruv@ruv.net>

* feat(ruvector): multi-bit Pass-3 experiment + ADR-156/084 measured results

Adds the multi-bit half of the ADR-156 §8 "Multi-bit / Extended RaBitQ"
item as a MEASURED experiment (coverage::measure_multibit): rotate, then
b-bit uniform scalar-quantize each coord, rank by L1 over codes — the
natural multi-bit generalization of hamming. Measures the bit/coverage
tradeoff the backlog item asked for.

MEASURED at the strict bar (candidate_k=K=8, anisotropic planted-cluster
fixture, cosine ground truth):
  Pass1 (1-bit, no rot)  36.13%   16 B/vec
  Pass2 (1-bit, rot)     46.39%   16 B/vec
  Pass3 (rot, 2-bit)     54.39%   32 B/vec
  Pass3 (rot, 3-bit)     66.70%   48 B/vec
  Pass3 (rot, 4-bit)     74.22%   64 B/vec
Honest: multi-bit monotonically helps but even 4-bit (4x memory) reaches
only 74% at the strict bar — neither rotation nor <=4-bit multi-bit clears
the strict-K 90% bar on this distribution. The bar is met via over-fetch
(Pass2 @ candidate_k=24). Tests: multibit_tradeoff_report,
multibit_1bit_matches_pass2_approx (+ sanity that 1-bit ~= Pass-2).

Docs:
- ADR-156 §8 item #2 marked RESOLVED-PARTIAL; §5 #2 grade CLAIMED ->
  MEASURED-on-our-hardware; new §10 with full measured tables, the topk
  bugfix disclosure, and graded deferred sub-items.
- ADR-084: "Pass 2" section answering the rotation open-question with
  measured numbers + the topk bug note.
- CHANGELOG [Unreleased]: Added (Pass-2 milestone) + Fixed (topk heap).

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-13 16:02:18 -04:00
rUv 42dcf49f4d
fix(adr): resolve duplicate ADR numbers + close ADR-080 security + ADR-154 M1 signal backlog (#1051)
* fix(signal): circular phase variance for ghost-tap guard (ADR-154 §7.4 #1)

`phase_variance` computed a LINEAR sample variance over phase angles that
wrap at ±π, so a tightly-clustered set straddling the branch cut reported
spuriously HIGH dispersion — false-tripping the `> TAU` ghost-tap guard on
real, tightly-clustered CIR taps.

Replace with Mardia's circular variance V = 1 − R̄, bounded [0,1] and
invariant to where the cluster sits on the circle. Re-derive the guard
against the bounded metric via a named const
`GHOST_TAP_CIRCULAR_VARIANCE_MAX` (the old TAU-scaled threshold is
meaningless on [0,1]).

Grade: metric fix MEASURED; threshold value DATA-GATED — a clean single-path
ramp also sweeps the circle, so V alone cannot separate clean from
unsanitized without labelled frames. Conservative default (0.99) errs toward
never false-rejecting, strictly more permissive at the wrap boundary than the
buggy linear guard.

Fails-on-old test: `phase_variance_circular_not_fooled_by_branch_cut` —
inlines the old linear variance to show it exceeds TAU on wrap-straddling
phases while circular V≈0 and the guard no longer trips. Plus
`phase_variance_circular_is_bounded_and_extremal` (V∈[0,1], V≈0 identical,
V≈1 uniform).

cargo test -p wifi-densepose-signal --no-default-features --features cir --lib
→ 432 passed, 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix(signal): pin Welford n=0/n=1 finiteness guard (ADR-154 §7.4 #10)

The shared `WelfordStats` (field_model.rs, used by longitudinal.rs and others)
relies on `count < 2` guards in `variance`/`sample_variance`/`std_dev`/
`z_score` to stay finite at the boundaries. The guards existed but the n=0
boundary was UNTESTED — exactly the §4 divide-by-(n−1) family the ADR groups
this with.

Add `welford_finite_at_n0_and_n1` asserting every statistic is finite and
returns the documented sentinel (0.0) at n=0 and n=1, plus load-bearing doc
comments on the two guards.

Fails-on-old proof: with the `sample_variance` guard removed, the test FAILS
with "attempt to subtract with overflow" at the `(self.count - 1)` underflow
(0usize − 1); `variance` would similarly yield 0.0/0.0 = NaN. The guard is
restored; the test pins it so a future regression is caught.

Grade: MEASURED (boundary finiteness is asserted; the guard is the §4-family
fix made testable).

cargo test -p wifi-densepose-signal --no-default-features --lib field_model
→ 22 passed, 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>

* refactor(signal): de-magic adversarial thresholds + boundary tests (ADR-154 §7.4 #13)

Lift the bare numeric literals buried in `check`/`check_consistency` into
named, documented module consts (FIELD_MODEL_GINI_VIOLATION=0.8,
ENERGY_RATIO_HIGH_VIOLATION=2.0, ENERGY_RATIO_LOW_VIOLATION=0.1,
CONSISTENCY_ACTIVE_FRACTION_OF_MEAN=0.1, SCORE_W_* weights). VALUES UNCHANGED —
each const equals the original literal; only names + pinning tests are new.

Grade: DATA-GATED. The operating values stay empirical (defensible values need
labelled spoofed/clean CSI — Wi-Spoof, §6.2/§7.3). The de-magicking +
characterization tests are MEASURED: `tuning_consts_unchanged_from_literals`,
`energy_ratio_high_boundary`, `energy_ratio_low_boundary`,
`field_model_gini_boundary`, `consistency_active_fraction_boundary` pin the
decision boundaries at/just-below/just-above each threshold, so a future
data-driven retune is a visible, tested change.

Fails-on-change proof: bumping ENERGY_RATIO_HIGH_VIOLATION 2.0→3.0 makes
`energy_ratio_high_boundary` FAIL (restored). Operating values explicitly
NOT changed.

cargo test -p wifi-densepose-signal --no-default-features --lib ruvsense::adversarial
→ 20 passed, 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>

* refactor(signal): de-magic coherence drift/gate thresholds (ADR-154 §7.4 #9)

Lift the bare detection literals in `coherence.rs::classify_drift`
(DRIFT_STABLE_SCORE=0.85, DRIFT_STEP_CHANGE_MAX_STALE=10) and the
`coherence_gate.rs` Default impl (DEFAULT_ACCEPT_THRESHOLD=0.85,
DEFAULT_REJECT_THRESHOLD=0.5, DEFAULT_MAX_STALE_FRAMES=200,
DEFAULT_PREDICT_ONLY_NOISE=3.0) into named, documented consts. VALUES
UNCHANGED. The gate already exposed these via GatePolicyConfig (config seam);
this names + pins the defaults.

Grade: DATA-GATED. Operating values stay empirical (defensible Z-score
thresholds need labelled stable/drifting coherence traces). De-magicking +
boundary tests are MEASURED: `classify_drift_stable_score_boundary`,
`classify_drift_stale_count_boundary` pin the at/just-below/just-above
decisions; `drift_consts_unchanged_from_literals` /
`gate_default_consts_unchanged_from_literals` pin the values. Operating values
explicitly NOT changed.

cargo test -p wifi-densepose-signal --no-default-features --lib ruvsense::coherence
→ 40 passed, 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr-154): mark §7.4 P1 backlog cleared — Milestone-1 (#1,#10 RESOLVED; #9,#13 DATA-GATED)

Update ADR-154 §7.4 backlog rows #1, #9, #10, #13 with commit refs + grades,
the §7.4 intro count (four P1 items cleared, ~41 P2/P3 remain), the
Horizon-ledger one-liner (Milestone-1 DONE), and the §8 honest-limits #1 line
(metric now correct; threshold still DATA-GATED). Add CHANGELOG [Unreleased]
entry.

Grades: #1 RESOLVED (MEASURED metric / DATA-GATED threshold), #10 RESOLVED
(MEASURED), #9 & #13 RESOLVED-PARTIAL (DATA-GATED — de-magicked + boundary
tested, operating values unchanged).

Validation: cargo test --workspace --no-default-features → 2057 passed, 0
failed; wifi-densepose-signal lib → 442 passed (no-default + --features cir);
python archive/v1/data/proof/verify.py → VERDICT: PASS, hash f8e76f21…46f7a
UNCHANGED (CIR ghost-tap guard is not on the deterministic proof path).

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix(sensing-server): stop leaking internal errors in HTTP responses (ADR-080 #2)

Six handlers in `main.rs` serialized the internal error `Display` straight
into the JSON response body, leaking server internals to any client (ADR-080
finding #2, CWE-209; reframed onto the Rust boundary by ADR-164 G11):

  - edge_registry_endpoint: a panicked spawn_blocking `JoinError`
    ("task … panicked") in a 500, and the raw upstream error in a 503
  - delete_model / delete_recording / start_recording: std::io::Error
    strings carrying OS detail / filesystem paths
  - calibration_start / calibration_stop: the FieldModel error chain

New `error_response` module: `internal_error` / `internal_error_json` /
`upstream_unavailable` log the full detail server-side only (tagged with a
correlation id) and return a generic body
(`{"error":"internal_error","correlation_id":…}`) — no `panicked`, no file
paths, no Debug chain. The correlation id lets an operator join a client
report to the exact server log line without ever shipping the detail.

Pinned by 5 error_response tests, incl. a leak-substring guard
(internal_error_body_does_not_leak_detail) verified to FAIL on the reverted
old body (returns the panic message / path / "os error"). The HOMECORE sweep
(ADR-161) covered homecore-server, not this crate.

Co-Authored-By: claude-flow <ruv@ruv.net>

* test(sensing-server): pin XFF-immunity + no-query-token (ADR-080 #1, #3)

Findings #1 (XFF-spoofing bypass) and #3 (JWT-in-URL, CWE-598) were logged
against the Python v1 API but are VERIFIED ABSENT on the current Rust
sensing-server, so they get regression tests rather than redundant fixes:

  - #1 XFF: there is no IP-based rate-limiter or IP-allowlist to bypass, and
    neither security middleware reads a forwarded header. Added
    bearer_auth::xff_header_never_affects_auth_decision (spoofed
    X-Forwarded-For never flips a 401<->200 decision) and
    host_validation::forwarded_headers_never_bypass_host_allowlist (spoofed
    X-Forwarded-Host: localhost never lets Host: evil.com past the allowlist).

  - #3 JWT-in-URL: require_bearer reads the token only from the Authorization
    header; WS handlers take no query token; the sole Query extractor
    (EdgeRegistryParams) is a non-secret refresh flag. Added
    bearer_auth::query_string_token_is_never_accepted — ?token= / ?access_token=
    in the URL never authenticates (stays 401) while the header path still 200s.
    Verified to FAIL when a query-token path is injected into require_bearer.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr-080): mark P0 security findings #1-#3 RESOLVED; close ADR-164 G11

- ADR-080: Status note + per-finding closure (#1 XFF and #3 JWT-in-URL
  verified absent + regression-pinned; #2 leaked errors fixed via the
  error_response module). Records the v1-vs-Rust boundary distinction
  explicitly: v1 paths remain archived; this closure governs the shipped
  Rust sensing-server.
- ADR-164: Gap Register G11 and the Open/Gated Backlog entry marked
  RESOLVED with the fix + branch reference.
- CHANGELOG: [Unreleased] -> ### Security entry covering all three findings.

Co-Authored-By: claude-flow <ruv@ruv.net>

* docs(adr): renumber 6 displaced ADRs to resolve duplicate-number collisions (ADR-164 G1)

Resolves the 5 duplicate ADR numbers (6 displaced files) flagged by ADR-164
Gap Register item G1. Canonical keeper per number = first file committed at
that number (date tie-broken by inbound cross-reference count / parent-appendix
relationship). Displaced files renumbered to the next free numbers (166-171):

  050 keeps provisioning-tool-enhancements (5 refs vs 1)
    -> ADR-166-quality-engineering-security-hardening
  052 keeps tauri-desktop-frontend (parent ADR)
    -> ADR-167-ddd-bounded-contexts (its appendix)
  147 keeps nvidia-cosmos/OccWorld (the actual ADR, has Status header)
    -> ADR-168-benchmark-proof (proof companion, no Status)
    -> ADR-169-adam-mode-light-theme (was untracked)
  148 keeps drone-swarm-control-system (committed #862)
    -> ADR-170-yoga-mode-pose-system (was untracked)
  149 keeps public-community-leaderboard-huggingface (committed 16:47 vs 17:38)
    -> ADR-171-swarm-benchmarking-evaluation-methodology

Updates in-file `# ADR-NNN` headers and intra-file self-references (yoga-modes

* docs(adr): repoint inbound cross-references to renumbered ADRs (166-171)

Follow-up to the ADR renumbering (ADR-164 G1). Updates every inbound reference
that pointed at a displaced ADR, disambiguating shared numbers by title/slug so
only references to the DISPLACED topic move and keeper references stay put.

ADR-168 (was 147 benchmark-proof): README, CHANGELOG, user-guide,
  proof-of-capabilities, research docs 00/03 — all path/label refs updated.
ADR-169 (was 147 adam-mode) / ADR-170 (was 148 yoga-mode): docs/adr/README index.
ADR-171 (was 149 swarm-benchmarking): all ruview-swarm eval code+docs
  (Cargo.toml, evals/, eval_swarm.rs, metrics/mod/report/runner.rs), research
  doc 03 (every §-ref matched ADR-171 sections, not AetherArena), 00-system-review,
  series README, CHANGELOG, and ADR-148's forward/"open issues" pointers.
ADR-166 (was 050 quality-engineering / security-hardening): disambiguated from the
  ADR-050 provisioning KEEPER by topic. The HMAC/secure_tdm, directory-traversal,
  bind-address, and OTA-PSK-auth references in code comments
  (wifi-densepose-hardware Cargo.toml + secure_tdm.rs, sensing-server main.rs) and
  in ADR-052-tauri / ADR-167 all describe the security-hardening ADR -> ADR-166.
ADR-167 (was 052 ddd-appendix): inbound appendix references.

Index/registry updates: docs/adr/README.md, gap-analysis/census.md (rows +
header count), gap-analysis/lens-findings.md (collision table marked RESOLVED),
and ADR-164 Gap Register G1 marked RESOLVED with the full renumber map.

Keeper references deliberately untouched: all ADR-147 OccWorld code, all ADR-148
drone-swarm code/docs, all ADR-149 AetherArena refs (incl. ADR-150's SSL/resampling
refs, which ADR-150 explicitly binds to the AetherArena benchmark), ADR-050
provisioning refs, ADR-052 tauri refs. The frozen GitHub blob URLs in
docs/adr/.issue-177-body.md (pinned to an old branch) are left as historical.

Comment-only code edits; no behavior change. wifi-densepose-hardware compiles
clean; the sensing-server build's sole blocker is the pre-existing upstream
midstreamer-temporal-compare@0.2.1 registry crate, unrelated to these edits.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-13 14:31:38 -04:00
rUv 74ecce3218
Merge pull request #1048 from ruvnet/fix/issues-1031-894-fusion-guard-model-load
fix: multistatic fusion guard for real TDM (#1031) + load published HF model via auto-detect/convert (#894)
2026-06-13 12:23:06 -04:00
ruv fd1430e46f test(engine): update contradiction_demotes_privacy for #1031 guard thresholds
The streaming-engine privacy-demotion test fed a 2 ms timestamp spread, which
demoted under the old 1 ms soft guard. #1031 raised the default soft guard to
20 ms (to accommodate the real TDM slot offset), so 2 ms now fuses cleanly with
no demotion. Bump the test spread to 25 ms (above the 20 ms soft guard, within
the 60 ms hard guard) so it still proves the ADR-137 -> ADR-141 demotion wiring.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-13 12:14:11 -04:00
ruv 107232c0be fix(sensing-server): load published HuggingFace model via RVF auto-detect+convert (#894)
ProgressiveLoader rejected the published ruvnet/wifi-densepose-pretrained model
with the opaque "invalid magic at offset 0: expected 0x52564653 (RVFS), got
0x77455735", then silently fell back to signal heuristics (the "10 persons for
1" garbage reporters saw). The HF repo ships model.safetensors,
model-q{2,4,8}.bin (magic 0x77455735 = "5WEw"), and model.rvf.jsonl -- none
carry the binary-RVF magic the loader wants.

- New model_format module: auto-detects RVFS / safetensors / HF-quant-bin /
  JSONL by magic+name; returns a typed actionable ModelLoadError (lists accepted
  formats + the one-command convert path, never the opaque magic); converts
  safetensors / model.rvf.jsonl -> RVF in-memory so the published full-precision
  model loads via --model.
- load_or_convert_model: native RVF first, else auto-detect+convert+load, else
  typed error. The silent heuristics fallback is now a loud, actionable message.
- --convert-model <in> --convert-out <out> CLI subcommand: one-command offline
  conversion, verifies the output loads before writing.
- #1031 env seam: WDP_TDM_SLOTS + WDP_TDM_SLOT_US derive the multistatic guard
  from a deployment TDM schedule (default 60 ms / 20 ms otherwise).

Honest scope: the converter wires the format/load path (safetensors F32 tensors
-> RVF weight segment, manifest written, Layer A/B/C succeed, weights
round-trip). It does NOT claim end-to-end pose accuracy -- the HF pose-decoder
architecture differs from this crate inference head (data-gated in #894).
Quantized .bin blobs are rejected with a typed error pointing at safetensors.

Tests (fail on the old opaque-magic path):
- model_format::safetensors_converts_and_loads
- model_format::hf_quant_classifies_to_actionable_error
- model_format::{jsonl_converts_and_loads, convert_to_rvf_dispatches_and_rejects_quant, ...}

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-13 12:05:05 -04:00
ruv 287885776b fix(signal): multistatic fusion guard too tight for real TDM hardware (#1031)
MultistaticConfig::default().guard_interval_us was 5_000 us (5 ms) with a
comment claiming "well within the 50 ms TDMA cycle". That is wrong: on an
N-slot TDM schedule node k transmits in slot k, so two nodes are separated by
the slot offset, not clock jitter. A real 2-node mesh (slots 0/1) measured an
18,194 us spread, so every real frame set exceeded the 5 ms guard and fuse()
silently fell back to per-node sum/dedup -- multistatic fusion never ran on
hardware.

- Raise default hard guard to 60 ms (full 50 ms TDMA cycle + 20% jitter
  headroom, derived from the slot model and documented in the field doc).
- Raise soft guard to 20 ms (just above the observed 18.2 ms 2-slot spread).
- Add MultistaticConfig::for_tdm_schedule(total_slots, slot_duration_us).
- Keep the honest per-node fallback for genuinely-mismatched frames.

Tests (fail on the old 5 ms default):
- fuse_real_tdm_spread_18194us_fuses_with_default_guard
- configurable_guard_rejects_too_large_spread
- for_tdm_schedule_invariants

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-13 12:04:47 -04:00
rUv 29e937ef52
Merge pull request #1044 from ruvnet/feat/edge-skills-synthetic-validation
feat(wasm-edge): unified EdgePipeline (all ~64 skills) + honest synthetic validation harness
2026-06-13 00:46:29 -04:00
ruv 41665d3de9 test(wasm-edge): synthetic-ground-truth validation harness for edge skills (ADR-160)
Plant signals with known answers, run the real detector, MEASURE detection
accuracy / precision / recall / rate-error — synthetic-ground-truth ONLY, not
field accuracy.

MEASURED-on-synthetic (12 tests, all green):
- vital_trend, exo_ghost_hunter(hidden breathing), occupancy, intrusion,
  exo_rain_detect, sig_optimal_transport: acc 1.000
- exo_time_crystal: 1.000 on periodic-vs-aperiodic (its sub-harmonic-vs-clean-
  period claim is NOT separable by autocorrelation — recorded honestly)
- sig_flash_attention: 8/8 peak localization; spt_spiking_tracker: 4/4 zone
  localization (sparse plant); sig_mincut_person_match: 0 id-swaps/40 frames
- lrn_dtw_gesture_learn: enrollment validated (replay-match reported, not asserted)
- sig_sparse_recovery: trigger validated; recovery accuracy reported NEGATIVE
  (-2.2% vs unrecovered baseline) — only its detect/trigger path is validated

DATA-GATED (listed, NOT faked): med_seizure/apnea/cardiac/respiratory/gait,
sec_weapon_detect, exo_emotion/happiness/dream_stage/gesture_language — each
needs real labelled clinical/affect/ASL/metal-object data; no number claimed.

benchmarks/edge-skills/RESULTS.md documents every result + reproduce command and
the explicit honesty boundary. ADR-160 deferred 'per-skill accuracy validation'
item updated to PARTIALLY MEASURED-on-synthetic + DATA-GATED.

Suite: 631 passed default / 669 medical, 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-13 00:33:51 -04:00
ruv c6eacb7ff8 feat(wasm-edge): unified EdgePipeline wiring all ~64 edge skills (ADR-160)
Register every runtime skill module behind one uniform EdgeSkill trait and
run them all per CSI frame, aggregating (skill, event_id, value) triples.

- src/pipeline_all.rs: CsiFrameView (borrowed per-frame inputs), EdgeSkill
  trait, EdgePipeline (Box<dyn> dispatch over all skills), SkillEvent/SkillInfo
  introspection. Host-only (std); the wasm no_std build keeps the flagship
  lib.rs pipeline.
- src/skill_registry.rs: per-skill adapters (fwd_skill! direct-forward +
  synth_skill! for non-tuple returns). No skill DSP changed — only call wiring.
  gesture/coherence/adversarial synthesize one event; sig_sparse_recovery gets
  an owned mutable amplitude scratch; timer skills driven once per frame.
- med_* tier registered only under --features medical-experimental (preserves
  the ADR-160 safety gate). Default tier = 59 skills; +medical = 64.
- tests/pipeline_all.rs: 4 tests — all skills run without panic over 300
  deterministic synthetic frames, every emitted id is declared by its skill,
  introspection well-formed, default tier excludes medical (59) / medical adds 5 (64).
- examples/run_all_skills.rs: runnable demo printing per-skill event totals.

Full suite: 619 passed default (615 M6 baseline + 4 new), 0 failed.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-13 00:20:29 -04:00
rUv 153bc0595b
Merge pull request #1043 from ruvnet/docs/adr-gap-remediation-1
docs(adr): Gap Register remediation — write phantom ADR-132/165, fix ADR-134 collision, correct statuses
2026-06-12 23:11:10 -04:00
ruv 8fd4ee917d docs(adr): mark ADR-164 Gap Register items resolved (G3, G5) + correct G2
Records the remediation done in this branch:
- G3 (homecore-recorder/migrate phantom ADRs) → RESOLVED: ADR-132 + ADR-165 written.
- G5 (10 streaming-engine Proposed-while-built) → RESOLVED: 136-145 flipped to
  "Accepted — partial", with the honest caveat that the notes describe building
  blocks built+tested, not live-path integration.
- G2 (missing Status headers) → corrected: ADR-134-CIR was mislabeled as missing
  (it has a Status row); the 2 genuine misses (147-benchmark-proof, 052-ddd) are
  both inside owner-gated duplicate-number collisions, so left untouched. Early
  ADRs using "| Status |" vs "| **Status** |" are different-format-but-present.
  Net: 0 status headers added.
- Updated Coverage-Gaps bullets for recorder/migrate.

Renumbering/dedup of the 6 collisions left owner-gated, as instructed.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-12 23:01:10 -04:00
ruv 5c5112db0e docs(adr): correct streaming-engine statuses 136-145 Proposed→Accepted — ADR-164 G5
All 10 streaming-engine ADRs (136-145) carried Status: Proposed while each has a
concrete commit-pinned "Built -- tested building block" Implementation-Status note
(136: 11f89727f; 137: 4fa3847ac; 138: fc7674bde; 139: 521a012d8; 140: 169a355bd;
141: 7d88eb84c; 142: 1f8e180d6; 143: 2d4f3dea5; 144: b10bc2e9a; 145: 0f336b7d3),
each with a test count.

Flipped each to "Accepted — partial (built + tested building block; integration
glue pending — see Implementation Status, commit <hash>)". Honest "partial", not
full Accepted: the notes themselves state the blocks are tested+compiling but
"mostly not yet on the live 20 Hz path". 143 (v2 dataset-gated) and 144 (no UWB
radio in fleet) carry their specific residual gates inline.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-12 23:00:54 -04:00
ruv e3696da8d8 docs(adr): write ADR-165 (HOMECORE-MIGRATE), repoint migrate 134→165 — ADR-164 G3
homecore-migrate cited "ADR-134 (HOMECORE-MIGRATE)", but on-disk ADR-134 is
"First-Class CIR Support" — a different decision. The migrate crate was governed
by a phantom identity (ADR-164 Gap G3).

- New ADR-165-homecore-migrate-from-home-assistant.md (next free number),
  reverse-documented from the shipped P1 scaffold: HA .storage reader, versioned
  format gate (unknown minor_version = hard error), per-artifact parsers, inspect
  CLI, structured errors. Status: Accepted — P1 scaffold (full conversion P2).
  Trust-boundary rationale for the untrusted .storage import is the centerpiece.
- Repointed every ADR-134 governing reference in v2/crates/homecore-migrate/
  (Cargo.toml, README.md, src/lib.rs, src/config_entries.rs,
  src/storage_format/mod.rs) → ADR-165. Left the ADR-132 (recorder-feature)
  refs intact. Explanatory renumber notes retained.
- On-disk ADR-134 (CIR) untouched. ADR-126 series-map registry row owner-gated.

Docs/comments only — cargo build -p homecore-migrate --no-default-features
still compiles.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-12 23:00:33 -04:00
ruv 9457d441b2 docs(adr): write missing ADR-132 (HOMECORE-RECORDER) — resolves ADR-164 G3
homecore-recorder cites "ADR-132" in Cargo.toml/README/lib.rs/schema.rs/
semantic.rs, but no ADR-132 file existed — the durable-state backbone was
ungoverned (ADR-164 Gap G3 / Coverage-Gaps Lens A).

Reverse-documented from the shipped, tested crate (not invented): SQLite
HA-compatible recorder schema v48 (P1, 14 tests), ruvector HNSW semantic
index (P2, feature-gated, 20 tests), hash-embedding honesty note, P3 real
embeddings planned. Status: Accepted (shipped). Filename matches the link
the crate README already pointed at. Documented retroactively; honest about
hash-embedding limits and unbenchmarked latency targets.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-12 23:00:15 -04:00
rUv 626b4b2e97
Merge pull request #1042 from ruvnet/docs/adr-164-gap-analysis
docs(adr): ADR-164 — ADR corpus gap analysis & remediation backlog (162 ADRs)
2026-06-12 22:47:21 -04:00
ruv 260fceefe9 docs(adr): ADR-164 corpus gap analysis + research notes (162 ADRs)
Parallel gap analysis of all 162 ADRs (14-agent workflow): status distribution,
prioritized Gap Register, supersession integrity, contradictions/retractions
(anti-slop centerpiece), coverage gaps, and the honestly-gated backlog.

Key findings: 6 duplicate ADR numbers + 3 missing Status headers (breaks the
index); shipped crates citing phantom governing ADRs (homecore-recorder->ADR-132
nonexistent, homecore-migrate->ADR-134 mis-identified); streaming-engine ADRs
136-145 marked Proposed but actually Built; open ADR-080 sensing-server security
findings never closed; ~64 proposed-only ADRs; pre-ADR-155 accuracy claims are
CLAIMED not MEASURED. Detail in docs/adr/gap-analysis/{census,lens-findings}.md.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-06-12 22:40:32 -04:00
rUv e063de5970
Merge pull request #1039 from ruvnet/release/patch-1009-1004
release: patch-bump signal/sensing-server/cli for #1009+#1004 fixes (+ first-publish calibration)
2026-06-12 17:09:29 -04:00