feat: add sensing evidence claim gate

This commit is contained in:
rUv
2026-08-21 20:48:53 -04:00
parent a3b6e1d500
commit a56687ca1d
14 changed files with 2712 additions and 15 deletions

11
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,11 @@
# Evidence policy and its enforcement path require explicit repository-owner
# review. Branch protection must require Code Owner approval for this to bind.
/.github/CODEOWNERS @ruvnet
/evidence/policies/ @ruvnet
/.github/workflows/model-release-gate.yml @ruvnet
/v2/crates/wifi-densepose-train/ @ruvnet
/README.md @ruvnet
/benchmarks/ @ruvnet
/docs/benchmarks/ @ruvnet
/docs/releases/ @ruvnet
/docs/huggingface/ @ruvnet

View File

@@ -5,16 +5,17 @@ name: Model release gate (ADR-298)
# boundary, near-constant output, degenerate class balance, a metric
# surfaced under a task name it wasn't computed as) before it ships.
#
# Checker: v2/crates/wifi-densepose-train/src/model_gates.rs
# Checkers:
# * v2/crates/wifi-densepose-train/src/model_gates.rs
# * v2/crates/wifi-densepose-train/src/sensing_claim_gate.rs (ADR-328)
#
# IMPORTANT — the honest scope of this job: it protects the *checker itself*
# from regressing (the gate logic + its issue-1521 regression fixture are
# exercised on every push/PR that touches this crate), and running it is
# required before ADR-298 can be called "wired in" at all. It does NOT gate
# an actual model publish — this repository does not automate uploading to
# the HuggingFace model repo (`ruvnet/wifi-densepose-pretrained`); that
# remains a manual, human-run step. Before publishing or replacing a model
# artifact there, run this gate against the real head weights locally:
# IMPORTANT — the honest scope of this job: it protects the structural model
# checker from regressing and hard-fails every committed sensing claim manifest
# that does not satisfy the repository-owned ADR-328 policy. It still does NOT
# gate an actual HuggingFace model publish because this repository does not
# automate uploads to `ruvnet/wifi-densepose-pretrained`; that remains a manual,
# human-run step. Before publishing or replacing a model artifact there, run
# the structural model gate against the real head weights locally:
#
# cargo test -p wifi-densepose-train model_gates
#
@@ -29,14 +30,45 @@ on:
- master
paths:
- "v2/crates/wifi-densepose-train/**"
- "evidence/claims/**"
- "evidence/fixtures/**"
- "evidence/policies/**"
- "README.md"
- "benchmarks/**"
- "docs/benchmarks/**"
- "docs/releases/**"
- "docs/huggingface/**"
- "docs/adr/ADR-298-model-release-sanity-gates.md"
- "docs/adr/ADR-304-evidence-engine.md"
- "docs/adr/ADR-328-sensing-evidence-claim-gate.md"
- ".github/workflows/model-release-gate.yml"
- ".github/CODEOWNERS"
pull_request:
paths:
- "v2/crates/wifi-densepose-train/**"
- "evidence/claims/**"
- "evidence/fixtures/**"
- "evidence/policies/**"
- "README.md"
- "benchmarks/**"
- "docs/benchmarks/**"
- "docs/releases/**"
- "docs/huggingface/**"
- "docs/adr/ADR-298-model-release-sanity-gates.md"
- "docs/adr/ADR-304-evidence-engine.md"
- "docs/adr/ADR-328-sensing-evidence-claim-gate.md"
- ".github/workflows/model-release-gate.yml"
- ".github/CODEOWNERS"
workflow_dispatch:
permissions:
contents: read
env:
# Update only with the CODEOWNERS-reviewed policy. This prevents an unnoticed
# threshold edit from changing the policy consumed by the same workflow.
CLAIM_POLICY_SHA256: 1ba2b73ede726a789aa50f4eb60a443429bb2f38ff3a2acf27755708265e303f
jobs:
model-release-gate:
name: Model release gate check
@@ -46,22 +78,167 @@ jobs:
with:
persist-credentials: false
submodules: recursive
fetch-depth: 0
- name: Verify protected claim-policy digest
run: |
actual="$(sha256sum evidence/policies/sensing-claim-policy-v1.json | cut -d ' ' -f 1)"
test "$actual" = "$CLAIM_POLICY_SHA256"
- name: Require evidence manifest for changed public claim surfaces
if: github.event_name == 'pull_request'
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
mapfile -d '' -t changed < <(git diff --name-only -z --diff-filter=ACMRT "$BASE_SHA" "$GITHUB_SHA")
changed_manifests=()
for path in "${changed[@]}"; do
case "$path" in
evidence/claims/research/*.json|evidence/claims/production/*.json|evidence/claims/safety_critical/*.json)
changed_manifests+=("$path")
;;
esac
done
for surface in "${changed[@]}"; do
required_class=""
case "$surface" in
README.md)
if git diff --unified=0 "$BASE_SHA" "$GITHUB_SHA" -- README.md \
| grep -Eiq '^\+[^+].*(accuracy|auc|precision|recall|sensitivity|specificity|false[ -]?positive|confidence|detect|through walls|heart[ -]?rate|breathing|occupancy|presence|pose|pck|mpjpe|latency|held[ -]?out|benchmark|score|production[ -]?ready|[0-9]+([.][0-9]+)?(%|[[:space:]]*(ms|hz|bpm)))'; then
required_class="production"
else
continue
fi
;;
benchmarks/*|docs/benchmarks/*)
if git diff --unified=0 "$BASE_SHA" "$GITHUB_SHA" -- "$surface" \
| grep -Eiq '^\+[^+].*(production|ready|deploy|ship|safety|medical|commercial)'; then
required_class="production"
else
required_class="any"
fi
;;
docs/releases/*|docs/huggingface/*)
required_class="production"
;;
*)
continue
;;
esac
matched=false
for manifest in "${changed_manifests[@]}"; do
[[ -f "$manifest" ]] || continue
relative="${manifest#evidence/claims/}"
class="${relative%%/*}"
filename="${relative#*/}"
[[ "$filename" != */* ]] || continue
if [[ "$required_class" == production && "$class" != production ]]; then
continue
fi
if jq -e --arg surface "$surface" \
'(.claim_surface_paths | type == "array") and (.claim_surface_paths | index($surface) != null)' \
"$manifest" >/dev/null; then
matched=true
break
fi
done
if [[ "$matched" != true ]]; then
echo "Public claim surface $surface changed without a matching $required_class class-bound evidence manifest." >&2
exit 1
fi
done
- name: Install Rust toolchain
run: rustup toolchain install stable --profile minimal
run: rustup toolchain install 1.89 --profile minimal
- name: Run the model-release gate's own test suite
working-directory: v2
run: cargo test -p wifi-densepose-train --no-default-features model_gates -- --nocapture
- name: Run sensing evidence and claim gate tests
working-directory: v2
run: cargo test -p wifi-densepose-train --no-default-features sensing_claim_gate -- --nocapture
- name: Validate repository policy with research-only fixture
working-directory: v2
run: |
mkdir -p ../evidence-receipts
cargo run -p wifi-densepose-train --no-default-features \
--bin sensing-claim-gate -- \
--manifest ../evidence/fixtures/research-synthetic.json \
--policy ../evidence/policies/sensing-claim-policy-v1.json \
--required-class research \
--receipt ../evidence-receipts/research-synthetic.receipt.json
- name: Gate every committed sensing claim manifest
working-directory: v2
run: |
mkdir -p ../evidence-receipts
manifest_count=0
gate_failed=false
while IFS= read -r -d '' manifest; do
relative="${manifest#../evidence/claims/}"
class="${relative%%/*}"
filename="${relative#*/}"
if [[ ! -f "$manifest" || -L "$manifest" ]]; then
echo "Claim manifest must be a regular non-symlink file: $manifest" >&2
gate_failed=true
continue
fi
if [[ "$filename" == */* || ! "$filename" =~ ^[a-z0-9][a-z0-9._-]*\.json$ ]]; then
echo "Claim JSON must be exactly one level below a class directory and use a canonical filename: $manifest" >&2
gate_failed=true
continue
fi
case "$class" in
research|production|safety_critical) ;;
*)
echo "Unsupported claim class directory for $manifest" >&2
gate_failed=true
continue
;;
esac
cli_class="${class//_/-}"
stem="${filename%.json}"
manifest_count=$((manifest_count + 1))
if ! cargo run -p wifi-densepose-train --no-default-features \
--bin sensing-claim-gate -- \
--manifest "$manifest" \
--policy ../evidence/policies/sensing-claim-policy-v1.json \
--required-class "$cli_class" \
--receipt "../evidence-receipts/${class}-${stem}.receipt.json"; then
gate_failed=true
fi
done < <(find ../evidence/claims -name '*.json' -print0 | sort -z)
if (( manifest_count == 0 )); then
echo "Claim inventory is empty; at least one class-bound manifest is required." >&2
exit 1
fi
if [[ "$gate_failed" == true ]]; then
echo "One or more sensing claim manifests failed closed." >&2
exit 1
fi
- name: Upload machine-readable evidence receipts
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: sensing-claim-receipts
path: evidence-receipts/
if-no-files-found: warn
- name: Summarize result
if: always()
run: |
{
echo '### Model release gate (ADR-298)'
echo ''
echo 'This job protects `model_gates.rs` from regressing. It does not itself'
echo 'gate a real HuggingFace model publish — that upload is a manual step'
echo 'outside this repository; run `cargo test -p wifi-densepose-train model_gates`'
echo 'against real head weights before publishing one.'
echo 'This job protects `model_gates.rs` and the ADR-328 claim gate from'
echo 'regressing, and gates every JSON manifest in `evidence/claims/`.'
echo 'This is repository evidence lint. The committed policy disables'
echo 'production and safety claims until artifact retrieval, a real presence'
echo 'reproducer, authenticated evaluator attestation, and maintainer review.'
echo 'It does not gate the external HuggingFace upload.'
} >> "$GITHUB_STEP_SUMMARY"

View File

@@ -39,6 +39,13 @@ for any classifier artifact proposed for release, fails on:
Each gate emits a structured, human-readable failure explaining the defect and
the offending numbers.
ADR-328 adds the complementary evidence-release boundary. ADR-298 answers
"is this model artifact structurally degenerate?" ADR-328 answers "does the
submitted physical-sensing evidence support this class of public claim under a
pre-registered policy?" A model must pass both checks. Structural model health
cannot substitute for real-hardware held-out evidence, and strong benchmark
evidence cannot excuse a degenerate classifier head.
## Consequences
- The specific degenerate presence head cannot ship again, and the
@@ -55,6 +62,9 @@ the offending numbers.
metric cannot be constructed with a presence label.
- `cargo test -p wifi-densepose-train`; the CI gate runs in the model-check
workflow.
- `cargo test -p wifi-densepose-train --no-default-features sensing_claim_gate`;
`.github/workflows/model-release-gate.yml` also evaluates committed claim
manifests and retains machine-readable receipts.
- This ADR does **not** withdraw the already-published artifact (an
outward-facing action requiring maintainer sign-off) — it prevents
recurrence and documents the model-card correction.

View File

@@ -1,6 +1,6 @@
# ADR-304: Evidence engine — MLflow for physical sensing
- **Status**: Accepted — initial implementation planned (ADR-300 phase 1)
- **Status**: Accepted — ADR-328 claim receipt edge implemented; signed ledger planned
- **Date**: 2026-08-11
- **Deciders**: ruv
- **Tags**: evidence, provenance, ledger, accuracy, drift, benchmark, honesty, substrate
@@ -52,6 +52,14 @@ that unifies them per deployment context:
Build an **evidence engine**: a per-`(room, device, subject)` append-only
accuracy ledger that every model automatically writes to.
ADR-328 now supplies the first enforceable edge of this decision: a strict
claim manifest, a separately versioned policy, fail-closed production/safety
rules, and a content-addressed JSON receipt. It deliberately does **not** claim
to be the append-only signed ledger described below. Until the ledger and
ADR-319 witness anchoring land, the receipt carries artifact digests and CI
decisions, production cannot exceed `metadata_attested`, and the committed
policy denies production entirely. It cannot authorize a production claim.
### 1. The evidence record
- An `EvidenceRecord` keyed by context — space id (ADR-306), signed device id
@@ -104,6 +112,10 @@ accuracy ledger that every model automatically writes to.
## Validation
- ADR-328 gate: simulator evidence cannot release a production claim; held-out
environment overlap, insufficient samples, missing thresholds, and a failed
confidence bound each produce a deterministic denial receipt. CI evaluates
every manifest in `evidence/claims/` against the repository-owned policy.
- `cargo test` on the evidence-engine crate — append-only invariant (no
in-place mutation; corrections are new records); per-context aggregation math
against fixtures; evidence-level is set by provenance and cannot be upgraded;

View File

@@ -0,0 +1,179 @@
# ADR-328: Sensing evidence and claim gate
- **Status**: Proposed, implementation in this PR
- **Date**: 2026-08-21
- **Deciders**: ruv
- **Tags**: evidence, claims, provenance, release-gate, safety, benchmark, receipt
## Context
RuView has several strong evidence primitives but no single enforceable release
boundary. ADR-291 supplies leak-free evaluation reports, ADR-295 keeps synthetic
sources from presenting as live, ADR-298 rejects degenerate model heads, and
`wifi-densepose-train::occupancy_bench` withholds claims from synthetic or mock
data. ADR-304, ADR-317, ADR-318, and ADR-319 describe the longer-term evidence
ledger, scorecard, capability certificate, and witness chain.
The immediate gap is narrower and operationally important: a production-facing
claim can still be written without submitting metadata that documents physical
hardware provenance, held-out room coverage, minimum sample size, a confidence
bound, or a pre-registered threshold. Existing checks are library APIs. They do
not produce a content-addressed decision that CI can retain.
## Decision
Add a strict, dependency-light claim gate to `wifi-densepose-train` and run it
in `.github/workflows/model-release-gate.yml`.
### Separate observations from policy
The claim author submits a JSON manifest containing:
- Claim id, capability, statement, and release class.
- Source kind: synthetic, simulator, mock, recorded hardware, or live hardware.
- For physical sources: device family, pseudonymous device id, firmware build,
capture digest, and independent ground-truth digest.
- Evaluation protocol, train and test environment identifiers, subject
identifiers where applicable, aggregate sample counts, and per-environment
sample accounting.
- Aggregate and per-environment metric point estimates and confidence bounds.
- Exact source commit plus model, split-manifest, and evaluation-report digests.
- A policy-allowlisted evaluator id, registered confidence method, exact
allowlisted reproducer argv, and optional independent-validation report
digest.
- Exact repository paths where the statement is presented publicly.
The manifest contains no acceptance thresholds. Thresholds come from the
separate, versioned repository policy in
`evidence/policies/sensing-claim-policy-v1.json`. This prevents a claim author
from choosing a weaker rule in the same artifact being evaluated.
For production and safety classes, the policy also registers the SHA-256 of the
exact manifest bytes by claim id. This protects the statement and every bound
commit, sample count, metric, artifact, evaluator, reviewer, reproducer, and
surface path as one reviewed unit. Changing any field without updating the
reviewed registry fails `registered_manifest`.
Both inputs use strict Serde structures with unknown fields rejected and a 1 MiB
metadata-only size limit. Raw CSI, video, personal data, and credentials are not
accepted as claim metadata.
### Release classes and invariants
The gate evaluates all applicable rules and fails closed.
1. Research claims may use synthetic evidence, but a passing decision is
`research_only`, never production.
2. The implementation can evaluate production metadata only when a reviewed
policy enables it. Production claims require physical hardware metadata, a
held-out-environment protocol, disjoint train and test room identifiers, at
least 300 samples in at least two unseen rooms with 100 samples, 20 positive
cases, and 20 negative cases per room, aggregate and per-room
confidence-bound metrics, exact evaluation bindings, and capability-specific
thresholds. Identifiers are canonical ASCII, trimmed, and case-folded for
leakage comparison; whitespace, case, and Unicode aliases cannot manufacture
a held-out room.
3. Metric thresholds are compared against the conservative confidence bound.
A production or safety rule cannot use only a point estimate.
4. The v1 production policy covers presence only. Pose, vitals, and every other
production capability fail closed until a reviewed policy is added.
5. Production metadata that passes every enabled rule is `metadata_attested`,
not release-authorized. It remains `claim_releasable: false` because v1 does not
retrieve and hash private artifacts or verify a trusted evaluator signature.
6. Production and safety-critical claims are disabled in the committed v1
policy. Production cannot be enabled until a real presence reproducer and
authenticated artifact-verification path exist. Enabling safety also requires
a new reviewed policy with capability-specific thresholds, at least three
unseen environments, at least 1,000 samples, subject-disjoint evaluation,
real hardware evidence, and independent validation. Passing this software
gate would still not constitute medical, functional-safety, or regulatory
certification.
The dormant initial presence thresholds require confidence-bound ROC AUC,
sensitivity, and specificity of at least 0.90, an upper-confidence-bound
false-positive rate of at most 0.10, and at least 60 positive and 60 negative
samples. These are
minimum evidence-governance thresholds, not a claim that the current RuView
model meets them.
### Content-addressed receipt
Every well-formed evaluation emits
`ruview.sensing-claim-receipt/v1` JSON containing:
- SHA-256 of the exact manifest and policy bytes.
- Claim, capability, class, and source kind.
- One stable rule result with observed and required values per invariant.
- `metadata_attested`, `research_only`, or `denied` decision semantics plus
separate `metadata_gate_passed` and `claim_releasable` booleans.
- SHA-256 of the canonical compact JSON receipt.
Denied evidence and production metadata both produce a receipt and exit with
code 2. Only policy-conformant research evidence exits 0. Malformed input exits
1. This gives CI and reviewers a durable, machine-readable answer without
pretending that the receipt is already a signed ADR-319 witness.
## Consequences
- Simulation can no longer release a production or safety claim, even with
perfect submitted metrics.
- A strong point estimate with a weak lower confidence bound is blocked.
- Missing policies and missing capability thresholds are denials, not implicit
passes.
- Once a protected caller selects production, a manifest cannot downgrade that
decision to research: the CLI requires an exact class match.
- Production and safety manifests must list a protected public surface, and CI
requires a changed surface to be named by a changed class-bound manifest.
- Capture and ground-truth artifacts, model/split/evaluation artifacts, and the
independent review report must use distinct content digests for distinct
evidence roles.
- Repository CI is evidence lint, not the external HuggingFace publication
authority. Dedicated benchmark, release, and model-card paths require a
class-bound manifest; claim-like README additions use a conservative keyword
lint. CODEOWNERS covers all of those surfaces because the keyword lint is not
semantic proof that every possible prose claim was detected. The policy
digest is also pinned, but branch protection must require Code Owner review
for either control to bind.
- The gate adds negligible evaluation cost for sub-1-MiB metadata. Rust compile
time, approximately minutes on a cold runner, dominates the workflow.
- The default presence requirements will block current weak or incomplete
evidence once production is enabled. This is intended.
- This change is prospective. It does not attest the existing numeric and
capability statements already present in README or documentation. Those
statements remain an explicit evidence-audit backlog; only new or modified
claim surfaces are forced through the CI association check.
The largest residual risk is artifact authenticity. A syntactically valid
SHA-256 reference proves content identity only after the referenced artifact is
retrieved and hashed; it does not prove who captured it or whether the
ground-truth process was independent. ADR-304 and ADR-319 remain responsible
for append-only storage, signatures, authenticated identity, witness anchoring,
and offline chain verification. Until that integration lands, the gate cannot
accept a production claim; it can only issue a manual-review-required metadata
receipt.
## Validation
- `cargo test -p wifi-densepose-train --no-default-features sensing_claim_gate`
- The test suite proves: simulator production denial; research-only synthetic
handling; environment leakage denial; minimum-sample denial; confidence-bound
gating; missing-threshold denial; default safety denial; strict JSON; and
deterministic receipt hashing.
- CI evaluates the committed policy against a synthetic research fixture, then
fails an empty or improperly nested claim inventory, evaluates every JSON
manifest in `evidence/claims/`, and uploads receipts.
- Acceptance test: a simulator manifest with perfect values exits 2 with failed
`source_class` and `hardware_provenance` rules. Under the unit-test policy
that enables production, real-hardware presence metadata that clears every
registered confidence-bound rule still exits 2 as `metadata_attested` with
`claim_releasable: false`; the committed policy denies production earlier.
## References
- ADR-291: Public benchmark evaluation harness
- ADR-295: Source provenance state machine
- ADR-298: Model release sanity gates
- ADR-304: Evidence engine
- ADR-317: Multi-domain scorecard
- ADR-318: Capability certificates
- ADR-319: Witness chain

View File

@@ -180,6 +180,7 @@ Statuses: **Proposed** (under discussion), **Accepted** (approved and/or impleme
| [ADR-319](ADR-319-witness-chain.md) | Witness chain — staged, signed epistemic envelope | Accepted (phase 1) |
| [ADR-320](ADR-320-sensor-hal.md) | RuView sensor HAL — abstract all sensing hardware to one Observation type | Proposed (phase 2) |
| [ADR-321](ADR-321-decision-policy-action-authorization.md) | Decision policy — action authorization conditioned on certificate class, freshness, uncertainty, evidence | Accepted (phase 1) |
| [ADR-328](ADR-328-sensing-evidence-claim-gate.md) | Sensing evidence claim gate — physical provenance, held-out metrics, policy thresholds, receipts | Proposed (implementation in PR) |
| [ADR-323](ADR-323-native-rust-physics-constrained-pose-refinement.md) | Native Rust physics-constrained pose refinement | Proposed |
---

80
evidence/claims/README.md Normal file
View File

@@ -0,0 +1,80 @@
# Sensing claim manifests
Any new or modified production or safety-facing RuView sensing claim must have
a strict JSON manifest in this directory and pass the ADR-328 gate against the
committed policy. This is a forward-only control: existing unmodified README
and documentation claims are not grandfathered as valid evidence and have not
passed this gate; they require a separate evidence audit. Claim authors provide
observations and artifact digests. They cannot provide or weaken their own
thresholds.
Place manifests under `research/`, `production/`, or `safety_critical/`. CI
derives the required class from that directory and rejects a mismatched
self-declared class. Changes in the dedicated benchmark, release, and model-card
directories, plus claim-like additions detected in `README.md`, must add or
update a manifest that names the exact surface path in the same pull request.
CODEOWNERS also covers these surfaces because text classification is a
conservative lint heuristic, not semantic proof that every prose claim was
detected.
Run the gate from `v2/`:
```bash
mkdir -p ../evidence-receipts
cargo run -p wifi-densepose-train --no-default-features \
--bin sensing-claim-gate -- \
--manifest ../evidence/claims/<class>/<claim>.json \
--policy ../evidence/policies/sensing-claim-policy-v1.json \
--required-class <research|production|safety-critical> \
--receipt ../evidence-receipts/<claim>.receipt.json
```
The process exits with code `0` only for a policy-conformant research statement.
The committed v1 policy disables production and safety claims until a real
presence reproducer, authenticated evaluator signature, and artifact retrieval
and hashing are integrated. A later reviewed policy may let structurally valid
production metadata reach `metadata_attested`, but it remains
`claim_releasable: false` and exits with code `2`. A denied claim also writes
its receipt and exits with code `2`.
## Required evidence
A policy that enables production requires all of the following:
1. `recorded_hardware` or `live_hardware` source provenance.
2. Device family, stable device identifier, firmware version, capture SHA-256,
and independently recorded ground-truth SHA-256. Artifact roles must have
distinct digests.
3. A held-out-environment split with disjoint train and test room identifiers,
plus per-room sample accounting and confidence-bound metrics.
4. At least 300 test samples across at least two unseen rooms, with at least 100
samples, 20 positive cases, and 20 negative cases in each room; per-room
counts must sum exactly to the aggregate.
5. Exact source commit, model, split, and evaluation-report digests plus an
exact policy-allowlisted reproducer argv and registered confidence method.
6. Capability-specific metrics whose confidence bounds, not only point
estimates, clear the repository-owned thresholds.
7. A registered evaluator identifier and a distinct, allowlisted independent
reviewer with a content-addressed report. These identifiers are allowlist
checks, not proof of identity; production stays blocked until they are
signed.
8. An exact manifest SHA-256 registered in the reviewed policy and at least one
protected public claim-surface path. Any change to the statement, metrics,
counts, evaluator, or artifact bindings invalidates that registration.
The v1 policy defines dormant production thresholds only for presence and
disables production and safety-critical claims entirely. Pose, vitals, and
other production claims also lack a capability policy. Reviewers must add a
real evaluator, authenticated artifact verification, and a versioned policy
before any production class can be enabled.
This gate is evidence governance, not medical, product-safety, or regulatory
certification.
Research manifests may use synthetic or simulator evidence, but a passing
receipt is marked `research_only` and cannot be promoted to production. CI
binds each manifest to the release class selected by its protected directory or
workflow argument, so a caller-classified production surface cannot self-label
its manifest as research.
Do not commit raw CSI, video, personal data, credentials, or private subject
identifiers here. Store only pseudonymous metadata and content digests.

View File

@@ -0,0 +1,30 @@
{
"schema_version": "ruview.sensing-claim-evidence/v1",
"claim_id": "synthetic-gate-validation-v1",
"capability": "presence",
"claim_class": "research",
"statement": "Synthetic evidence validates gate behavior only and is not a hardware performance result.",
"claim_surface_paths": [],
"source": {
"kind": "synthetic",
"device_family": null,
"device_id": null,
"firmware_version": null,
"capture_artifact": null,
"ground_truth_artifact": null
},
"evaluation": {
"protocol": "in_domain",
"train_environment_ids": ["synthetic-train"],
"test_environment_ids": ["synthetic-test"],
"environment_results": [],
"train_subject_ids": [],
"test_subject_ids": [],
"test_samples": 30,
"positive_test_samples": null,
"negative_test_samples": null
},
"metrics": {},
"evaluation_binding": null,
"independent_validation": null
}

View File

@@ -0,0 +1,30 @@
{
"schema_version": "ruview.sensing-claim-evidence/v1",
"claim_id": "fixture-synthetic-research-v1",
"capability": "presence",
"claim_class": "research",
"statement": "Synthetic fixture exercises the claim gate and is not a production result.",
"claim_surface_paths": [],
"source": {
"kind": "synthetic",
"device_family": null,
"device_id": null,
"firmware_version": null,
"capture_artifact": null,
"ground_truth_artifact": null
},
"evaluation": {
"protocol": "in_domain",
"train_environment_ids": ["synthetic-train"],
"test_environment_ids": ["synthetic-test"],
"environment_results": [],
"train_subject_ids": [],
"test_subject_ids": [],
"test_samples": 30,
"positive_test_samples": null,
"negative_test_samples": null
},
"metrics": {},
"evaluation_binding": null,
"independent_validation": null
}

View File

@@ -0,0 +1,89 @@
{
"schema_version": "ruview.sensing-claim-policy/v1",
"policy_id": "ruview-sensing-claims-2026-08-v1",
"registered_manifest_sha256": {},
"trusted_evaluators": ["ruview-ci"],
"trusted_independent_reviewers": ["independent-lab"],
"allowed_reproducer_argv": [],
"allowed_confidence_methods": ["bootstrap-percentile-95", "wilson-score-95"],
"claim_classes": {
"research": {
"enabled": true,
"allow_non_hardware": true,
"require_hardware_evidence": false,
"min_test_samples": 30,
"min_test_environments": 1,
"min_samples_per_environment": 0,
"require_held_out_environment": false,
"require_subject_disjoint": false,
"require_reproducer": false,
"require_independent_validation": false,
"capabilities": {
"*": {
"require_binary_class_counts": false,
"min_positive_samples": 0,
"min_negative_samples": 0,
"min_positive_samples_per_environment": 0,
"min_negative_samples_per_environment": 0,
"metrics": {}
}
}
},
"production": {
"enabled": false,
"allow_non_hardware": false,
"require_hardware_evidence": true,
"min_test_samples": 300,
"min_test_environments": 2,
"min_samples_per_environment": 100,
"require_held_out_environment": true,
"require_subject_disjoint": false,
"require_reproducer": true,
"require_independent_validation": true,
"capabilities": {
"presence": {
"require_binary_class_counts": true,
"min_positive_samples": 60,
"min_negative_samples": 60,
"min_positive_samples_per_environment": 20,
"min_negative_samples_per_environment": 20,
"metrics": {
"roc_auc": {
"minimum": 0.9,
"maximum": null,
"statistic": "lower_confidence_bound"
},
"sensitivity": {
"minimum": 0.9,
"maximum": null,
"statistic": "lower_confidence_bound"
},
"specificity": {
"minimum": 0.9,
"maximum": null,
"statistic": "lower_confidence_bound"
},
"false_positive_rate": {
"minimum": null,
"maximum": 0.1,
"statistic": "upper_confidence_bound"
}
}
}
}
},
"safety_critical": {
"enabled": false,
"allow_non_hardware": false,
"require_hardware_evidence": true,
"min_test_samples": 1000,
"min_test_environments": 3,
"min_samples_per_environment": 250,
"require_held_out_environment": true,
"require_subject_disjoint": true,
"require_reproducer": true,
"require_independent_validation": true,
"capabilities": {}
}
}
}

View File

@@ -27,6 +27,13 @@ required-features = ["tch-backend"]
name = "aa_score_runner"
path = "src/bin/aa_score_runner.rs"
# ADR-328 sensing evidence and claim gate. This binary validates a claim
# manifest against a repository-owned policy and emits a deterministic JSON
# receipt. It is dependency-light and runs under --no-default-features.
[[bin]]
name = "sensing-claim-gate"
path = "src/bin/sensing_claim_gate.rs"
[features]
default = []
tch-backend = ["tch"]

View File

@@ -0,0 +1,103 @@
//! CLI for the ADR-328 sensing evidence and claim gate.
use std::fs;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use anyhow::{bail, Context, Result};
use clap::{Parser, ValueEnum};
use wifi_densepose_train::sensing_claim_gate::{
evaluate_claim_json_for_class, ClaimClass, MAX_INPUT_BYTES,
};
#[derive(Debug, Clone, Copy, ValueEnum)]
enum RequiredClass {
Research,
Production,
SafetyCritical,
}
impl From<RequiredClass> for ClaimClass {
fn from(value: RequiredClass) -> Self {
match value {
RequiredClass::Research => ClaimClass::Research,
RequiredClass::Production => ClaimClass::Production,
RequiredClass::SafetyCritical => ClaimClass::SafetyCritical,
}
}
}
#[derive(Debug, Parser)]
#[command(
name = "sensing-claim-gate",
about = "Validate a RuView sensing claim and emit a content-addressed JSON receipt"
)]
struct Args {
/// Strict JSON claim manifest.
#[arg(long)]
manifest: PathBuf,
/// Repository-owned claim policy.
#[arg(long)]
policy: PathBuf,
/// Release class selected by the protected caller. The manifest must match.
#[arg(long, value_enum)]
required_class: RequiredClass,
/// Optional path for the same receipt printed to stdout. The parent must
/// already exist, which avoids creating directories from untrusted input.
#[arg(long)]
receipt: Option<PathBuf>,
}
fn main() -> ExitCode {
match run(Args::parse()) {
Ok(code) => ExitCode::from(code),
Err(error) => {
eprintln!("sensing claim gate error: {error:#}");
ExitCode::FAILURE
}
}
}
fn run(args: Args) -> Result<u8> {
let manifest = read_metadata_json(&args.manifest, "manifest")?;
let policy = read_metadata_json(&args.policy, "policy")?;
let envelope = evaluate_claim_json_for_class(&manifest, &policy, args.required_class.into())?;
let output = serde_json::to_string_pretty(&envelope).context("serialize receipt envelope")?;
println!("{output}");
if let Some(path) = args.receipt {
let parent = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
if !parent.is_dir() {
bail!("receipt parent does not exist: {}", parent.display());
}
fs::write(&path, format!("{output}\n"))
.with_context(|| format!("write receipt {}", path.display()))?;
}
Ok(if envelope.receipt.claim_releasable {
0
} else {
2
})
}
fn read_metadata_json(path: &Path, label: &str) -> Result<Vec<u8>> {
let metadata =
fs::metadata(path).with_context(|| format!("stat {label} {}", path.display()))?;
if !metadata.is_file() {
bail!("{label} is not a regular file: {}", path.display());
}
if metadata.len() > MAX_INPUT_BYTES as u64 {
bail!(
"{label} is {} bytes; maximum is {MAX_INPUT_BYTES}",
metadata.len()
);
}
fs::read(path).with_context(|| format!("read {label} {}", path.display()))
}

View File

@@ -71,6 +71,10 @@ pub mod model_gates;
pub mod protocols;
pub mod rapid_adapt;
pub mod ruview_metrics;
/// Sensing evidence and claim gate (ADR-328) — validates provenance,
/// held-out-environment coverage, sample counts, pre-registered metric
/// thresholds, and claim class, then emits a deterministic JSON receipt.
pub mod sensing_claim_gate;
pub mod signal_features;
pub mod subcarrier;
pub mod virtual_aug;
@@ -127,6 +131,12 @@ pub use model_gates::{
LabeledMetric, LinearHead, MetricKind, ModelGateReport, ProbeSet,
};
// ADR-328 — sensing evidence and claim receipts.
pub use sensing_claim_gate::{
evaluate_claim_json_for_class, ClaimGateError, ClaimManifest, ClaimPolicy, GateDecision,
GateReceipt, ReceiptEnvelope,
};
pub use error::{ConfigError, DatasetError, MaeError, ProtocolError, SubcarrierError, TrainError};
// TrainResult<T> is the generic Result alias from error.rs; the concrete
// TrainResult struct from trainer.rs is accessed via trainer::TrainResult.

File diff suppressed because it is too large Load Diff