mirror of
https://github.com/ruvnet/RuView.git
synced 2026-08-26 02:04:55 +00:00
feat: add sensing evidence claim gate
This commit is contained in:
11
.github/CODEOWNERS
vendored
Normal file
11
.github/CODEOWNERS
vendored
Normal 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
|
||||
205
.github/workflows/model-release-gate.yml
vendored
205
.github/workflows/model-release-gate.yml
vendored
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
179
docs/adr/ADR-328-sensing-evidence-claim-gate.md
Normal file
179
docs/adr/ADR-328-sensing-evidence-claim-gate.md
Normal 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
|
||||
@@ -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
80
evidence/claims/README.md
Normal 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.
|
||||
30
evidence/claims/research/synthetic-gate-validation.json
Normal file
30
evidence/claims/research/synthetic-gate-validation.json
Normal 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
|
||||
}
|
||||
30
evidence/fixtures/research-synthetic.json
Normal file
30
evidence/fixtures/research-synthetic.json
Normal 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
|
||||
}
|
||||
89
evidence/policies/sensing-claim-policy-v1.json
Normal file
89
evidence/policies/sensing-claim-policy-v1.json
Normal 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": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
|
||||
103
v2/crates/wifi-densepose-train/src/bin/sensing_claim_gate.rs
Normal file
103
v2/crates/wifi-densepose-train/src/bin/sensing_claim_gate.rs
Normal 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()))
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
1958
v2/crates/wifi-densepose-train/src/sensing_claim_gate.rs
Normal file
1958
v2/crates/wifi-densepose-train/src/sensing_claim_gate.rs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user