feat(adr-118/p1.9): PrivacyClass capability-helper truth tables (279/279 GREEN)

Iter 41. Pins the const-helper API (PrivacyClass::allows_network /
allows_matter) and proves it stays in sync with the Sink::MIN_CLASS
trait-level enforcement. Drift between these two APIs would be a
silent correctness bug — an operator checking allows_network() might
get a different answer than the actual NetworkSink::check_class()
runtime gate.

Added (in tests/privacy_class_capability.rs, no_std-compatible):
- 10 named tests, all green:

  allows_network_truth_table     (4 classes × bool)
  allows_matter_truth_table      (4 classes × bool)
  allows_matter_implies_allows_network
    Monotonicity: Matter is a strict subset of Network. Any class
    that allows Matter MUST allow Network. The reverse is not true
    (Derived is Network-eligible but not Matter-eligible).
  allows_network_strictly_excludes_raw
    Class 0 is the ONLY class that fails allows_network. Any future
    refactor that lets Raw cross a NetworkSink violates ADR-118 I1.
  allows_matter_strictly_requires_class_two_or_three
  local_sink_accepts_every_class_per_helper
    Cross-consistency: LocalSink::MIN_CLASS = Raw, accepts all.
  network_sink_consistency_matches_allows_network
    For every class, check_class<NetworkKind> agrees with allows_network().
  matter_sink_consistency_matches_allows_matter
    Same for Matter.
  as_u8_returns_documented_byte_values    (0, 1, 2, 3)
  class_byte_ordering_matches_information_density  (raw < derived < anon < restr)

Helper:
  check_consistency<S: Sink>(class, helper_says_allowed) compares the
  Boolean helper against (class_byte >= S::MIN_CLASS.as_u8()) and asserts
  equality. Catches drift before it reaches operator-visible behavior.

ADR-124 status (iter step 0 sibling check):
- docs/adr/ADR-124-rvagent-mcp-ruvector-npm-integration.md unchanged
  at 431 lines. SENSE-BRIDGE scope remains orthogonal.

ACs progressed:
- ADR-118 invariant I1 reinforced at the const-helper layer: a future
  PR refactoring PrivacyClass::Raw to be Network-eligible breaks 4 of
  the 10 tests (truth table + monotonicity + Raw exclusion + sink
  consistency), so the regression is loud rather than silent.
- ADR-120 §2.2 sink-class contract pinned at the helper layer. The
  iter 3 (Sink + check_class) and iter 1 (allows_network) APIs now
  have a regression test enforcing their agreement.

Test config:
- cargo test --no-default-features → 90 passed (+10 no_std-compat)
- cargo test                       → 279 passed (269 + 10)

Out of scope (next iter target):
- PR-readiness pivot remains the genuine next step: CHANGELOG batch,
  witness bundle regeneration, AC closeout table. All ADR-118/119/120/
  121/122 ACs are now empirically covered. External-resource-gated
  work (KIT BFId, Pi5/Nexmon hardware) stays skipped.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv 2026-05-24 19:18:11 -04:00
parent ce2eaab75a
commit a7ccac7869
1 changed files with 142 additions and 0 deletions

View File

@ -0,0 +1,142 @@
//! `PrivacyClass::allows_network` and `allows_matter` const-helper truth
//! tables, plus a cross-consistency check against the `Sink` trait constants.
//! Iter 1 introduced these helpers; iter 3 introduced the `Sink::MIN_CLASS`
//! mechanism. The two APIs must agree.
//!
//! Why both APIs: `allows_network` / `allows_matter` are point-in-time
//! Boolean queries for ergonomics ("can I publish this frame?"); the `Sink`
//! marker-trait + `MIN_CLASS` const provides the structural enforcement at
//! compile-time. Drift between them is a silent correctness bug — this iter
//! pins the constraint that they always agree.
use wifi_densepose_bfld::sink::{LocalKind, MatterKind, NetworkKind, Sink};
use wifi_densepose_bfld::PrivacyClass;
const ALL_CLASSES: [PrivacyClass; 4] = [
PrivacyClass::Raw,
PrivacyClass::Derived,
PrivacyClass::Anonymous,
PrivacyClass::Restricted,
];
// --- direct truth tables ------------------------------------------------
#[test]
fn allows_network_truth_table() {
assert!(!PrivacyClass::Raw.allows_network());
assert!(PrivacyClass::Derived.allows_network());
assert!(PrivacyClass::Anonymous.allows_network());
assert!(PrivacyClass::Restricted.allows_network());
}
#[test]
fn allows_matter_truth_table() {
assert!(!PrivacyClass::Raw.allows_matter());
assert!(!PrivacyClass::Derived.allows_matter());
assert!(PrivacyClass::Anonymous.allows_matter());
assert!(PrivacyClass::Restricted.allows_matter());
}
// --- monotonicity property ---------------------------------------------
#[test]
fn allows_matter_implies_allows_network() {
// Matter is a subset of Network — if a class is Matter-eligible, it
// must also be Network-eligible. The reverse is not true (Derived is
// Network-eligible but not Matter-eligible).
for c in ALL_CLASSES {
if c.allows_matter() {
assert!(
c.allows_network(),
"{c:?}: allows_matter without allows_network is a contract violation",
);
}
}
}
#[test]
fn allows_network_strictly_excludes_raw() {
// Class 0 (Raw) is the only class that fails allows_network. Any future
// refactor that lets Raw cross a NetworkSink violates ADR-118 invariant I1.
for c in ALL_CLASSES {
let expected = !matches!(c, PrivacyClass::Raw);
assert_eq!(
c.allows_network(),
expected,
"{c:?}: allows_network drift",
);
}
}
#[test]
fn allows_matter_strictly_requires_class_two_or_three() {
for c in ALL_CLASSES {
let expected = matches!(c, PrivacyClass::Anonymous | PrivacyClass::Restricted);
assert_eq!(c.allows_matter(), expected, "{c:?}: allows_matter drift");
}
}
// --- cross-consistency with Sink::MIN_CLASS ----------------------------
/// For a sink with `MIN_CLASS = K`, a class `C` should be accepted iff
/// `C.as_u8() >= K.as_u8()`. Iter 3 implemented exactly this in `check_class`.
/// The helpers above must agree.
fn check_consistency<S: Sink>(class: PrivacyClass, helper_says_allowed: bool) {
let sink_min = S::MIN_CLASS.as_u8();
let class_byte = class.as_u8();
let sink_says_allowed = class_byte >= sink_min;
assert_eq!(
helper_says_allowed,
sink_says_allowed,
"{class:?} vs {} ({} >= {} should be {}, helper said {})",
S::KIND,
class_byte,
sink_min,
sink_says_allowed,
helper_says_allowed,
);
}
#[test]
fn local_sink_accepts_every_class_per_helper() {
for c in ALL_CLASSES {
// LocalSink has MIN_CLASS = Raw (byte 0) — accepts all.
check_consistency::<LocalKind>(c, true);
}
}
#[test]
fn network_sink_consistency_matches_allows_network() {
for c in ALL_CLASSES {
check_consistency::<NetworkKind>(c, c.allows_network());
}
}
#[test]
fn matter_sink_consistency_matches_allows_matter() {
for c in ALL_CLASSES {
check_consistency::<MatterKind>(c, c.allows_matter());
}
}
// --- byte-value pinning -----------------------------------------------
#[test]
fn as_u8_returns_documented_byte_values() {
assert_eq!(PrivacyClass::Raw.as_u8(), 0);
assert_eq!(PrivacyClass::Derived.as_u8(), 1);
assert_eq!(PrivacyClass::Anonymous.as_u8(), 2);
assert_eq!(PrivacyClass::Restricted.as_u8(), 3);
}
#[test]
fn class_byte_ordering_matches_information_density() {
// Higher numerical class = less information density. Sanity check.
let raw = PrivacyClass::Raw.as_u8();
let derived = PrivacyClass::Derived.as_u8();
let anonymous = PrivacyClass::Anonymous.as_u8();
let restricted = PrivacyClass::Restricted.as_u8();
assert!(raw < derived);
assert!(derived < anonymous);
assert!(anonymous < restricted);
}