diff --git a/.github/workflows/ruforecast-ci.yml b/.github/workflows/ruforecast-ci.yml new file mode 100644 index 000000000..2f17c8ab9 --- /dev/null +++ b/.github/workflows/ruforecast-ci.yml @@ -0,0 +1,627 @@ +name: RuForecast Rust CI + +on: + push: + branches: [main, develop, 'feat/*', 'feature/*'] + paths: + - 'v2/crates/ruforecast' + - 'v2/Cargo.toml' + - 'v2/Cargo.lock' + - 'v2/rust-toolchain.toml' + - 'scripts/csi-data-policy-check.sh' + - 'scripts/run-ruforecast-benchmarks.sh' + - 'docs/adr/ADR-348-*.md' + - 'docs/adr/ADR-349-*.md' + - 'docs/adr/ADR-350-*.md' + - 'docs/adr/README.md' + - 'docs/benchmarks/ruforecast.md' + - 'docs/huggingface/RUVIEW_FORECAST_MODEL_CARD_TEMPLATE.md' + - 'docs/security/ruview-forecast-*' + - 'docs/validation/ruforecast-requirements-evidence.md' + - '.github/workflows/ruforecast-ci.yml' + pull_request: + branches: [main, develop] + paths: + - 'v2/crates/ruforecast' + - 'v2/Cargo.toml' + - 'v2/Cargo.lock' + - 'v2/rust-toolchain.toml' + - 'scripts/csi-data-policy-check.sh' + - 'scripts/run-ruforecast-benchmarks.sh' + - 'docs/adr/ADR-348-*.md' + - 'docs/adr/ADR-349-*.md' + - 'docs/adr/ADR-350-*.md' + - 'docs/adr/README.md' + - 'docs/benchmarks/ruforecast.md' + - 'docs/huggingface/RUVIEW_FORECAST_MODEL_CARD_TEMPLATE.md' + - 'docs/security/ruview-forecast-*' + - 'docs/validation/ruforecast-requirements-evidence.md' + - '.github/workflows/ruforecast-ci.yml' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ruforecast-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + CARGO_PROFILE_DEV_DEBUG: '0' + CARGO_PROFILE_TEST_DEBUG: '0' + CARGO_PROFILE_BENCH_DEBUG: '0' + CARGO_PROFILE_RELEASE_DEBUG: '0' + +jobs: + contract-rust-189: + name: Forecast contract and feature-off boundary (Rust 1.89) + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: Checkout recursively + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + submodules: recursive + + - name: Install Rust 1.89 with lint components + uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 + with: + toolchain: 1.89.0 + components: rustfmt,clippy + + - name: Cache Cargo artifacts + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + workspaces: v2/crates/ruforecast + key: ruforecast-rust-189 + + - name: Validate benchmark runner and evidence contract + shell: bash + run: | + set -euo pipefail + bash -n scripts/run-ruforecast-benchmarks.sh + test -x scripts/run-ruforecast-benchmarks.sh + test -f docs/benchmarks/ruforecast.md + test -f docs/huggingface/RUVIEW_FORECAST_MODEL_CARD_TEMPLATE.md + test -f docs/security/ruview-forecast-clean-room.md + test -f docs/security/ruview-forecast-threat-model.md + test -f docs/validation/ruforecast-requirements-evidence.md + + for adr_number in 348 349 350; do + mapfile -t adr_files < <( + find docs/adr -maxdepth 1 -type f -name "ADR-${adr_number}-*.md" -print + ) + if [[ "${#adr_files[@]}" -ne 1 ]]; then + echo "Expected exactly one ADR-${adr_number} file, found ${#adr_files[@]}" >&2 + exit 1 + fi + grep -q "^# ADR-${adr_number}:" "${adr_files[0]}" + grep -Eq '^[-*] \*\*Status\*\*: (Proposed|Accepted|Superseded|Deprecated)( |$|—)|^## Status' "${adr_files[0]}" + done + + grep -q '^## Requirements' docs/adr/ADR-348-*.md + grep -q '^## Acceptance gates' docs/adr/ADR-348-*.md + grep -q '^## Requirements and acceptance' docs/adr/ADR-349-*.md + grep -q '^## Requirements and acceptance' docs/adr/ADR-350-*.md + for adr_number in 348 349 350; do + grep -q "| \[ADR-${adr_number}\](ADR-${adr_number}-" docs/adr/README.md \ + || { echo "ADR-${adr_number} is missing from the ADR index" >&2; exit 1; } + done + + for number in $(seq 1 12); do + requirement="$(printf 'RF-%03d' "$number")" + grep -q "| ${requirement} |" docs/validation/ruforecast-requirements-evidence.md \ + || { echo "Missing evidence row for ${requirement}" >&2; exit 1; } + done + for number in $(seq 1 8); do + requirement="$(printf 'FT-%03d' "$number")" + grep -q "| ${requirement} |" docs/validation/ruforecast-requirements-evidence.md \ + || { echo "Missing evidence row for ${requirement}" >&2; exit 1; } + done + for number in $(seq 1 9); do + requirement="$(printf 'PM-%03d' "$number")" + grep -q "| ${requirement} |" docs/validation/ruforecast-requirements-evidence.md \ + || { echo "Missing evidence row for ${requirement}" >&2; exit 1; } + done + + python3 - <<'PY' + from pathlib import Path + import tomllib + + root = Path("v2/crates/ruforecast/crates") + core = tomllib.loads((root / "ruforecast-core/Cargo.toml").read_text()) + model = tomllib.loads((root / "ruforecast-model/Cargo.toml").read_text()) + train = tomllib.loads((root / "ruforecast-train/Cargo.toml").read_text()) + + assert core.get("features", {}).get("default", []) == [] + assert model["features"]["default"] == [], "model defaults must stay backend-free" + assert train["features"]["default"] == [], "train defaults must stay backend-free" + assert {"model", "cpu", "cuda", "wgpu", "ruvector"} <= model["features"].keys() + assert {"cpu", "cuda", "training", "cli", "server", "fal-client"} <= train["features"].keys() + assert {"model", "dep:burn-ndarray"} <= set(model["features"]["cpu"]) + assert {"model", "dep:burn-cuda"} <= set(model["features"]["cuda"]) + assert {"model", "dep:burn-wgpu"} <= set(model["features"]["wgpu"]) + assert "dep:ruvector-core" in model["features"]["ruvector"] + assert {"ruforecast-model/cpu", "training"} <= set(train["features"]["cpu"]) + assert {"ruforecast-model/cuda", "training"} <= set(train["features"]["cuda"]) + + bins = {entry["name"]: entry for entry in train.get("bin", [])} + assert bins["ruforecast"]["required-features"] == ["cli"] + + model_benches = {entry["name"]: entry for entry in model.get("bench", [])} + train_benches = {entry["name"]: entry for entry in train.get("bench", [])} + assert model_benches["forecast_inference"]["harness"] is False + assert model_benches["forecast_inference"]["required-features"] == ["cpu"] + assert train_benches["data_pipeline"]["harness"] is False + assert train_benches["data_pipeline"]["required-features"] == ["cpu"] + PY + + - name: Check formatting for forecast packages only + working-directory: v2/crates/ruforecast + run: >- + cargo +1.89.0 fmt + -p ruforecast-core + -p ruforecast-model + -p ruforecast-train + -- --check + + - name: Prove feature-off graph excludes Burn and CubeCL + working-directory: v2/crates/ruforecast + shell: bash + run: | + set -euo pipefail + : > forecast-feature-off-tree.txt + for package in ruforecast-core ruforecast-model ruforecast-train; do + cargo +1.89.0 tree --locked -e normal,build -p "$package" --no-default-features \ + >> forecast-feature-off-tree.txt + done + if grep -Eiq '(^|[[:space:]])(burn|cubecl)(-|[[:space:]])' forecast-feature-off-tree.txt; then + echo 'Burn/CubeCL entered the Rust 1.89 feature-off dependency graph.' >&2 + cat forecast-feature-off-tree.txt >&2 + exit 1 + fi + + - name: Check feature-off packages + working-directory: v2/crates/ruforecast + run: >- + cargo +1.89.0 check --locked + -p ruforecast-core + -p ruforecast-model + -p ruforecast-train + --no-default-features --all-targets + + - name: Test model-neutral core + working-directory: v2/crates/ruforecast + run: >- + cargo +1.89.0 test --locked + -p ruforecast-core + --no-default-features --lib --tests + + - name: Test model contracts without a model backend + working-directory: v2/crates/ruforecast + run: >- + cargo +1.89.0 test --locked + -p ruforecast-model + --no-default-features --lib --tests + + - name: Test RuVector retrieval boundary without a model backend + working-directory: v2/crates/ruforecast + run: >- + cargo +1.89.0 test --locked + -p ruforecast-model + --no-default-features --features ruvector --lib --tests + + - name: Clippy RuVector retrieval boundary + working-directory: v2/crates/ruforecast + run: >- + cargo +1.89.0 clippy --locked + -p ruforecast-model + --no-default-features --features ruvector --all-targets -- -D warnings + + - name: Clippy forecast packages without backend features + working-directory: v2/crates/ruforecast + run: >- + cargo +1.89.0 clippy --locked + -p ruforecast-core + -p ruforecast-model + -p ruforecast-train + --no-default-features --all-targets -- -D warnings + + - name: Upload feature-off dependency tree + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: ruforecast-feature-off-tree + path: v2/crates/ruforecast/forecast-feature-off-tree.txt + if-no-files-found: error + + hosted-boundaries-rust-189: + name: Forecast hosted boundaries and privacy contract (Rust 1.89) + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: Checkout recursively + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + submodules: recursive + + - name: Install Rust 1.89 with Clippy + uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 + with: + toolchain: 1.89.0 + components: clippy + + - name: Cache Cargo artifacts + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + workspaces: v2/crates/ruforecast + key: ruforecast-hosted-rust-189 + + - name: Enforce clean-room source, secret, and hosted-wire boundary + shell: bash + run: | + set -euo pipefail + python3 - <<'PY' + from pathlib import Path + import re + import sys + + roots = [ + Path("v2/crates/ruforecast/crates/ruforecast-core"), + Path("v2/crates/ruforecast/crates/ruforecast-model"), + Path("v2/crates/ruforecast/crates/ruforecast-train"), + ] + text_suffixes = {".json", ".py", ".rs", ".sh", ".toml", ".yaml", ".yml"} + forbidden_source = re.compile(r"(?i)\b(?:times[ _-]?fm|google[ _/-]?research)\b") + secret_patterns = { + "private key": re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"), + "AWS access key": re.compile(r"\bAKIA[0-9A-Z]{16}\b"), + "JWT": re.compile(r"\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b"), + "presigned URL": re.compile(r"(?i)[?&](?:X-Amz-Signature|X-Goog-Signature)="), + "literal FAL_KEY": re.compile(r"(?i)\bFAL_KEY\b\s*[:=]\s*['\"][^'$<{\"\n]{8,}['\"]"), + } + forbidden_blobs = {".ckpt", ".mpk", ".npy", ".npz", ".onnx", ".pt", ".pth", ".safetensors"} + + errors = [] + for root in roots: + for path in root.rglob("*"): + if not path.is_file() or "target" in path.parts: + continue + if path.suffix.lower() in forbidden_blobs: + errors.append(f"forbidden checked-in model/data blob: {path}") + continue + if path.suffix.lower() not in text_suffixes: + continue + text = path.read_text(encoding="utf-8") + if forbidden_source.search(text): + errors.append(f"forbidden clean-room source/import reference: {path}") + for label, pattern in secret_patterns.items(): + if pattern.search(text): + errors.append(f"possible {label} in {path}") + + fal_source = Path("v2/crates/ruforecast/crates/ruforecast-train/src/fal.rs").read_text() + for struct_name in ("HostedOptimizer", "HostedBudget", "HostedSyntheticPayload"): + match = re.search( + rf"pub struct {struct_name}\s*\{{(?P.*?)^\}}", + fal_source, + flags=re.MULTILINE | re.DOTALL, + ) + if match is None: + errors.append(f"missing hosted wire struct: {struct_name}") + continue + body = match.group("body") + fields = re.findall(r"^\s*pub\s+([A-Za-z0-9_]+)\s*:", body, re.MULTILINE) + forbidden_fields = ( + "account", "data_policy", "dataset", "device", "identity", "path", + "person", "room", "session", "site", "split", "subject", "tenant", + "workspace", + ) + for field in fields: + lowered = field.lower() + if any(fragment in lowered for fragment in forbidden_fields): + errors.append(f"forbidden hosted field {struct_name}.{field}") + if "bytes" in lowered and field not in { + "max_artifact_bytes", + "max_memory_bytes", + }: + errors.append(f"unapproved hosted byte field {struct_name}.{field}") + if "DataPolicy" in body: + errors.append(f"DataPolicy entered hosted wire struct {struct_name}") + + if errors: + print("\n".join(errors), file=sys.stderr) + raise SystemExit(1) + PY + + - name: Enforce tracked-data policy + run: | + set -euo pipefail + bash scripts/csi-data-policy-check.sh --self-test + bash scripts/csi-data-policy-check.sh --tracked + + - name: Validate minimal fal deployment archive policy + run: >- + python3 + v2/crates/ruforecast/crates/ruforecast-train/deploy/fal/deploy.py + self-test + + - name: Prove hosted feature graph excludes Burn and CubeCL + working-directory: v2/crates/ruforecast + shell: bash + run: | + set -euo pipefail + cargo +1.89.0 tree --locked -e normal,build \ + -p ruforecast-train \ + --no-default-features --features cli,server,fal-client \ + > forecast-hosted-feature-tree.txt + if grep -Eiq '(^|[[:space:]])(burn|cubecl)(-|[[:space:]])' \ + forecast-hosted-feature-tree.txt; then + echo 'Burn/CubeCL entered the Rust 1.89 hosted dependency graph.' >&2 + cat forecast-hosted-feature-tree.txt >&2 + exit 1 + fi + + - name: Require named hosted boundary regressions + working-directory: v2/crates/ruforecast + shell: bash + env: + FAL_KEY: '' + run: | + set -euo pipefail + cargo +1.89.0 test --locked \ + -p ruforecast-train \ + --no-default-features --features cli,server,fal-client \ + --lib --bins --tests -- --list \ + | tee hosted-boundary-tests.txt + for regression in \ + fal_app_rejects_path_and_host_confusion \ + fal_urls_require_exact_origin_path_and_no_query \ + fal_submit_headers_disable_retry_and_omit_store_io \ + hosted_payload_dto_has_no_customer_fields \ + queue_status_discards_provider_error_body \ + result_url_accepts_exact_response_suffix \ + artifact_file_url_uses_expected_api_path \ + fal_key_debug_is_redacted \ + hosted_payload_rejects_app_field \ + hosted_reservation_cannot_exceed_source_retention \ + artifact_handoff_expiry_boundary_is_fail_closed \ + fal_result_enforces_cumulative_artifact_budget_boundary \ + fal_download_rejects_over_budget_without_writing \ + direct_server_train_requires_request_id_header \ + request_id_rejects_path_injection \ + unknown_cancel_is_not_success \ + privacy_external_dataset_payload_is_denied; do + grep -q "$regression" hosted-boundary-tests.txt \ + || { echo "Missing hosted boundary regression: $regression" >&2; exit 1; } + done + + - name: Exercise hosted external-data rejection + working-directory: v2/crates/ruforecast + env: + FAL_KEY: '' + run: >- + cargo +1.89.0 test --locked + -p ruforecast-train + --no-default-features --features cli,server,fal-client + privacy_external_dataset_payload_is_denied + + - name: Test server and fal client with mock integrations + working-directory: v2/crates/ruforecast + env: + FAL_KEY: '' + run: >- + cargo +1.89.0 test --locked + -p ruforecast-train + --no-default-features --features cli,server,fal-client + --lib --bins --tests + + - name: Clippy server and fal client boundaries + working-directory: v2/crates/ruforecast + env: + FAL_KEY: '' + run: >- + cargo +1.89.0 clippy --locked + -p ruforecast-train + --no-default-features --features cli,server,fal-client + --all-targets -- -D warnings + + - name: Clippy standalone CLI feature boundary + working-directory: v2/crates/ruforecast + env: + FAL_KEY: '' + run: >- + cargo +1.89.0 clippy --locked + -p ruforecast-train + --no-default-features --features cli + --bin ruforecast -- -D warnings + + - name: Upload hosted dependency tree + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: ruforecast-hosted-feature-tree + path: v2/crates/ruforecast/forecast-hosted-feature-tree.txt + if-no-files-found: error + + burn-cpu-rust-192: + name: Forecast Burn CPU tests and bench compile (Rust 1.92) + runs-on: ubuntu-latest + timeout-minutes: 90 + steps: + - name: Checkout recursively + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + submodules: recursive + + - name: Install Rust 1.92 with Clippy + uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 + with: + toolchain: 1.92.0 + components: clippy + + - name: Cache Cargo artifacts + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + workspaces: v2/crates/ruforecast + key: ruforecast-rust-192-cpu + + - name: Test model with Burn CPU backend + working-directory: v2/crates/ruforecast + run: >- + cargo +1.92.0 test --locked + -p ruforecast-model + --no-default-features --features cpu --lib --tests + + - name: Test trainer and CLI with Burn CPU backend + working-directory: v2/crates/ruforecast + run: >- + cargo +1.92.0 test --locked + -p ruforecast-train + --no-default-features --features cpu,cli --lib --bins + + - name: Run one-step local JSONL training smoke + working-directory: v2/crates/ruforecast + run: >- + cargo +1.92.0 test --locked + -p ruforecast-train + --no-default-features --features cpu + --test local_jsonl_smoke -- + --exact local_hash_addressed_jsonl_executes_one_real_optimizer_step + + - name: Run idempotent synthetic CLI training smoke + working-directory: v2/crates/ruforecast + run: >- + cargo +1.92.0 test --locked + -p ruforecast-train + --no-default-features --features cpu,cli + --test cli_smoke -- + --exact cli_smoke_trains_and_writes_the_complete_candidate_set + + - name: Test combined CPU and hosted feature surface + working-directory: v2/crates/ruforecast + env: + FAL_KEY: '' + run: >- + cargo +1.92.0 test --locked + -p ruforecast-train + --no-default-features --features cpu,cli,server,fal-client + --lib --bins + + - name: Clippy model with Burn CPU backend + working-directory: v2/crates/ruforecast + run: >- + cargo +1.92.0 clippy --locked + -p ruforecast-model + --no-default-features --features cpu --all-targets -- -D warnings + + - name: Clippy trainer and CLI with Burn CPU backend + working-directory: v2/crates/ruforecast + env: + FAL_KEY: '' + run: >- + cargo +1.92.0 clippy --locked + -p ruforecast-train + --no-default-features --features cpu,cli,server,fal-client + --all-targets -- -D warnings + + - name: Compile forecast inference benchmark + working-directory: v2/crates/ruforecast + run: >- + cargo +1.92.0 bench --locked + -p ruforecast-model + --no-default-features --features cpu + --bench forecast_inference --no-run + + - name: Compile forecast data-pipeline benchmark + working-directory: v2/crates/ruforecast + run: >- + cargo +1.92.0 bench --locked + -p ruforecast-train + --no-default-features --features cpu + --bench data_pipeline --no-run + + - name: Run quick inference benchmark for trend visibility + continue-on-error: true + timeout-minutes: 15 + working-directory: v2/crates/ruforecast + run: | + set -o pipefail + mkdir -p ../../bench-out/ruforecast + cargo +1.92.0 bench --locked \ + -p ruforecast-model \ + --no-default-features --features cpu \ + --bench forecast_inference -- \ + --warm-up-time 1 --measurement-time 2 --sample-size 10 \ + | tee ../../bench-out/ruforecast/forecast-inference.txt + + - name: Run quick data-pipeline benchmark for trend visibility + continue-on-error: true + timeout-minutes: 15 + working-directory: v2/crates/ruforecast + run: | + set -o pipefail + mkdir -p ../../bench-out/ruforecast + cargo +1.92.0 bench --locked \ + -p ruforecast-train \ + --no-default-features --features cpu \ + --bench data_pipeline -- \ + --warm-up-time 1 --measurement-time 2 --sample-size 10 \ + | tee ../../bench-out/ruforecast/data-pipeline.txt + + - name: Upload informational benchmark logs + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: ruforecast-informational-cpu-benchmarks + path: bench-out/ruforecast/ + if-no-files-found: warn + + burn-cuda-compile-rust-192: + name: Forecast Burn CUDA/WGPU compile checks only (Rust 1.92) + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout recursively + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + submodules: recursive + + - name: Install Rust 1.92 + uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 + with: + toolchain: 1.92.0 + + - name: Cache Cargo artifacts + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + workspaces: v2/crates/ruforecast + key: ruforecast-rust-192-cuda-check + + - name: Check model CUDA feature + working-directory: v2/crates/ruforecast + run: >- + cargo +1.92.0 check --locked + -p ruforecast-model + --no-default-features --features cuda --lib + + - name: Check model WGPU feature + working-directory: v2/crates/ruforecast + run: >- + cargo +1.92.0 check --locked + -p ruforecast-model + --no-default-features --features wgpu --lib + + - name: Check trainer CUDA and hosted-server feature combination + working-directory: v2/crates/ruforecast + env: + FAL_KEY: '' + run: >- + cargo +1.92.0 check --locked + -p ruforecast-train + --no-default-features --features cuda,cli,server + --lib --bins diff --git a/.gitmodules b/.gitmodules index 39ef969a3..312c1b9bf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -33,3 +33,7 @@ path = vendor/metaharness url = https://github.com/ruvnet/metaharness branch = main +[submodule "v2/crates/ruforecast"] + path = v2/crates/ruforecast + url = https://github.com/ruvnet/RuForecast.git + branch = main diff --git a/docs/adr/ADR-348-independent-rust-multivariate-forecasting.md b/docs/adr/ADR-348-independent-rust-multivariate-forecasting.md new file mode 100644 index 000000000..50272fe23 --- /dev/null +++ b/docs/adr/ADR-348-independent-rust-multivariate-forecasting.md @@ -0,0 +1,336 @@ +# ADR-348: Independent Rust multivariate forecasting for RuView + +- **Status**: Proposed +- **Date**: 2026-09-01 +- **Deciders**: ruv +- **Owners**: RuView perception, model, security, and data-governance maintainers +- **Tags**: forecasting, rust, ruvector, temporal, uncertainty, clean-room, provenance +- **Extends**: ADR-016, ADR-020, ADR-145, ADR-273, ADR-282, ADR-295, + ADR-298, ADR-302, ADR-304, ADR-317, ADR-318, ADR-319 +- **Supersedes**: None + +## Executive decision + +RuView will develop an independently specified and independently trained Rust +multivariate forecasting subsystem. It will forecast bounded, versioned +temporal feature streams, publish calibrated quantiles and an explicit +abstention state, optionally use a split-safe RuVector analogue index, and +remain advisory until all release gates in this ADR pass. + +This is not a port, compatibility layer, distillation, or behavioural clone of +Google TimesFM. The implementation API is derived from RuView requirements. +Its weights must descend from an approved random initialization and approved +training data only. + +**Evidence status at proposal:** the forecasting accuracy, false-alert +reduction, calibration, CPU latency, memory use, cross-building generalization, +and operational value described below are all **UNMEASURED targets**. This ADR +authorizes implementation and evaluation; it makes no `MEASURED` capability +claim and approves no production model. + +## Context + +RuView currently observes present and recent RF state. Forecasting could add a +separate answer to questions such as whether a feature trajectory is consistent +with real occupancy, whether motion is likely to transition between zones, or +whether a radio link is degrading. A forecast is not a sensor observation. It +is derived evidence with uncertainty and must never overwrite the immutable +observation that produced it. + +TimesFM 3 is useful research context because its public description discusses +multivariate targets, past covariates, known-future covariates, probabilistic +outputs, temporal patching, and cross-series attention. Its licensing creates +two distinct surfaces: + +- the Google TimesFM source repository states that source code is Apache-2.0; +- the TimesFM 3 pretrained weights are distributed under a separate + non-commercial, non-production license that also restricts commercial + training, fine-tuning, and distillation from the model. + +An Apache-compliant Rust port would be a permissible but attributed derivative +source path, subject to its exact license obligations. It would not support the +strongest independent-development claim. Using the restricted TimesFM 3 model +or its outputs to create a RuView commercial model is outside this ADR. The +project therefore chooses the stricter clean-room protocol in +[`../security/ruview-forecast-clean-room.md`](../security/ruview-forecast-clean-room.md). + +Clean-room controls reduce copyright, contract, and provenance risk. They do +not establish freedom to operate against patents, clear trademarks, approve a +dataset, or replace legal review in a launch jurisdiction. + +## Options considered + +1. **Use the TimesFM 3 checkpoint in RuView.** Rejected for commercial and + production use under the current checkpoint license. +2. **Translate the Apache-licensed implementation into Rust.** Not selected. + This can be evaluated under a separate ADR, with attribution and license + compliance, but must not be mixed with the independent-development path. +3. **Build an independent Rust forecasting specialist for RuView.** Chosen. + This narrows model scope, keeps inference native, controls provenance, and + makes RF-specific evaluation the source of authority. +4. **Do not add forecasting.** Retained as the deployment baseline. Forecasting + must beat simpler deterministic and statistical baselines before acquiring + runtime authority. + +## Requirements + +| ID | Requirement | Verification authority | +|---|---|---| +| RF-001 | No TimesFM 3 code, configuration, weights, parameters, outputs, activations, or derivatives enter specification, implementation, training, evaluation feedback, or release artifacts. | Clean-room manifest, contributor attestations, repository and artifact scans | +| RF-002 | Every contributor, reference, dependency, dataset, transform, training job, and checkpoint has immutable provenance. | Signed provenance manifests and review receipts | +| RF-003 | Every training byte has documented rights for commercial machine-learning use and applicable privacy approval. | Dataset licensing gate and data-steward approval | +| RF-004 | The default runtime is native Rust, bounded, offline, deterministic for a fixed artifact/input within the declared platform class, and free of runtime model download. | Unit, property, fuzz, replay, and dependency tests | +| RF-005 | Inputs and outputs are versioned, finite, bounded, timestamped, mask-aware, and provenance-labelled. Invalid, stale, insufficient, or OOD inputs abstain. | Contract tests and malformed-input corpus | +| RF-006 | Forecasts remain derived evidence. They cannot rewrite observations, silently increase observation confidence, or be relabelled `MEASURED`. | Schema invariants and evidence-engine tests | +| RF-007 | Quantile ordering, interval calibration, missing-data behaviour, and abstention are evaluated on leakage-free site/session/device holdouts. | Frozen evaluation manifest and reproducible report | +| RF-008 | RuVector retrieval is optional, split-scoped, provenance-recorded, and evaluated against the same model without retrieval. | Retrieval isolation tests and paired ablation | +| RF-009 | RuVLLM may explain or summarize a signed forecast but cannot alter numeric forecasts, bypass policy, spend money, or trigger medical, emergency, access-control, or life-safety action. | Capability tests and downstream policy review | +| RF-010 | Training on local Linux or a hosted accelerator uses identical signed code, container, data, and configuration identities; the provider is an untrusted processor. | Training receipts and digest comparison | +| RF-011 | Accuracy, latency, memory, power, and operational claims remain `UNMEASURED` or `CLAIMED` until a named reproducer satisfies the repository evidence contract. | Documentation and release review | +| RF-012 | Rollout is reversible and cannot advance from offline evaluation to shadow or advisory operation without the mapped gates below. | Signed mode transition and rollback drill | + +## Architecture boundary + +The initial implementation is split across three crates: + +| Crate | Owned responsibility | Dependency/runtime rule | +|---|---|---| +| `ruview-forecast-core` | Backend-neutral schemas, invariants, metrics, forecast receipts, and the `Forecaster` trait | No Burn, CUDA, WGPU, provider SDK, sensing-server, or network dependency | +| `ruview-forecast-model` | Independent Burn 0.21 patch-mixer architecture and artifact execution | CPU, CUDA, and WGPU are explicit optional features; every backend feature is off by default | +| `ruview-forecast-train` | Dataset manifests/splits, trainer, evaluator, the `ruforecast` CLI, and Linux/fal.ai training assets | Training-only authority; no production activation or sensing-server mutation | + +This PR does not connect the forecaster to the sensing server. A shadow bridge +requires a follow-up change after the core contract and evidence receipt have +stabilized. Landing crates or passing synthetic tests therefore creates no +live RuView forecasting capability. + +Version one activates only the exact reviewed `tiny_ci` and `large_linux` +architecture profiles. The model and training boundaries enforce checked +dimension, parameter, activation-cell, input-cell, and forward multiply-add +limits per batch. Adding or changing a profile is therefore a reviewed code and +artifact-schema decision, not untrusted request configuration. + +The proposed logical pipeline is: + +```text +authenticated RF observations + | + v +versioned one-second feature windows + | + +----> split-scoped RuVector analogue retrieval (optional) + | + v +independent Rust temporal model + | + v +point forecast + ordered quantiles + validity mask + abstention + | + v +signed derived-evidence record + | + +----> RuView policy/evidence engine + +----> RuVLLM explanation with no numeric mutation authority +``` + +The initial feature contract should favour compact one-second summaries rather +than feeding an unbounded raw CSI stream. Candidate fields include motion +energy, Doppler summary, amplitude/phase dispersion, coherence, RSSI, packet +loss, channel utilization, current detector confidence, device temperature, +and source-validity masks. Each field needs a schema version, physical unit, +aggregation rule, validity semantics, and provenance. Adding a field is a +schema change, not an implicit positional extension. + +The model may independently combine normalization, temporal patch encoding, +temporal mixing, cross-stream fusion, and quantile heads. This is a functional +design space, not permission to reproduce protected source expression, +checkpoint dimensions, constants, tests, diagrams, or API choices. Exact +architecture and parameter count remain implementation decisions recorded in +the model card and training receipt. + +## Forecast contract + +For each request the runtime returns one typed result, including abstention and +error paths. The result must identify: + +- input schema, feature-window, model, configuration, and calibration digests; +- source time range, requested horizon, cadence, target streams, and masks; +- point estimate and declared quantiles for each target/horizon cell; +- whether quantiles were corrected for crossing and which method was used; +- OOD, insufficient-context, stale-input, and non-finite dispositions; +- optional RuVector index version, neighbour identifiers, distances, and + leakage-scope receipt; +- runtime platform class, duration, and peak-memory measurement when enabled; +- evidence label `DERIVED_FORECAST`, never `MEASURED_OBSERVATION`; +- deterministic content hash and optional RVF signature. + +The runtime rejects non-finite values, duplicate or non-monotonic timestamps, +unsupported schema versions, unbounded dimensions, invalid quantile requests, +future covariates without an allowed source, and payloads above configured +limits. It abstains rather than imputing an authoritative state when history or +validity coverage is below the model-card threshold. + +## RuVector integration + +RuVector supplies temporal memory, not ground truth. During training and +evaluation, each split has a separate index built only from records permitted +for that split. A test query may retrieve training analogues, but it may never +retrieve a window from the same subject/session/site holdout, an overlapping +target horizon, or any test record. Fitted normalizers and retrieval thresholds +come from training data only. + +Every release reports three comparable rows on identical examples: + +1. deterministic/statistical baseline; +2. forecasting model without retrieval; +3. forecasting model with RuVector retrieval. + +This prevents retrieval from hiding a weak model or leaking future state. + +## RuVLLM and action authority + +RuVLLM consumes an immutable, signed forecast record and may produce an +explanation referencing its uncertainty and provenance. Its prose is not the +forecast and receives no higher evidence grade. Numeric values exposed to APIs +come from the forecasting record, not regenerated text. + +No forecast or LLM explanation independently triggers a health alert, emergency +response, access decision, actuator, firmware change, model promotion, or +commercial spend. Such actions require a separate governed policy, explicit +capability, and appropriate observed evidence. + +## Training and artifact boundary + +Training jobs run from a pinned container or reproducible local environment. +The receipt binds the source commit, lockfile, compiler, container, datasets, +transforms, architecture, hyperparameters, seeds, hardware, provider job ID, +start/end time, and every emitted checkpoint. Hosted workers receive only the +least data and credentials necessary. Provider caches, logs, retention, and +reuse rights must pass security and data review before customer-derived data is +uploaded. + +A release artifact is immutable and hash addressed. It is loaded locally after +signature, schema, size, and compatibility verification. The inference process +does not fetch models, execute embedded code, accept arbitrary operators, or +contact the training provider. + +## Leakage and evaluation protocol + +- Split by deployment/site first, then subject, session, device, and contiguous + time block as applicable. No overlapping raw sequence or derived window may + cross a split. +- Freeze test manifests before tuning. A failed test result does not become a + new training target; material architecture changes require a new untouched + holdout. +- Fit normalization, calibration, feature selection, thresholds, and RuVector + index parameters on training/validation data only. +- Report missingness, abstention coverage, selective risk, per-horizon errors, + interval coverage, quantile loss, and results by site/device/interference + regime. Pooled performance cannot hide a failed domain. +- Compare against last value, seasonal naive, and at least one small classical + or recurrent baseline. TimesFM 3 output is not an acceptance oracle. +- Forecast evaluation does not prove presence, pose, fall, respiration, or + medical accuracy. Any downstream claim requires its own labelled protocol. + +## Acceptance gates and requirement mapping + +Targets below are release criteria, not current results. + +| Gate | Requirements | Pass condition | Current state | +|---|---|---|---| +| G0 independent-authoring boundary | RF-001, RF-002 | Approved source allowlist; 100% current contributor attestations; zero restricted artifacts or outputs; all similarity findings adjudicated by the clean-room custodian. | **OPEN / UNMEASURED** | +| G1 data and lineage | RF-002, RF-003, RF-010 | 100% of training/evaluation bytes resolve to approved manifests; zero unknown, noncommercial, research-only, no-ML, or no-derivatives sources; checkpoint lineage reaches approved random initialization; local/hosted receipts bind identical governed inputs. | **OPEN / UNMEASURED** | +| G2 bounded Rust contract | RF-004, RF-005, RF-006 | Unit/property/fuzz tests cover dimensions, non-finite values, timestamp order, masks, quantile order, stale/OOD/insufficient context, deterministic hashes, offline loading, evidence labels, and resource caps; 24-hour accelerated replay has no panic or unbounded growth. | **OPEN / UNMEASURED** | +| G3 leakage-free model evidence | RF-007, RF-008, RF-011 | Frozen site/session/device-disjoint report; all baseline and ablation rows present; nominal 80% interval coverage target is 75%-85%; weighted quantile loss target is at least 10% better than seasonal naive; every metric has a reproducer. | **OPEN / TARGETS UNMEASURED** | +| G4 RuView shadow value | RF-006, RF-007, RF-008, RF-009 | At least 14 days shadow-only; empty-room false-alert target is at least 50% relative reduction without more than 2 percentage points occupied-room recall loss; no safety-critical action authority; drift and abstention reported by deployment. | **OPEN / TARGETS UNMEASURED** | +| G5 deployment fitness | RF-004, RF-009, RF-010, RF-011, RF-012 | CPU p95 target is at most 1 second for 32 declared streams with peak process memory at most 4 GiB; signed model card/SBOM/provenance; privacy, security, trademark, dataset, and patent reviews; rollback drill; no open severity 1/2 issue. | **OPEN / TARGETS UNMEASURED** | + +Failure at any gate keeps the model offline or shadow-only. Passing a software +gate is not real-hardware evidence and does not convert an accuracy target into +a measurement. + +## Rollout and rollback + +The only permitted progression is: + +```text +OFF -> OFFLINE_EVAL -> SHADOW -> ADVISORY +``` + +This ADR does not authorize autonomous action. Each transition records actor, +old/new mode, artifact and configuration digests, evidence report, and reason. +Regression, provenance failure, calibration drift, security incident, or +licensing uncertainty returns immediately to `OFF` or `SHADOW` while observed +RuView sensing continues unchanged. + +Rollback deactivates the forecast artifact atomically, clears ephemeral model +and retrieval state, preserves signed aggregate evidence and incident records, +and never downgrades or rewrites the raw observation stream. + +## Security and privacy + +Forecasting can infer routines from occupancy and movement even without raw +CSI. Feature windows, neighbour identifiers, forecasts, and explanations are +therefore deployment data subject to purpose limitation, tenant isolation, +retention, deletion, access control, and audit. + +Model artifacts, feature schemas, calibration, indexes, and manifests are +untrusted until their signature and digest verify. Cardinality, dimensions, +horizon, context, allocations, execution time, and concurrent requests are +bounded. Metrics use allowlisted aggregate values and exclude raw CSI, precise +room coordinates, persistent person identifiers, and unrestricted feature +payloads. + +The detailed authoring, data, AI-tool, incident, trademark, and patent controls +are normative in +[`../security/ruview-forecast-clean-room.md`](../security/ruview-forecast-clean-room.md). + +## Consequences + +### Positive + +- RuView gains a provider-neutral predictive evidence primitive with explicit + uncertainty and abstention. +- Rust-native inference can be evaluated on CPU without requiring a Python or + cloud runtime in production. +- RuVector retrieval becomes a measurable ablation rather than an implicit + memory claim. +- Signed provenance supports reproducible Linux and hosted training. + +### Negative + +- Independent training needs substantial diverse, correctly licensed temporal + data and untouched deployment holdouts. +- Strict source separation and manifests increase contributor and review cost. +- A plausible forecast can make weak sensing look more authoritative unless + downstream evidence labels remain intact. +- Clean-room development does not remove patent, trademark, privacy, or dataset + risk. + +### Neutral + +- This ADR does not repair an inaccurate upstream presence or pose estimator. +- It does not approve TimesFM 3 for RuView use. +- It does not select a final model size, training budget, or hosted provider. +- No production checkpoint or measured benchmark is created by this decision. + +## References + +- [TimesFM 3 public research article](https://research.google/blog/timesfm-3-a-zero-shot-foundation-model-for-multivariate-forecasting/) +- [Google TimesFM repository and source-license notice](https://github.com/google-research/timesfm) +- [TimesFM 3 checkpoint license](https://huggingface.co/google/timesfm-3.0-pytorch/blob/main/LICENSE) +- [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) +- [17 U.S.C. section 102](https://www.copyright.gov/title17/92chap1.html) +- [USPTO Patent Public Search](https://www.uspto.gov/patents/search/patent-public-search) +- [USPTO comprehensive trademark clearance](https://www.uspto.gov/trademarks/search/comprehensive-clearance-search-similar-trademarks) +- [RuView Forecast clean-room protocol](../security/ruview-forecast-clean-room.md) +- [RuView Forecast model-card template](../huggingface/RUVIEW_FORECAST_MODEL_CARD_TEMPLATE.md) +- [ADR-145](./ADR-145-ablation-eval-harness-privacy-leakage.md) +- [ADR-282](./ADR-282-ruview-ecosystem-positioning.md) +- [ADR-295](./ADR-295-source-provenance-state-machine.md) +- [ADR-298](./ADR-298-model-release-sanity-gates.md) +- [ADR-302](./ADR-302-out-of-distribution-detection.md) +- [ADR-304](./ADR-304-evidence-engine.md) +- [ADR-317](./ADR-317-benchmark-multi-domain-scorecard.md) +- [ADR-318](./ADR-318-capability-certificates.md) +- [ADR-319](./ADR-319-witness-chain.md) diff --git a/docs/adr/ADR-349-governed-local-and-fal-forecast-training.md b/docs/adr/ADR-349-governed-local-and-fal-forecast-training.md new file mode 100644 index 000000000..caafd240f --- /dev/null +++ b/docs/adr/ADR-349-governed-local-and-fal-forecast-training.md @@ -0,0 +1,231 @@ +# ADR-349: Governed local and fal.ai forecast training + +- **Status**: Proposed +- **Date**: 2026-09-01 +- **Deciders**: ruv +- **Owners**: RuView forecast training, security, and release maintainers +- **Tags**: forecasting, training, fal, rust, receipts, idempotency, cost-control +- **Parent**: ADR-348 +- **Extends**: ADR-010, ADR-145, ADR-298, ADR-319, ADR-348 +- **Supersedes**: None + +## Decision + +RuView Forecast will use one Rust training engine behind two deliberately +different request boundaries. The local Linux request may bind a governed, +hash-addressed RuView JSONL shard. Hosted v1 accepts only a bounded, +deterministic synthetic generator recipe. It cannot serialize a local +`TrainSpec`, `DataPolicy`, path, dataset bytes, tenant/account/workspace/site/ +device/session identity, split receipt, or RuVector namespace. + +The fal.ai path is a bounded execution adapter, not a remote shell. It may +train the fixed synthetic profile and export fixed artifact kinds, but it may +not choose an arbitrary command, executable, container, URL, environment +variable, output path, or release action. A later real-data hosted path needs a +new accepted ADR, provider/privacy review, and an operator-signed export grant; +it is intentionally not implemented here. + +Every hosted result is untrusted and quarantined until it is exported with a +complete receipt, downloaded through the authenticated provider channel, +re-hashed locally, scanned, and bound to the original request. Only the local +release boundary may sign or activate it. Signing keys never enter fal.ai. + +This ADR delegates dataset rights, clean-room evidence, model-card, patent, +trademark, and production gates to [ADR-348](./ADR-348-independent-rust-multivariate-forecasting.md) +and the [clean-room protocol](../security/ruview-forecast-clean-room.md). + +**Evidence status:** typed local and hosted configuration, root-confined file +verification, atomic fixed-kind artifact publication, and cooperative +cancellation source surfaces exist. End-to-end local training and mock hosted +transport are software-testable in this change. Real fal.ai execution, +provider cancellation, cost reconciliation, quarantine promotion, and local +signature verification remain **UNMEASURED and unapproved** until their named +acceptance evidence exists. + +## Context + +The same training engine needs to run on a 128 GiB Linux workstation and on an +optional hosted accelerator without creating two architecture lineages. A +generic remote-command interface would allow request data to become executable +authority, make cost difficult to bound, and weaken cancellation. Sending a +general local request to fal would also expose governance and customer +identifiers that the hosted smoke path does not need. + +The initial `ruview-forecast-train` crate already separates three relevant +modules: + +| Module | Current contract | +|---|---| +| [`config.rs`](../../v2/crates/ruview-forecast-train/src/config.rs) | `TrainingRequest`, `ValidatedTrainingRequest`, bounded `JobId`, root-relative dataset path, exact size/SHA-256, named model profiles, typed CPU/CUDA choice, and bounded optimizer values; unknown fields are denied | +| [`artifact.rs`](../../v2/crates/ruview-forecast-train/src/artifact.rs) | verified open dataset handle, root confinement, fixed `Model`/`Manifest`/`Receipt`/`Checkpoint` kinds, atomic writes, content descriptors, and conflict-on-different-bytes idempotency | +| [`cancel.rs`](../../v2/crates/ruview-forecast-train/src/cancel.rs) | `Cancellation`, `CancelToken`, and cooperative cancellation checks for batch/checkpoint boundaries | + +The training crate features keep `cli`, `server`, `fal-client`, `cpu`, and +`cuda` explicit, with default features empty. WGPU remains a model-crate +inference backend and is not advertised as a training runner. The `ruforecast` +binary requires the `cli` feature. Source presence is not execution evidence. + +## Typed execution contracts + +The non-serializable trusted `TrainingRequest` is built locally from a +size-bounded `LocalTrainingRequestWire`. It contains: + +- stable job identity; +- a constructor-validated local split/horizon/normalization `TrainSpec`; +- either a root-relative dataset identity with exact byte count and SHA-256, + or a deterministic synthetic recipe; +- one allowlisted model capacity profile and compiled-in local backend; +- bounded optimizer, checkpoint cadence, seed, memory, step, time, checkpoint, + and artifact budgets. + +Local JSON and TOML deny unknown fields. Dataset paths resolve below an +operator-configured root, and the exact regular file remains open after +size/hash verification to avoid replacement between validation and use. JSONL +windows are decoded incrementally with per-line and per-window caps rather +than loading the full shard into memory. + +The distinct `HostedSyntheticRequestV1` contains only a schema version, +idempotency/request digests, a fixed model profile, deterministic generator +parameters and seed, bounded optimizer/resource caps, and public build +identities. Its constructor needs a non-serializable core governance authority +proving that the synthetic recipe is the approved hosted source. Hosted v1 has +no external dataset field. + +Neither request contains a shell string, argument vector, executable path, +container/image selector, package installer, arbitrary URL, callback URL, +environment map, secret, source fragment, or unrestricted output name. + +The local client maps `HostedSyntheticRequestV1` to one allowlisted, +process-configured fal app and pre-deployed content-addressed worker image. The +caller cannot override the app, machine, worker entry point, or artifact +destination. The endpoint returns typed status or artifact descriptors only. +Provider paths are derived from validated job identity and fixed artifact +kinds, then downloaded through the authenticated fal Platform Files API; they +are not accepted as remote URLs or local paths. + +The deploy wrapper builds from an allowlisted `git archive`, selects the exact +`fal_app.py::run_server` symbol, and passes `--auth private` for both `fal run` +and `fal deploy`; ephemeral Fal runs otherwise default to public. Hosted v1 +retains only bounded synthetic request/result metadata so its typed queue result +can be reconciled. It sets `X-Fal-No-Retry: 1` and a bounded timeout rather than +claiming that `X-Fal-Store-IO: 0` or an unverified lifecycle header protects +the result. A live unauthenticated 401/403 probe and provider retention/deletion +reconciliation remain blocking operational evidence. + +## Idempotency and cost authority + +The local effective idempotency identity is: + +```text +job_id + canonical request digest + source commit + lockfile digest ++ container digest + dataset/split digest + initial-weight digest +``` + +Hosted v1 replaces `dataset/split digest` with the canonical synthetic-recipe +digest. No local governance identifier is part of, or derivable from, the +hosted payload. + +Repeating an identical completed request returns the existing verified receipt +and descriptors. Reusing `job_id` with any different governed input fails as a +conflict. A retry after an ambiguous network failure must first query the same +provider job; it must not silently create a second billable run. + +Before hosted submission, the caller sets explicit maximum steps, wall-clock +time, memory, checkpoint count, export bytes, and billable units. Submission +also requires an explicit local spend-approval record bound to the request +digest and maximum units. The adapter rejects a request when its configured +cap exceeds that approval. The final receipt records estimated and actual +provider units/cost when the provider supplies them, the price source/time, +and whether the amount is final or provider-reported. A provider estimate is +`CLAIMED`, never `MEASURED`, until reconciled against a bill. + +No automatic retry may increase the approved budget. Budget changes create a +new signed authorization record. The initial wire carries operator ceilings +for wall time, billable seconds, and micro-USD, but it does not yet bind a +provider price quote or reconcile a final provider ledger. Monetary enforcement +therefore remains an operator/Fal-account control and the production hosted +spend gate stays open. + +## Cancellation and checkpoints + +Cancellation is cooperative and idempotent: + +1. Local signal or authenticated hosted cancel marks one `job_id` cancelled. +2. Training checks `Cancellation` at every batch boundary and before/after each + checkpoint/export boundary. +3. A cancellation checkpoint is committed atomically when safe and within the + artifact budget; a partial file never becomes a valid artifact. +4. The terminal receipt records `cancelled`, the last completed epoch/step, + checkpoint digest, provider state, elapsed resource units, and final known + cost. +5. Repeating cancel returns the same terminal state. + +Cancellation does not promise immediate GPU termination. Cancellation latency, +checkpoint durability, and residual provider billing are **UNMEASURED** until a +real fal.ai run supplies a receipt. A timeout or lost cancellation response +keeps the job and any output quarantined. + +## Export, quarantine, and local signing + +The worker may emit only the fixed artifact set from `ArtifactKind`: + +| Kind | Required purpose | +|---|---| +| `Model` | Burn model record bytes | +| `Manifest` | architecture, data/split, seed, build, and clean-room identity | +| `Receipt` | request, environment, metrics, cost, status, and export lineage | +| `Checkpoint` | final or cancellation-time model weights; v1 does not claim optimizer/cursor resume | + +Every descriptor includes kind, size, and SHA-256. Export acceptance requires +all mandatory descriptors, bounded lengths, exact hashes, the expected job and +request digest, no duplicate kind, and a terminal status consistent with the +artifact set. + +Downloaded files enter a non-executable quarantine outside the model search +path. Local verification repeats envelope/schema/size/hash checks, SBOM and +malware/policy scans, clean-room declarations, dataset/split lineage, metric +labels, and parent-checkpoint validation. No provider output may update a +symlink, current-model pointer, RuVector index, sensing server, or release tag. + +After quarantine passes, an authorized local signer binds the exact export +receipt and artifact digests to an `ArtifactReceipt` and release manifest. The +signature records algorithm, public key ID, signer capability, and time. Fal.ai +receives no private signing material and cannot promote its own output. The +current core supplies canonical digests and receipts, not a complete release +signature implementation; signature-backed activation remains open. + +## Requirements and acceptance + +| ID | Requirement | ADR-348 gate | Acceptance evidence | Current state | +|---|---|---|---|---| +| FT-001 | Local and hosted adapters invoke the same model/trainer implementation while using intentionally separate local-data and hosted-synthetic request schemas. | G1 | Shared-engine tests plus hosted DTO exclusion and recipe-digest binding tests | **OPEN / UNMEASURED** | +| FT-002 | No request field or endpoint can select arbitrary code, command, image, URL, environment, or output path. | G2 | Schema negatives, endpoint capability test, dependency review | **OPEN / UNMEASURED** | +| FT-003 | Job/request idempotency prevents duplicate execution and conflicts on changed governed input. | G1, G5 | Concurrent/retry/lost-response tests with provider job query | **OPEN / UNMEASURED** | +| FT-004 | Hosted execution has explicit non-escalating resource and monetary caps with estimated/actual cost receipts. | G5 | Over-budget rejection and reconciled provider-bill fixture plus real-run receipt | **OPEN / UNMEASURED** | +| FT-005 | Cancellation is authenticated, cooperative, idempotent, checkpoint-safe, and terminally receipted. | G2, G5 | Local property tests and a real hosted cancellation drill | **OPEN / UNMEASURED** | +| FT-006 | Export is fixed-kind, bounded, complete, hash-verified, and bound to request/job/environment. | G1, G2 | Missing/duplicate/truncated/tampered export tests | **OPEN / UNMEASURED** | +| FT-007 | Hosted outputs remain quarantined until local verification and local-only signing succeed. | G1, G5 | Promotion-denial tests, signing-key absence check, rollback drill | **OPEN / UNMEASURED** | +| FT-008 | Hosted v1 receives synthetic inputs only; provider credentials, logs, retention, region, output reuse, and deletion satisfy ADR-348 privacy/security approval before any real run. | G1, G5 | Hosted DTO exclusion tests, provider review, and deletion/export receipts | **OPEN / UNMEASURED** | + +No successful unit test closes a hosted gate. G5 requires one bounded real +fal.ai run, one cancellation drill, one ambiguous-retry/idempotency drill, cost +reconciliation, quarantine rejection of a tampered export, and local signature +verification over the accepted artifact. Until then fal.ai support is a typed +software surface, not an operational capability claim. + +## Consequences + +The design gives local and hosted training one auditable lineage and keeps +provider output below local release authority. It adds receipt, budget, +quarantine, and signing work, and cooperative cancellation may still incur +provider cost. If a hosted provider cannot support bounded idempotent status, +export, cancellation, and deletion, the Linux path remains the only approved +training environment. + +## References + +- [ADR-348](./ADR-348-independent-rust-multivariate-forecasting.md) +- [RuView Forecast clean-room protocol](../security/ruview-forecast-clean-room.md) +- [`ruview-forecast-train` manifest](../../v2/crates/ruview-forecast-train/Cargo.toml) +- [`ruview-forecast-core` receipts](../../v2/crates/ruview-forecast-core/src/receipt.rs) +- [`ruview-forecast-model` public artifact boundary](../../v2/crates/ruview-forecast-model/src/lib.rs) diff --git a/docs/adr/ADR-350-ruvector-predictive-memory-and-ruvllm-boundary.md b/docs/adr/ADR-350-ruvector-predictive-memory-and-ruvllm-boundary.md new file mode 100644 index 000000000..675eeeff0 --- /dev/null +++ b/docs/adr/ADR-350-ruvector-predictive-memory-and-ruvllm-boundary.md @@ -0,0 +1,217 @@ +# ADR-350: RuVector predictive memory and RuVLLM authority boundary + +- **Status**: Proposed +- **Date**: 2026-09-01 +- **Deciders**: ruv +- **Owners**: RuView forecast, RuVector, evidence, and RuVLLM integration maintainers +- **Tags**: forecasting, ruvector, ruvllm, retrieval, memory, receipts, authority +- **Parent**: ADR-348 +- **Extends**: ADR-004, ADR-010, ADR-016, ADR-145, ADR-261, ADR-295, + ADR-304, ADR-319, ADR-348 +- **Supersedes**: None + +## Decision + +RuVector may augment RuView Forecast by retrieving analogous historical +feature/forecast states from a tenant- and split-scoped predictive-memory +index. Retrieval is optional context, never ground truth. Every release must +report the same frozen examples with retrieval disabled and enabled so its +incremental value and leakage risk remain visible. + +Each forecast is published in an immutable, locally signed envelope binding the +model artifact, request, output, source evidence, and retrieval receipt. RuVLLM +may read that envelope to explain uncertainty and analogous history. It may not +rewrite numeric forecasts, create a stronger evidence class, select a model, +promote an artifact, spend money, invoke an actuator, or make medical, +emergency, access-control, or life-safety decisions. + +This child ADR delegates forecasting, clean-room, data, and general production +gates to [ADR-348](./ADR-348-independent-rust-multivariate-forecasting.md). It +does not authorize a sensing-server bridge. + +**Evidence status:** canonical forecast/receipt types and an indexable latent +state exist in source. A RuVector adapter, retrieval receipt, signature wrapper, +outcome reconciler, RuVLLM explanation adapter, paired retrieval evaluation, +latency, storage cost, accuracy lift, and operational value are all +**UNMEASURED and unapproved**. + +## Existing implementation boundary + +The current crates establish only the preconditions: + +| Module | Implemented surface used by this ADR | +|---|---| +| [`ruview-forecast-core/src/forecast.rs`](../../v2/crates/ruview-forecast-core/src/forecast.rs) | validated `ForecastRequest`, backend-neutral `Forecaster`, ordered finite `Forecast`, canonical output digest, and receipt verification | +| [`ruview-forecast-core/src/receipt.rs`](../../v2/crates/ruview-forecast-core/src/receipt.rs) | `SourceState`, `ArtifactReceipt`, and `ForecastReceipt`; derived forecasts cannot retain `MEASURED` merely because measured input existed | +| [`ruview-forecast-core/src/series.rs`](../../v2/crates/ruview-forecast-core/src/series.rs) | bounded feature schema, masked time series, canonical series digest, and training-only scaler fit | +| [`ruview-forecast-model/src/network.rs`](../../v2/crates/ruview-forecast-model/src/network.rs) | `ForecastModelOutput.state` with shape `[batch, variates, d_model]`, intended as a bounded representation candidate for indexing | + +`ForecastReceipt` is content-addressed but not itself a digital signature. +`ForecastModelOutput.state` is indexable but not automatically safe, private, +stable across model versions, or useful. Those distinctions remain release +gates. + +## Predictive-memory record + +RuVector stores a versioned `PredictiveMemoryRecord` containing: + +- tenant and index namespace; +- model/artifact/config/feature-schema digests; +- source series and forecast-request digests; +- split, site, subject-class, session, device, calibration, and bounded time + scope using pseudonymous identifiers; +- bounded latent-state or engineered-feature digest plus the approved vector; +- forecast envelope digest and horizon; +- observed validity mask and evidence class; +- optional reconciled outcome digest added only after the forecast horizon; +- retention/deletion class and creation/expiry time. + +Raw CSI, unrestricted feature windows, precise room coordinates, persistent +person identity, RuVLLM prompts, and explanation prose are excluded by default. +The vector is still potentially sensitive because it can encode routines or +location. Tenant isolation, encryption, access control, retention, deletion, +and membership-inference review apply. + +An outcome is appended through a new immutable record linked to the original; +the historical forecast is never rewritten. Outcome reconciliation can measure +forecast quality but cannot retroactively turn the forecast into a measured +observation. + +## Split-scoped analogue retrieval + +Every index is bound to an immutable corpus manifest, model version, feature +schema, preprocessing configuration, and split policy. These rules are +mandatory: + +1. Tenants never share an index or query result without a separately authorized + privacy-preserving federation protocol. +2. Training, validation, calibration, and test records have distinct + namespaces. A test query may retrieve approved training analogues only; it + may not retrieve test examples or validation/calibration records used to set + thresholds. +3. The same site, subject, session, device, calibration episode, overlapping + context, or overlapping target horizon is excluded when that dimension is a + holdout. +4. Index construction, distance metric, normalization, filter policy, `k`, and + score threshold are fitted on training data and frozen before test. +5. Missing, stale, mismatched, unauthorized, or unverifiable indexes disable + retrieval and return the no-retrieval forecast or abstention according to + the model card. They never trigger an implicit global-index fallback. +6. Every query returns a bounded `RetrievalReceipt` even when zero neighbours + qualify. + +The retrieval receipt binds query digest, index/corpus/policy digests, +namespace, exclusion filter, neighbour record IDs and distances, `k`, latency, +and disposition. It excludes raw neighbour payloads from ordinary logs. + +## Paired ablation + +Retrieval has no deployment authority without a paired evaluation on identical +frozen requests: + +| Row | Model and examples | Retrieval | +|---|---|---| +| A | exact candidate artifact and frozen examples | disabled | +| B | exact candidate artifact and frozen examples | enabled with frozen index/policy | + +The report includes weighted quantile loss, interval coverage/width, +abstention, per-horizon error, per-site/device/interference slices, retrieval +hit/filtered/empty rates, latency, and memory/storage overhead. It reports both +aggregate and paired deltas with uncertainty. A gain on pooled error cannot +hide a failed deployment domain, calibration regression, or neighbour leakage. + +The no-retrieval row remains a supported fallback. Retrieval is removed when +its lower confidence bound does not show useful improvement, when it weakens +calibration beyond the ADR-348 gate, or when its privacy/latency/storage cost +exceeds its measured value. No uplift is claimed today. + +## Immutable signed forecast envelope + +The signed envelope covers: + +```text +envelope schema/version ++ ArtifactReceipt canonical digest ++ ForecastRequest canonical digest ++ Forecast payload/output digest ++ ForecastReceipt canonical digest ++ RetrievalReceipt digest or explicit retrieval-disabled marker ++ policy/calibration/index digests ++ creation/expiry time and tenant namespace +``` + +Signing occurs at the trusted local boundary after artifact and retrieval +verification. The wrapper records algorithm, public key ID, signer capability, +and signature. Private keys never enter model artifacts, RuVector, RuVLLM, or a +hosted training worker. + +Consumers verify signature, expiry, tenant, schemas, every nested digest, and +the `Forecast::verify_receipt` invariant before using numeric values. Any +failure yields unavailable/abstain. Unsigned content-addressed receipts remain +useful for local tests but are not described as signed and cannot cross the +production trust boundary. + +## RuVLLM explanation boundary + +RuVLLM receives a read-only projection of the verified envelope: + +- exact point and quantile values as structured fields; +- units, horizons, calibration/OOD/abstention state; +- bounded analogue summaries and retrieval receipt references; +- evidence labels, model/version, and envelope digest; +- approved explanation policy and audience. + +Explanation prose is stored separately and linked to the envelope digest. It +is `CLAIMED_EXPLANATION`, not a replacement forecast. Numeric API fields are +copied from the verified envelope after generation, never parsed back from LLM +text. If prose contradicts a structured number, evidence state, unit, or +disposition, the response fails validation and the numeric envelope remains +authoritative. + +The explanation capability exposes no tools for artifact activation, model +promotion, retraining, spending, messaging, emergency dispatch, access control, +or actuation. A separate downstream policy may consume a forecast only under +its own ADR, capabilities, approvals, and observed-evidence requirements. The +LLM cannot grant itself that authority or lower an approval threshold. + +## Requirements and acceptance + +| ID | Requirement | ADR-348 gate | Acceptance evidence | Current state | +|---|---|---|---|---| +| PM-001 | Every predictive-memory query is tenant-, model-, schema-, corpus-, split-, and time-scope bound. | G3, G5 | Cross-tenant/split/version negative tests and signed index manifest | **OPEN / UNMEASURED** | +| PM-002 | Holdout identities, overlapping contexts, and target horizons cannot appear as neighbours. | G3 | Property tests plus frozen leakage report | **OPEN / UNMEASURED** | +| PM-003 | Every query, including disabled/empty/error, produces a bounded retrieval receipt. | G2, G3 | Receipt round-trip, tamper, bound, and zero-result tests | **OPEN / UNMEASURED** | +| PM-004 | Retrieval-disabled and retrieval-enabled rows use identical artifact/examples and report paired metrics and overhead. | G3 | Frozen paired ablation with reproducer | **OPEN / UNMEASURED** | +| PM-005 | Forecast envelope signatures bind all nested forecast/retrieval identities and fail closed on mutation, expiry, tenant, or key error. | G2, G5 | Signature/tamper/replay/cross-tenant tests and local signing receipt | **OPEN / UNMEASURED** | +| PM-006 | Derived forecast and explanation can never become `MEASURED_OBSERVATION` or exceed source evidence. | G2, G4 | Evidence-monotonicity schema/property tests | **OPEN / UNMEASURED** | +| PM-007 | RuVLLM cannot mutate structured numbers; contradictions fail validation and explanations remain separately labelled. | G4 | Numeric/unit/evidence mutation corpus and fail-closed integration tests | **OPEN / UNMEASURED** | +| PM-008 | RuVLLM explanation has no spending, promotion, messaging, safety, access-control, or actuator capability. | G4, G5 | Default-deny capability and attempted-escalation tests | **OPEN / UNMEASURED** | +| PM-009 | Vector/explanation privacy, retention, deletion, extraction, and membership risks are approved and operationally tested. | G5 | Privacy review, tenant deletion drill, and access audit | **OPEN / UNMEASURED** | + +G3 additionally requires the paired retrieval ablation to satisfy ADR-348's +forecast and calibration gates. G4 requires shadow evidence that retrieval and +explanation improve operator comprehension or forecast value without changing +numeric/action authority. G5 requires signed-envelope verification, rollback, +tenant deletion, and key-rotation drills. None has passed. + +## Consequences + +RuVector can evolve from retrospective search into outcome-linked predictive +memory while preserving a measurable no-retrieval baseline. RuVLLM can make +forecasts understandable without becoming the numeric or action authority. The +cost is additional index isolation, storage, signature, deletion, evaluation, +and policy complexity. If paired evidence does not justify that cost, the +correct release keeps retrieval and explanation disabled. + +## References + +- [ADR-348](./ADR-348-independent-rust-multivariate-forecasting.md) +- [ADR-349](./ADR-349-governed-local-and-fal-forecast-training.md) +- [RuView Forecast clean-room protocol](../security/ruview-forecast-clean-room.md) +- [RuView Forecast model-card template](../huggingface/RUVIEW_FORECAST_MODEL_CARD_TEMPLATE.md) +- [RuForecast benchmark protocol](../benchmarks/ruforecast.md) +- [ADR-016](./ADR-016-ruvector-integration.md) +- [ADR-145](./ADR-145-ablation-eval-harness-privacy-leakage.md) +- [ADR-261](./ADR-261-ruvector-graph-ann-index.md) +- [ADR-304](./ADR-304-evidence-engine.md) +- [ADR-319](./ADR-319-witness-chain.md) diff --git a/docs/adr/README.md b/docs/adr/README.md index 95a200e4f..d317a01c5 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -92,6 +92,9 @@ Statuses: **Proposed** (under discussion), **Accepted** (approved and/or impleme | [ADR-150](ADR-150-rf-foundation-encoder.md) | RF Foundation Encoder: pose-preserving, subject/room/device-invariant CSI embedding | Proposed | | [ADR-151](ADR-151-room-calibration-specialist-training.md) | Per-Room Calibration & Specialized Model Training (room-first → bank of small ruVector specialists) | Proposed | | [ADR-152](ADR-152-wifi-pose-sota-2026-intake.md) | WiFi-Pose SOTA 2026 Intake: geometry-conditioned calibration, external benchmarks, foundation-encoder recipe | Proposed | +| [ADR-348](ADR-348-independent-rust-multivariate-forecasting.md) | Independent Rust multivariate forecasting for RuView | Proposed | +| [ADR-349](ADR-349-governed-local-and-fal-forecast-training.md) | Governed local and fal.ai forecast training | Proposed | +| [ADR-350](ADR-350-ruvector-predictive-memory-and-ruvllm-boundary.md) | RuVector predictive memory and RuVLLM authority boundary | Proposed | ### Platform and UI diff --git a/docs/benchmarks/ruforecast.md b/docs/benchmarks/ruforecast.md new file mode 100644 index 000000000..a8393c53e --- /dev/null +++ b/docs/benchmarks/ruforecast.md @@ -0,0 +1,380 @@ +# RuForecast benchmark protocol + +## Evidence status + +No RuForecast runtime, accuracy, calibration, memory, or operational result is +recorded here yet. Every threshold below is an ADR-348 target, not a measured +claim. The first accepted row must identify a clean commit, the `Cargo.lock` +digest, the exact Rust toolchain, host, backend, model/configuration digest, +fixture or corpus digest, command, and evidence label. + +## Benchmark boundaries + +RuForecast uses two different forms of evidence: + +1. Deterministic correctness evidence comes from Rust unit, property, replay, + split-isolation, and artifact-tamper tests. A fixed input and artifact must + produce the same output within its declared platform class. +2. Runtime evidence comes from Criterion on a named host. Criterion inputs are + generated from fixed checked-in code and seeds, but elapsed time is not + deterministic. Timing from shared GitHub runners is informational only. + +The benchmark implementation must not download a model or dataset, read raw +CSI, use a hosted model output, or enable CUDA implicitly. CPU benchmarks use +the explicit `cpu` feature and the Burn ndarray backend. CUDA validation belongs +to a separately governed Linux or hosted-accelerator receipt. + +## Required benchmark targets + +| Package | Target | Purpose | CI authority | +|---|---|---|---| +| `ruview-forecast-model` | `forecast_inference` | Fixed-seed forward pass, batch and shape scaling, ordered-quantile output | Compile gate; shared-runner timing is informational | +| `ruview-forecast-train` | `data_pipeline` | Fixed generated records through validation, windowing, masking and batching | Compile gate; shared-runner timing is informational | + +Both targets must use code-generated synthetic inputs, fixed seeds, bounded +allocations, `criterion::black_box`, and `required-features = ["cpu"]`. Setup, +artifact construction, and dataset generation stay outside the timed region +unless a benchmark name explicitly says they are included. + +The model implementation owns structural parameter-count assertions. The +currently reviewed design values are 35,700 parameters for the tiny CI preset +and 20,285,108 for the large preset. These are design invariants, not benchmark +results, and must be derived by a test from the actual module graph before they +are quoted in a model card. + +## Local Linux reproducer + +Run from a clean checkout after installing Rust 1.92.0: + +```bash +RUFORECAST_CPUSET=0-7 \ +RUFORECAST_THREADS=8 \ +scripts/run-ruforecast-benchmarks.sh +``` + +The runner executes the focused contract/model/training tests, one real +optimizer step over a local hash-addressed synthetic JSONL shard, and the +idempotent synthetic CLI smoke. It then compile-checks both Criterion targets, +runs the targets, captures the CPU and toolchain metadata, and hashes every +output. A failed run retains its partial logs with `status=FAILED` and its exit +code rather than looking like a complete report. Results go under +`target/ruforecast-evidence/`, which is excluded from source control. + +For a conservative single-thread reproducibility check, omit both environment +variables. To run against an uncommitted tree for diagnosis only, set +`RUFORECAST_ALLOW_DIRTY=1`; the resulting metadata is labelled `SYNTHETIC` +with scope `DIRTY_WORKTREE_DIAGNOSTIC_ONLY` and cannot support a release claim. + +A clean run labels its host timing `MEASURED` and its input class `SYNTHETIC`, +but remains `UNREVIEWED`. Only a maintainer may append it to the accepted ledger +after checking the digests, shape, command, Criterion report and host scope. + +The runner intentionally has no CUDA option and does not parse Criterion output +into a pass/fail performance verdict. This prevents a noisy host result from +silently acquiring release authority. + +To compile the two benchmark targets without measuring them: + +```bash +cd v2 +cargo +1.92.0 bench --locked -p ruview-forecast-model \ + --no-default-features --features cpu --bench forecast_inference --no-run +cargo +1.92.0 bench --locked -p ruview-forecast-train \ + --no-default-features --features cpu --bench data_pipeline --no-run +``` + +For a short informational run, use the same targets without `--no-run`: + +```bash +cargo +1.92.0 bench --locked -p ruview-forecast-model \ + --no-default-features --features cpu --bench forecast_inference -- \ + --warm-up-time 1 --measurement-time 2 --sample-size 10 +cargo +1.92.0 bench --locked -p ruview-forecast-train \ + --no-default-features --features cpu --bench data_pipeline -- \ + --warm-up-time 1 --measurement-time 2 --sample-size 10 +``` + +Running these commands does not add a ledger row automatically. Preserve the +raw report and environment metadata, then have a maintainer assign its evidence +scope before publishing a number. + +The inference bench runs only `tiny_ci` by default so a routine CI trend step +cannot accidentally start the very expensive large CPU probe. Set +`RUFORECAST_BENCH_LARGE=1` only on a controlled host when intentionally +measuring the fixed deployment shape: + +```bash +RUFORECAST_BENCH_LARGE=1 scripts/run-ruforecast-benchmarks.sh +``` + +## Deployment measurement shape + +The initial CPU deployment probe is batch 1, context 1,024, 32 declared feature +streams, the fixed `large_linux` horizon of 300, and all seven declared +quantiles. Record at least 20 warmup +iterations and 200 measured iterations for a release candidate. Report p50, +p95, p99 or maximum, throughput, and the process peak resident set size. + +ADR-348 G5 currently targets p95 at or below one second for 32 declared streams +and peak process memory at or below 4 GiB. A Criterion result alone cannot close +the memory gate because Criterion and Cargo are not the production inference +process. G5 remains open until a standalone inference probe reports its own peak +resident set size. + +## Accuracy and calibration protocol + +Runtime speed never substitutes for forecasting quality. The frozen evaluation +manifest must report identical examples for: + +1. Last-value and seasonal-naive baselines. +2. RuForecast without RuVector retrieval. +3. RuForecast with split-scoped RuVector retrieval. + +Required report fields include weighted quantile loss by horizon, nominal 80% +interval coverage, missingness, abstention coverage, selective risk, site and +device slices, interference regime, and retrieval ablation. ADR-348 G3 targets +weighted quantile loss at least 10% better than seasonal naive and 80% interval +coverage between 75% and 85%. Those targets remain unmeasured until a frozen, +leakage-free report is attached. + +### Informal HPO exploration note (unaccepted, not a release claim) + +**2026-09-01.** An exploratory session ran the accuracy protocol above end to +end against a governed 24-window **synthetic** dataset (`tiny_ci` profile, +context 64 / horizon 12, temporal train/test split, not entity-holdout — +only one synthetic generator was used, so entity holdout does not apply) and +a small `OptimizerSpec` hyperparameter search (learning rate, weight decay, +gradient clip norm, batch size, epochs) using a new Darwin Mode numeric-genome +evolution engine (upstream: `ruvnet/metaharness` PR #260, not yet merged). +This is **exploratory evidence only** — not a frozen, leakage-free, +maintainer-reviewed report, and not eligible for the ledger below until one +is produced. + +Prior to this exploration, a **single real household window** (76 real +1 Hz vital-signs samples, one physical ESP32 sensor, temporal not entity +holdout) scored **worse than both baselines** (WQL 0.537 vs. last-value +0.106 and seasonal-naive 0.123) — consistent with a single training window +overfitting rather than generalizing. + +With a larger (still synthetic, still `tiny_ci`) 24-window training set and +three rounds of hyperparameter search, weighted quantile loss on the held-out +synthetic split improved and stayed ahead of both baselines throughout: + +| Round | learning_rate | weight_decay | grad_clip | batch | epochs | WQL (model) | WQL (last-value) | WQL (seasonal-naive) | +|---|---:|---:|---:|---:|---:|---:|---:|---:| +| Default config | 0.0010000 | 1.00e-4 | 1.000 | 8 | 60 | 0.257 | 0.277 | 0.514 | +| Search round 1 | 0.0002356 | 3.05e-6 | 0.100 | 27 | 195 | 0.161 | 0.277 | 0.514 | +| Search round 2 | 0.0000298 | 4.09e-11 | 4.746 | 26 | 356 | **0.153** | 0.277 | 0.514 | + +Round-2 gain over round 1 (−0.008) was much smaller than round-1's gain over +the default (−0.096) — a diminishing-returns signal consistent with a local +optimum for this model size and dataset, not a converged global result. +`gradient_clip_norm` landed at opposite bound extremes across rounds +(0.1 then 4.7), so no directional recommendation on that parameter should be +drawn from this exploration alone. + +**Explicit scope limits — do not generalize beyond these:** +- `tiny_ci` only. Nothing here has been run against `large_linux`; its far + larger parameter count and different compute profile mean these + hyperparameters are not a starting point for it without their own search. +- Synthetic dataset only (24 windows, one generator/seed family). Not + validated against any real corpus at this scale. +- Self-signed, evaluation-only model activation (a throwaway local Ed25519 + key, not a release signature) was used to run inference for scoring. +- No security/provenance/maintainer-approval gate has passed — the Darwin + Mode promotion rule correctly refused to promote any candidate here. + +Reproducer: `harness/ruview/flywheel/ruforecast/` (genome, gate, evaluator, +dry-run/`--confirm` driver) in the `ruvnet/RuView` repo, paired with +`ruvnet/metaharness` PR #260 (`evolve-numeric`) linked locally via +`npm link`. Neither the genome defaults here nor any repo default config +were changed by this note — it is a record of exploratory evidence, not a +committed recommendation. + +### Amendment (2026-09-01, later same day): the round-2 result above did not generalize -- retracted + +**The "Search round 2" row above (WQL 0.153, learning_rate=0.0000298 etc.) is +RETRACTED as a claim of improvement.** It is kept in the table (append-only, +never silently edit a prior measurement) but must be read together with this +amendment: independent verification, run the same day using a new +regression-candidate promotion path (`ruvnet/autogenous` PR-in-progress, +branch `feat/regression-candidate-kind`, not yet merged/pushed -- +`v2/crates/ruforecast-autogenous-bridge` in this worktree) against TWO FRESH +synthetic corpora that were never part of that search (seeds 1000/1097, vs. +the search's single fixed seed 0), showed that "winner" genome performing +**WORSE than the baseline on both**: + +| Judge corpus | Candidate WQL | Baseline WQL | Candidate beats baseline by | +|---|---:|---:|---:| +| seed 1000 | 0.155 | **0.099** | -0.056 (worse) | +| seed 1097 | 0.219 | **0.109** | -0.110 (worse) | + +**Root cause**: every evaluation in the three search rounds above (default, +round 1, round 2) trained and scored every candidate against the exact same +fixed synthetic corpus (`prepare-synthetic-dataset`'s implicit `--seed 0` +default). The search had learned to exploit that one corpus's specific +random windows -- textbook overfitting -- not found a genuinely better +hyperparameter configuration. This is a real, measured failure mode, not a +hypothetical caveat. + +**Fix applied**: `harness/ruview/flywheel/ruforecast/gate.mjs`'s +`evaluateGenome` now trains and scores every candidate against THREE +independent synthetic corpora (`DEFAULT_SEARCH_SEEDS = [11, 23, 47]`, none +of which overlap the retracted search's seed 0 or the verification seeds +1000/1097) and takes the WORST CASE `primary` across them, not an average -- +a candidate only counts as a win if it beats both baselines on every corpus. +See `harness/ruview/flywheel/ruforecast/README.md`'s "Multi-seed fitness" +section for the full account. + +**Re-running the search with the fix, still no verified improvement.** A +fresh search under the corrected multi-seed fitness (2 generations x 2 +children, cleared `.metaharness-numeric` archive so no stale pre-fix state +leaked in) found a genuine winner ON ITS OWN THREE SEARCH SEEDS -- +`learning_rate=0.008012, weight_decay=0.000159, gradient_clip_norm=0.267, +batch_size=24, epochs=20`, beating the baseline on all three (primary +0.099 / 0.556 / 0.576, worst-case 0.099). Independent verification against +the same fresh seeds 1000/1097 (never part of this candidate's own search) +again showed it losing to the baseline on both: + +| Judge corpus | Candidate WQL | Baseline WQL | Candidate beats baseline by | +|---|---:|---:|---:| +| seed 1000 | 0.178 | **0.099** | -0.079 (worse) | +| seed 1097 | 0.314 | **0.109** | -0.205 (worse) | + +**Honest conclusion**: two independent search rounds (pre- and post- the +multi-seed fitness fix) both produced a genome that looked like a real +improvement on its own search data and both failed independent out-of-sample +verification. This converges on a different, deeper explanation than "the +search methodology was broken" (that part IS fixed): at this dataset scale +(24 synthetic training windows), held-out WQL varies enormously by which +corpus is drawn REGARDLESS of hyperparameters -- the baseline genome itself, +evaluated with the corrected multi-seed fitness function, swings from +primary 0.83 (a strong win) to a full regression (primary 0, WQL worse than +last-value) purely from which of the three fixed search seeds was used, with +identical hyperparameters throughout. The corpus-noise floor at n=24 +windows appears to dominate any real hyperparameter effect. No RuForecast +hyperparameter configuration has been shown, by this exploration, to +reliably beat the trivial baselines out-of-sample at this scale. A larger +training corpus (more windows, ideally real governed data under the +`large_linux` profile) is the more promising next lever than further +hyperparameter search on this fixture. + +A related implementation-only fix: the promotion verifier +(`envelope::regression::verify_regression_promotion` in the Autogenous +branch above) initially required all judges' receipts to share one +`corpus_id`, inherited unreviewed from a same-evidence review model that +does not fit this kind's intentionally cross-corpus judge design. This was +corrected (`ReceiptCorpusMismatch` is no longer produced by that function); +it did not change either REJECT verdict above, both of which were already +correctly driven by the real `NotBetterThanParent` signal. + +Reproducer for both verification runs above: same as below, plus +`v2/crates/ruforecast-autogenous-bridge` (`cargo +1.92.0 run --manifest-path +crates/ruforecast-autogenous-bridge/Cargo.toml -- --ruforecast-bin +./target/debug/ruforecast --candidate-genome .json --parent-genome +.json --judges 2 --work-dir `) against the unpushed +`ruvnet/autogenous` branch `feat/regression-candidate-kind`. + +### Real-household-data result (2026-09-01, MEASURED, unaccepted, not a release claim) + +Following the informal HPO exploration above, this session also collected +**6,390 real 1 Hz vital-signs samples** (heart rate, breathing rate, +signal quality) from a real, live, ESP32-sourced household sensing +deployment over a continuous 2-hour window (88.75% real sample coverage; +gaps handled honestly via `observed_mask=0`, never fabricated +interpolation) — the "more real training data" lever flagged as the +credible next step in the note above. This directly answers that open +question. + +Two genuinely independent temporal splits of the same real corpus (not +synthetic seeds — real data has no seed to vary, so independence here +means two different train/test boundary choices on the same timeline, +each with its own 90s embargo gap) were trained (default, untuned +`OptimizerSpec`: `lr=0.001, weight_decay=0.0001, gradient_clip_norm=1.0, +batch_size=8, epochs=60`) and scored via the real `evaluate` CLI, then +independently, cryptographically verified through +`ruforecast-autogenous-bridge`'s real signed regression-candidate +promotion path (`ruvnet/autogenous`, `envelope::regression`): + +| Judge | Split | Real test windows | Model WQL | Best trivial baseline WQL | Model beats baseline by | +|---|---|---:|---:|---:|---:| +| 1 | 70% train / 90s embargo / 30% test | 27 | 0.0514 | 0.0563 (last-value) | +0.0049 | +| 2 | 50% train / 90s embargo / 50% test | 46 | 0.0670 | 0.0543 (seasonal-naive) | −0.0128 | + +**Signed verdict: REJECT.** Judge 1's nominal win (+0.0049) is below the +0.01 non-inferiority margin, so it doesn't clear the promotion bar even +on its own; Judge 2 lost outright. Both rejections are recorded as +`NotBetterThanParent` in the signed promotion envelope. + +**Honest conclusion:** even with a real household corpus (n=6,390 real +samples, not synthetic), the result is exactly the same shape as every +synthetic search this session — a result that looks like a win on one +split does not hold up on an independently verified second split. This +is not evidence that real data can't help; it is evidence that this +scale of real data (6,390 samples, one household, one physical sensor) +is not yet enough to distinguish a genuine effect from split-dependent +noise. The credible next lever remains more real data — more households, +longer collection windows, or the `large_linux` profile — not further +hyperparameter search on any fixture this small, synthetic or real. + +Reproducer: `v2/crates/ruforecast-autogenous-bridge/examples/real_data_verify.rs` +and `v2/crates/ruforecast/crates/ruforecast-train/examples/real_data_windows.rs` +in this worktree (`train/ruforecast-rust` branch, commits `130f547` in the +`ruforecast` submodule and `788401c5` in this repo — both local, not yet +pushed). Raw real vitals data never left the collecting/training hosts and +was never written to any git-tracked or pushed path. + +## Append-only evidence ledger + +Never replace a prior measurement. Append a row and retain the failed or stale +row when code, model, configuration, corpus, hardware, or methodology changes. + +| Date | Commit | Lock SHA-256 | Host/toolchain | Backend/config | Shape | Samples | p50 | p95 | p99/max | Peak RSS | Evidence | Reproducer | +|---|---|---|---|---|---|---:|---:|---:|---:|---:|---|---| + +No rows have been accepted. + +### Real public dataset: BIDMC PPG/Respiration (2026-09-02, cross-entity holdout) + +Every real-data test up to this point used a single household/entity with only +a **temporal** holdout (same physical sensor, different time windows). This +test is the first with a genuine **cross-entity** holdout: 53 real ICU +patients from the BIDMC PPG and Respiration Dataset (PhysioNet, Open Data +Commons Attribution License v1.0, public and openly licensed — no +credentialing, https://physionet.org/content/bidmc/1.0.0/), splitting by +*patient*, not by time, so the held-out test set contains real people the +model never saw during training. + +25,546 real 1 Hz rows across 53 recordings (~8 minutes each), heart rate + +respiratory rate + SpO2. 3 of 53 patients' windows were excluded honestly +(genuine sensor-dropout `NaN` values in the source recordings, not +fabricated/interpolated). Two independent, disjoint patient-partition splits: + +| Judge | Train patients | Test patients | Test windows | Model WQL | Best baseline WQL | Result | +|---|---:|---:|---:|---:|---:|---| +| A (contiguous split) | 34 | 16 | 16 | 0.01964 | 0.01159 (last-value) | worse, +69% | +| B (interleaved split) | 24 | 26 | 26 | 0.07022 | 0.01002 (last-value) | worse, +601% | + +**Same conclusion as every prior test this session**, now on real, public, +multi-subject clinical data with genuine cross-entity generalization: the +model does not beat trivial forecasting baselines. The margin is decisive on +both independent splits, not a near-miss — the earlier hypothesis that a +larger, genuinely diverse real dataset (many different people, not one +household) might change the picture does not hold at this scale/model +configuration either. + +**Honest scope note on verification**: prior real-data tests in this document +were independently checked through Autogenous's signed regression-candidate +promotion path. That additional cryptographic-signing step was **not** run +for this test — the result is reported directly from the `evaluate` CLI's +real output on two genuinely disjoint, real patient-holdout splits, which is +itself real, independent, out-of-sample evidence, but it does not carry a +signed promotion-gate verdict the way the earlier entries do. Flagging this +explicitly rather than presenting it with the same evidentiary weight. + +Reproducer: `v2/crates/ruforecast/crates/ruforecast-train/examples/bidmc_prepare.rs` +(untracked scratch example, worktree `train/ruforecast-rust`) — downloads +`bidmc_NN_Numerics.csv` for patients 01-53 directly from PhysioNet, builds +one 76-row (context 64 + horizon 12) window per eligible patient, and writes +governed `train.jsonl`/`test.jsonl`/`train-local.toml` per judge split. Raw +data cached at `/tmp/bidmc-raw/` on the training host only. diff --git a/docs/huggingface/RUVIEW_FORECAST_MODEL_CARD_TEMPLATE.md b/docs/huggingface/RUVIEW_FORECAST_MODEL_CARD_TEMPLATE.md new file mode 100644 index 000000000..13cee5924 --- /dev/null +++ b/docs/huggingface/RUVIEW_FORECAST_MODEL_CARD_TEMPLATE.md @@ -0,0 +1,616 @@ +# RuView Forecast model card template + +> **Template only. Do not publish or treat this file as a model release.** Copy +> it for one immutable candidate, replace every `` field, and retain +> every unresolved item as an explicit blocker. Deleting a placeholder does not +> satisfy it. + +This template implements the model-card requirements in +[ADR-348](../adr/ADR-348-independent-rust-multivariate-forecasting.md) and the +[RuView Forecast clean-room protocol](../security/ruview-forecast-clean-room.md). +It does not imply that source, training, evaluation, clean-room, security, +privacy, trademark, patent, or production gates have passed. + +## Completion rules + +1. Create one card per exact weight digest. Do not reuse a card across retrains. +2. Label every numeric claim `MEASURED`, `SYNTHETIC`, `CLAIMED`, or + `UNMEASURED` under the repository evidence policy. +3. A `MEASURED` value needs an immutable dataset/split, artifact digest, + configuration, hardware/environment, metric definition, and reproducer. +4. Use `UNMEASURED` when evidence does not exist. Do not substitute an estimate. +5. State source-code, weight, dataset, and service terms separately. +6. Do not publish raw CSI, customer data, precise room coordinates, identities, + secrets, private contracts, or private contributor records in this card. +7. A completed card documents evidence; it cannot waive an ADR-348 gate. + +## Suggested Hugging Face metadata + +Copy and complete this front matter in the release card: + +```yaml +--- +license: +tags: + - time-series-forecasting + - multivariate-forecasting + - wifi-sensing + - rust + - ruvector + - probabilistic-forecasting + - edge-ai +language: + - en +library_name: burn +pipeline_tag: time-series-forecasting +--- +``` + +`license: other` is acceptable only when the card links the complete exact +license. Do not infer the weight license from the Rust source license. + +## Candidate identity + +Replace the template title with `` in the +release copy. + +## Release decision and evidence status + +| Field | Required value | +|---|---| +| Release candidate ID | `` | +| Weight SHA-256 | `` | +| Git commit | `` | +| Model-card SHA-256 | `` | +| Proposed mode | `OFFLINE_EVAL`, `SHADOW`, or `ADVISORY`; `` | +| ADR-348 highest gate passed | `G0` through `G5`, or `NONE`; `` | +| Accuracy status | `UNMEASURED` until a qualifying report exists; `` | +| Calibration status | `UNMEASURED` until a qualifying report exists; `` | +| CPU latency status | `UNMEASURED` until a named CPU reproducer exists; `` | +| Memory status | `UNMEASURED` until a named runtime reproducer exists; `` | +| Cross-site generalization status | `UNMEASURED` until untouched site holdouts exist; `` | +| Clean-room status | `OPEN`, `PASS`, or `FAIL`; `` | +| Production approval | `NOT APPROVED` unless all G5 receipts are signed; `` | + +### Release summary + +`` + +## Model details + +| Field | Value | +|---|---| +| Developer/owner | `` | +| Model family | `RuView Forecast` | +| Model version | `` | +| Artifact format | `` | +| Parameter count | `` | +| Numeric precision | `` | +| Architecture/config digest | `` | +| Input schema version | `` | +| Output schema version | `` | +| Maximum context | `` | +| Supported horizons | `` | +| Declared quantiles | `` | +| Training framework | `` | +| Inference runtime | `` | +| Source license | `` | +| Weight license | `` | + +### Rust implementation boundary + +Describe the exact code surfaces used by this candidate: + +| Crate or binary | Version/digest | Responsibility | Enabled features | +|---|---|---|---| +| `ruview-forecast-core` | `` | Backend-neutral schemas, invariants, metrics, receipts, and `Forecaster` trait | `` | +| `ruview-forecast-model` | `` | Independent Burn 0.21 patch mixer and artifact execution | `` | +| `ruview-forecast-train` / `ruforecast` | `` | Dataset splits, trainer, evaluator, training receipt, and Linux/fal.ai assets | `` | +| `` | `` | `` | `` | + +State whether inference is offline and whether any runtime network capability +exists. The expected production answer is no runtime model download and no +network requirement: + +`` + +State whether a sensing-server bridge exists. For the initial ADR-348 PR the +required answer is `NO: contracts/training only; a separately reviewed shadow +bridge is deferred`: + +`` + +## Intended use + +### Approved candidate use + +`` + +### Out-of-scope and prohibited use + +This artifact must not be used unless a later approved card explicitly changes +the boundary: + +- as a sensor observation or ground-truth label; +- as the sole source for medical, emergency, fall-response, industrial-safety, + access-control, policing, insurance, employment, or autonomous-actuation + decisions; +- to infer identity or protected characteristics; +- outside the feature schema, cadence, hardware, site, population, and horizon + validated by this card; +- after calibration, feature schema, source provenance, or OOD checks fail; +- to train another model unless the weight, dataset, and output licenses and a + new provenance review explicitly permit it; +- to claim TimesFM compatibility, affiliation, endorsement, or equivalence. + +Add candidate-specific exclusions: + +`` + +## Functional architecture + +Describe independently implemented components without reproducing external +source expression, diagrams, identifiers, or constants: + +```text + +``` + +| Component | Candidate implementation | Security/resource bound | +|---|---|---| +| Feature validation | `` | `` | +| Normalization | `` | `` | +| Temporal encoding/mixing | `` | `` | +| Cross-stream fusion | `` | `` | +| Missing-data handling | `` | `` | +| Point/quantile head | `` | `` | +| Quantile crossing policy | `` | `` | +| OOD/abstention | `` | `` | +| RuVector retrieval | `` | `` | +| Artifact verification | `` | `` | + +## Input contract + +### Feature schema + +| Field | Unit | Cadence/aggregation | Range | Missing/invalid semantics | Source provenance | +|---|---|---|---|---|---| +| `` | `` | `` | `` | `` | `` | + +State: + +- minimum and maximum context length; +- maximum targets, covariates, horizon, and batch; +- timestamp monotonicity and duplicate policy; +- whether any future-known covariate is permitted and how its authority is + verified; +- finite/range checks and allocation limits; +- minimum validity coverage before abstention; +- treatment of cadence gaps, resets, device changes, and calibration changes; +- OOD input behaviour. + +`` + +### Data not accepted + +`` + +## Output contract + +| Output | Shape/unit | Meaning | Evidence class | +|---|---|---|---| +| Point forecast | `` | `` | `DERIVED_FORECAST` | +| Quantile forecast | `` | `` | `DERIVED_FORECAST` | +| Validity mask | `` | `` | `DERIVED_METADATA` | +| Abstention/disposition | `` | `` | `DERIVED_METADATA` | +| OOD score/state | `` | `` | `DERIVED_METADATA` | +| Receipt/provenance | `` | `` | `SIGNED_METADATA` if signed | + +The output must bind model, configuration, input schema, calibration, source +time range, optional retrieval index, and content hashes. Explain whether and +how quantile crossing is corrected: + +`` + +## RuVector retrieval + +| Field | Value | +|---|---| +| Enabled | `` | +| Index artifact/version/digest | `` | +| Allowed retrieval corpus | `` | +| Split-isolation receipt | `` | +| Neighbour exclusion rules | `` | +| Maximum neighbours/search budget | `` | +| Behaviour when index is absent/stale | `` | + +Report identical-dataset ablations for no retrieval and retrieval. If these do +not exist, state `UNMEASURED`. + +## RuVLLM integration + +State whether RuVLLM consumes this artifact's signed forecast records. It may +explain results but must not rewrite point/quantile values or acquire action +authority from prose. + +| Control | Evidence | +|---|---| +| Numeric outputs originate only from signed forecast record | `` | +| Explanation cites artifact and forecast receipt | `` | +| No model promotion, spending, actuation, or safety-critical authority | `` | +| Prompt/data retention and tenant policy | `` | + +## Independent-development record + +| Record | Digest/approval | +|---|---| +| Approved source allowlist | `` | +| Specification digest | `` | +| Contributor exposure manifest | `` | +| Contributor attestation bundle | `` | +| AI-tool prompt/session manifest | `` | +| Prohibited-artifact scan | `` | +| Source-similarity review | `` | +| Clean-room custodian approval | `` | +| Legal release approval | `` | + +Declaration: + +`` + +Prior exposure disclosures and dispositions, without private details: + +`` + +## Training data + +Do not list a dataset until its licensing/privacy gate passes. + +| Dataset ID/version | Role | Origin/owner | License or contract ID | Commercial ML | Privacy class | Records/windows | Source/transform/split digests | +|---|---|---|---|---|---|---:|---| +| `` | train/validation/calibration | `` | `` | `YES` required | `` | `` | `` | + +### Excluded data + +Explicitly state that TimesFM 3 weights, outputs, activations, generated labels, +and derivatives were excluded, then list all other exclusions: + +`` + +### Collection, consent, minimization, and retention + +`` + +### Synthetic data + +| Generator/version | Seed-data rights | Provider/output terms | Seeds/config digest | Proportion | Use | +|---|---|---|---|---:|---| +| `` | `` | `` | `` | `` | `` | + +## Split and leakage protocol + +| Split | Sites | Subjects | Sessions | Devices | Time blocks | Windows | Manifest digest | +|---|---:|---:|---:|---:|---:|---:|---| +| Train | `` | `` | `` | `` | `` | `` | `` | +| Validation | `` | `` | `` | `` | `` | `` | `` | +| Calibration | `` | `` | `` | `` | `` | `` | `` | +| Test | `` | `` | `` | `` | `` | `` | `` | + +Required leakage checks: + +- parent raw record and derived-window overlap; +- contiguous sequence/session overlap; +- subject/site/device/calibration leakage; +- target-horizon overlap; +- fitted preprocessing or threshold leakage; +- RuVector neighbour/index leakage; +- duplicate and near-duplicate leakage; +- test-informed architecture or hyperparameter changes. + +Report and digest: + +`` + +## Training procedure + +| Field | Value | +|---|---| +| Random initialization | `true` required for this model family | +| Parent checkpoint | `NONE` or approved RuView artifact and digest | +| Source commit | `` | +| Cargo.lock digest | `` | +| Rust toolchain | `` | +| Build/training container | `` | +| Training config digest | `` | +| Seeds | `` | +| Optimizer/schedule/loss | `` | +| Batch/epochs/stopping | `` | +| Hardware | `` | +| Provider | `` | +| Provider job ID | `` | +| Wall time | `` | +| Peak host/GPU memory | `` | +| Estimated/actual cost | `` | +| Emitted checkpoint digests | `` | +| Training receipt digest | `` | + +If local and hosted runs are compared, list numerical-determinism differences +and prove that governed code, data, config, and parent identities match: + +`` + +## Evaluation + +### Baselines and ablations + +Every row uses the same frozen examples and metric implementation. + +| Model | Artifact/version | Retrieval | Parameters | Evidence label | Notes | +|---|---|---|---:|---|---| +| Last value | `` | no | n/a | `` | deterministic baseline | +| Seasonal naive | `` | no | n/a | `` | `` | +| Small classical/recurrent baseline | `` | no | `` | `` | `` | +| RuView Forecast | `` | no | `` | `` | required ablation | +| RuView Forecast | `` | yes | `` | `` | required if retrieval is enabled | + +Do not use TimesFM 3 as an implementation oracle, label source, tuning signal, +or required acceptance baseline. + +### Forecast metrics + +Report per horizon and per site/device/interference regime, plus pooled values. + +| Metric/domain/horizon | Result | 95% interval | Evidence label | Reproducer | +|---|---:|---:|---|---| +| Weighted quantile loss | `` | `` | `` | `` | +| MAE or scaled error | `` | `` | `` | `` | +| Nominal 80% interval coverage | `` | `` | `` | `` | +| Interval width | `` | `` | `` | `` | +| Quantile crossing before/after policy | `` | `` | `` | `` | +| Abstention coverage/selective risk | `` | `` | `` | `` | + +ADR-348 G3 targets are at least 10% weighted-quantile-loss improvement over +seasonal naive and 75%-85% measured coverage for a nominal 80% interval. These +remain `UNMEASURED targets` until populated by qualifying evidence. + +### RuView shadow outcomes + +| Metric | Baseline | Candidate | Delta/CI | Evidence label | Reproducer | +|---|---:|---:|---:|---|---| +| Empty-room false alerts | `` | `` | `` | `` | `` | +| Occupied-room recall | `` | `` | `` | `` | `` | +| Abstention by deployment | `` | `` | `` | `` | `` | +| Drift/OOD rate | `` | `` | `` | `` | `` | + +ADR-348 G4 targets at least 50% relative empty-room false-alert reduction with +no more than 2 percentage points occupied-room recall loss over at least 14 +shadow days. These are `UNMEASURED targets`, not current capabilities. + +### Runtime measurements + +| Platform | Build/features | Batch/streams | Context/horizon | p50/p95/p99 | Peak RSS | Evidence label | Reproducer | +|---|---|---:|---|---|---:|---|---| +| `` | `` | `` | `` | `` | `` | `` | `` | +| `` | `` | `` | `` | `` | `` | `` | `` | + +ADR-348 G5 CPU targets are at most 1 second p95 for 32 declared streams and at +most 4 GiB peak process memory. They are `UNMEASURED targets` until this table +contains a qualifying named-platform reproducer. + +## Calibration, OOD, and abstention + +| Control | Fit data | Frozen parameters/digest | Test result | Evidence label | +|---|---|---|---|---| +| Input normalization | `` | `` | `` | `` | +| Quantile calibration | `` | `` | `` | `` | +| OOD threshold | `` | `` | `` | `` | +| Minimum validity/context | `` | `` | `` | `` | +| Drift threshold | `` | `` | `` | `` | + +List all typed abstention reasons and demonstrate that unknown schema, missing +calibration, stale data, insufficient history, invalid masks, non-finite input, +OOD state, and artifact failure do not produce an authoritative forecast: + +`` + +## Robustness and failure analysis + +Report at minimum: + +- unseen site and unseen device; +- empty room and low-motion occupancy; +- burst loss, cadence change, clock reset, and sensor restart; +- interference, channel change, and calibration drift; +- long missing runs and adversarial non-finite/range inputs; +- corrupted/truncated/oversized model and input artifacts; +- absent or stale RuVector index; +- concurrent-load resource caps and timeout; +- false confidence from narrow intervals during distribution shift. + +| Scenario | Expected safe behaviour | Observed result | Evidence label | Open risk/owner | +|---|---|---|---|---| +| `` | `` | `` | `` | `` | + +## Security and privacy + +### Threat model summary + +| Threat | Control | Evidence | Residual risk/owner | +|---|---|---|---| +| Malicious/corrupt model artifact | Signature, digest, schema and size verification; no embedded code | `` | `` | +| Oversized/malformed request | Dimension, allocation, horizon, context, batch and deadline caps | `` | `` | +| Poisoned data or split leakage | Approved manifests, immutable transforms, lineage and overlap checks | `` | `` | +| Cross-tenant analogue retrieval | Tenant and split-scoped RuVector indexes | `` | `` | +| Routine/location inference | Minimization, purpose, retention, deletion, access and audit | `` | `` | +| Model extraction/membership inference | Rate/access control and privacy evaluation | `` | `` | +| Hosted-worker data retention | Approved provider terms, least data, deletion receipt | `` | `` | +| LLM numeric mutation or overclaim | Signed numeric record and capability policy | `` | `` | + +### Data handling + +`` + +## Deployment + +| Field | Value | +|---|---| +| Supported mode | `` | +| Supported platform/CPU/GPU | `` | +| Minimum RAM/storage | `` | +| Offline artifact source | `` | +| Signature/trust root | `` | +| Configuration/calibration artifact | `` | +| RuVector index requirement | `` | +| Startup self-test | `` | +| Health/drift metrics | `` | +| Timeout/backpressure policy | `` | +| Rollback artifact/mode | `` | + +No deployment may fetch an unpinned model at runtime. Describe exact startup +failure and fail-closed behaviour: + +`` + +## Monitoring, rollback, and retirement + +| Signal | Threshold | Window | Response | Owner | +|---|---:|---|---|---| +| Calibration/coverage drift | `` | `` | shadow/off | `` | +| OOD/abstention rate | `` | `` | `` | `` | +| Latency/resource regression | `` | `` | `` | `` | +| Empty/occupied outcome regression | `` | `` | `` | `` | +| Provenance/signature failure | any | immediate | disable artifact | `` | +| Security/privacy incident | any material incident | immediate | isolate, preserve evidence, disable | `` | + +Rollback drill receipt and result: + +`` + +Retirement and data/index/checkpoint deletion policy: + +`` + +## Limitations and largest uncertainty + +`` + +## Cost, energy, and operational burden + +| Item | Value | Evidence label | Method | +|---|---:|---|---| +| Training accelerator hours | `` | `` | `` | +| Training cost | `` | `` | `` | +| Training energy/emissions | `` | `` | `` | +| CPU inference cost/energy | `` | `` | `` | +| Storage/index overhead | `` | `` | `` | +| Operator/calibration burden | `` | `` | `` | + +## Licenses and notices + +| Surface | Exact license/terms | URL/file | Obligations | Approval | +|---|---|---|---|---| +| Rust source | `` | `` | `` | `` | +| Weights | `` | `` | `` | `` | +| Each dataset | `` | `` | `` | `` | +| Dependencies/SBOM | `` | `` | `` | `` | +| Training provider | `` | `` | `` | `` | + +Trademark clearance ID and scope: + +`` + +Patent freedom-to-operate disposition and launch jurisdictions: + +`` + +## Provenance and reproducibility + +| Artifact | Digest/signature/location | +|---|---| +| Source allowlist | `` | +| Specification | `` | +| Source commit | `` | +| Cargo.lock | `` | +| SBOM | `` | +| Dataset manifest | `` | +| Split/leakage report | `` | +| Training config | `` | +| Training receipt | `` | +| Evaluation report | `` | +| Clean-room report | `` | +| Model weights | `` | +| RVF signature | `` | +| Reproducer | `` | + +Reproduction commands must use pinned artifacts and must not contain secrets or +download restricted material: + +```bash + +``` + +## ADR-348 gate traceability + +| Gate | Requirements | Evidence in this card/bundle | Result | +|---|---|---|---| +| G0 independent-authoring boundary | RF-001, RF-002 | allowlist, exposure, attestation, scans, similarity review | `` | +| G1 data and lineage | RF-002, RF-003, RF-010 | dataset approvals, lineage, local/hosted receipts | `` | +| G2 bounded Rust contract | RF-004, RF-005, RF-006 | contract/fuzz/replay/resource/dependency reports | `` | +| G3 leakage-free model evidence | RF-007, RF-008, RF-011 | frozen split, baselines, ablations, calibration, reproducers | `` | +| G4 RuView shadow value | RF-006, RF-007, RF-008, RF-009 | 14-day outcomes, drift, abstention, no action authority | `` | +| G5 deployment fitness | RF-004, RF-009, RF-010, RF-011, RF-012 | runtime, approvals, signed bundle, rollback | `` | + +Highest permitted deployment mode from these results: + +`` + +## Approvals + +Signatures cover this exact model-card digest and weight digest. + +| Role | Approver/receipt | Decision | Timestamp | +|---|---|---|---| +| Model owner | `` | `` | `` | +| Clean-room custodian | `` | `` | `` | +| Data steward/privacy | `` | `` | `` | +| Security owner | `` | `` | `` | +| Runtime owner | `` | `` | `` | +| Legal release owner | `` | `` | `` | + +## Citation + +Use the final public model name, exact version, weight digest, repository commit, +and model-card URL: + +```bibtex +@software{, + title = {}, + author = {}, + year = {}, + version = {}, + url = {}, + note = {Weights SHA-256: ; source commit: } +} +``` + +## Change history + +| Card version | Weight digest | Change | Gate impact | Date | +|---|---|---|---|---| +| `` | `` | Initial candidate | All gates evaluated independently | `` | diff --git a/docs/security/ruview-forecast-clean-room.md b/docs/security/ruview-forecast-clean-room.md new file mode 100644 index 000000000..4e6d5bee0 --- /dev/null +++ b/docs/security/ruview-forecast-clean-room.md @@ -0,0 +1,511 @@ +# RuView Forecast clean-room, data, and release protocol + +**Status:** Proposed and mandatory for any artifact claiming independent +development under ADR-348. + +**Evidence status:** This document defines controls. It does not assert that the +controls have passed, that a trained model exists, or that any forecasting +capability is measured. Every ADR-348 gate remains **OPEN / UNMEASURED** until a +signed evidence bundle proves otherwise. + +## 1. Purpose and authority + +This protocol governs the specification, implementation, training, evaluation, +hosting, and release of the independent Rust multivariate forecaster defined by +[ADR-348](../adr/ADR-348-independent-rust-multivariate-forecasting.md). +It applies to source, issue and review text, prompts, dependencies, datasets, +intermediate tensors, checkpoints, containers, CI caches, RuVector indexes, +fal.ai jobs, benchmark results, model cards, and published artifacts. + +The protocol is intentionally stricter than the minimum conditions for using +Apache-licensed source. Its purpose is to preserve evidence that the RuView +implementation and weights were independently created. It is not a legal +opinion. The legal release owner must approve the current licenses, contributor +exposure record, patent review, trademark review, datasets, and intended launch +jurisdictions before production. + +## 2. License boundary as of 2026-09-01 + +The authoritative public materials state two different license surfaces: + +1. The [Google TimesFM repository](https://github.com/google-research/timesfm) + identifies its source code as Apache-2.0. +2. The [TimesFM 3 checkpoint license](https://huggingface.co/google/timesfm-3.0-pytorch/blob/main/LICENSE) + permits non-commercial, non-production use and restricts commercial use, + distribution, and use to train, fine-tune, or distill another commercial + model. + +The checkpoint license defines a derivative broadly enough that relying on its +logic, parameters, or model-generated material creates avoidable contractual +risk. No contributor may accept gated model terms on behalf of the project or +its owner without written authorization. + +The [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) permits +source reproduction and derivative works subject to its conditions, including +license delivery, change notices, retention of applicable notices, and NOTICE +handling. Apache also provides a limited contributor patent grant and no general +trademark grant. If maintainers ever choose an Apache-derived port, it must use +a separate branch, attribution path, artifact name, and superseding ADR. It +must not be described as the clean-room implementation. + +Publicly described ideas, procedures, processes, systems, and methods are not +the same as copied expression. United States copyright law states the +idea-expression boundary in [17 U.S.C. section 102(b)](https://www.copyright.gov/title17/92chap1.html). +That does not settle contract, patent, trademark, database-right, trade-secret, +or non-U.S. law questions. + +## 3. Non-mixing rule + +Only one of these paths may govern a source tree: + +| Path | Source basis | Required representation | +|---|---|---| +| Independent implementation | Approved abstract specification, public papers/articles, general literature, RuView requirements, approved data | "Independently developed and independently trained" after all gates pass | +| Apache-derived port | Apache-licensed Google source with full license and notice compliance | "Apache-derived Rust port" with attribution | + +There is no hybrid path. A permissive source license makes porting possible; it +does not make a port independent. If exposure cannot be bounded, maintainers +must either quarantine and rewrite the affected component or reclassify the +whole affected component under the Apache-derived path. + +## 4. Roles and separation + +| Role | May access | Must not access or do | +|---|---|---| +| Clean-room custodian | Exposure records, allowlist, scan reports, quarantined evidence | Contribute core model implementation after reviewing prohibited source fragments | +| Specification reviewer | Frozen public-paper/article allowlist, RuView requirements, generic literature | Google TimesFM implementation, tests, configs, checkpoint files, model outputs; contribute implementation | +| Rust implementer | Approved independent specification, RuView schemas, approved generic dependencies | TimesFM source, tests, configs, weights, outputs, line-by-line/API translation, reference differential testing | +| Data steward | Dataset source, contracts, consent, privacy and transformation records | Approve unknown or incompatible rights; infer that a software license covers data | +| Training operator | Signed code/container/config and approved dataset manifests | Change code/data/config outside a new receipt; add an unapproved parent checkpoint | +| Independent evaluator | Frozen candidate, frozen evaluation manifests, approved baselines | Return reference predictions or implementation hints to developers; tune against the test set | +| Legal release owner | Complete evidence bundle and private legal analysis | Treat clean-room checks as patent clearance or dataset approval by implication | + +One person may hold multiple roles only where the forbidden-access rules remain +true. The strongest separation uses different people for public specification +and implementation. A specification reviewer who has inspected prohibited +source cannot become a core implementer for the independent path. + +## 5. Source classification + +### 5.1 Allowed + +- The public TimesFM research articles and peer-reviewed papers, frozen by URL, + retrieval date, and SHA-256 digest. +- General time-series, transformer, probabilistic forecasting, normalization, + missing-data, and calibration literature. +- RuView requirements, independently written feature schemas, and existing + RuView contracts whose provenance already satisfies repository policy. +- Generic Rust dependencies whose license, source, and purpose are recorded and + approved. +- Public benchmark specifications and datasets after the dataset gate passes. +- Published aggregate competitor results used only as contextual background. + +### 5.2 Conditional + +- Code implementing generic algorithms may be used only after license review + and must be declared as a dependency or attributed source. It cannot be used + to claim that every line was independently authored. +- TimesFM versions up to 2.5 may have permissively licensed code or weights, but + they remain outside the independent implementation and training path. An + evaluator may use an approved permissive baseline only after the candidate + is frozen, and its output cannot become training data or implementation + feedback. +- A contributor with prior exposure to TimesFM code must disclose the exact + material and date. The custodian and legal owner decide whether the person is + limited to non-implementation work or the affected component must be + reclassified. +- Synthetic data is allowed only when source data, generator code/model, + provider terms, output rights, and intended commercial use all pass review. + +### 5.3 Prohibited + +- TimesFM 3 weights, parameters, checkpoints, tensor dumps, checkpoint + configuration, hidden activations, embeddings, gradients, or derived + fingerprints. +- TimesFM 3 forecasts, quantiles, scores, probabilities, recommendations, or + synthetic labels in development, training, tuning, or release evidence. +- Fine-tuning, distillation, imitation, teacher-student learning, differential + testing, behavioural cloning, or hyperparameter search against TimesFM 3. +- Manual, mechanical, or AI-assisted translation of Google source, tests, + comments, constants, APIs, file layout, or diagrams. +- Third-party code, datasets, or checkpoints whose origin may contain copied or + distilled TimesFM material without documented permission. +- Model, dataset, or service terms labelled non-commercial, research-only, + evaluation-only, no-derivatives, no-ML, no-production, or unknown. +- Confidential third-party information or material obtained under an agreement + that does not authorize this implementation and commercial use. + +## 6. Exposure intake and contributor attestation + +Before receiving implementation access, every contributor completes an +exposure intake covering: + +- TimesFM repositories, model cards, gated checkpoint files, local caches, + notebooks, issues, tutorials, and generated outputs viewed or downloaded; +- whether gated terms were accepted and for which individual or entity; +- source or output material placed in an LLM, IDE assistant, retrieval index, + prompt, or code-generation session; +- prior work for Google, a competitor, customer, or other party involving + confidential forecasting implementation; +- datasets, pretrained models, and external code expected in the contribution. + +Each pull request also carries a signed attestation: + +```text +RuView Forecast clean-room attestation v1 + +Contributor: +Employer or represented entity: +Role: +Covered commits: +Prior TimesFM exposure: NONE or complete disclosure +Approved sources used: +Datasets introduced: +AI tools used: +Prompt/session manifest digest: + +I attest that the covered contribution was created from the approved +specification and listed sources. I did not access, copy, translate, decompile, +query, distill, or use TimesFM 3 implementation material, configuration, +weights, parameters, outputs, or internal representations. I did not place +prohibited material in an AI tool context. I disclosed all contrary facts +above. Every dependency and dataset I introduced has documented rights for its +stated use. + +Signature: +Timestamp: +``` + +A DCO `Signed-off-by` line does not replace this attestation. The attestation +digest, not private identity material, is referenced from the public release +manifest. + +## 7. Specification controls + +The custodian freezes an allowlist before implementation begins. Every entry +records title, canonical URL or DOI, publication date, retrieval time, content +digest, license or access terms, reviewer, and the abstract requirement it +supports. + +The specification may state mathematical functions, tensor roles, input/output +invariants, resource bounds, and RuView-specific requirements. It must not +include copied source, comments, tests, distinctive identifiers, constants, +checkpoint dimensions extracted from files, API compatibility requirements, or +Google diagrams. RuView API names must be derived from the local domain. + +Changes to the specification after test evaluation receive a new version and +must not encode held-out answers. A material architecture revision consumes a +new untouched holdout or remains labelled exploratory. + +## 8. AI coding-tool controls + +An AI assistant can unintentionally defeat source separation by retrieving or +reproducing reference code. For sessions contributing implementation: + +1. Disable web retrieval and repository indexing outside the approved local + tree where the tool supports it. +2. If a user describes the goal as a port, clone, reproduction, or emulation, + stop and restate the task as independently authored RuView functional + requirements. Record that wording in the exposure manifest; do not use it + as permission to retrieve reference implementation material or pursue + behavioral equivalence. +3. Do not attach Google code, checkpoint metadata, outputs, screenshots, or + detailed third-party implementation summaries. +4. State the independent specification and prohibited-source boundary in the + session instructions. +5. Retain a bounded prompt and tool-source manifest digest without committing + raw private transcripts. +6. Record the model/tool version and whether retrieval was enabled. +7. Treat unexplained code that resembles a prohibited implementation as an + incident, not as a harmless generated suggestion. + +AI-generated code receives the same authorship, license, security, and +similarity review as human-authored code. + +## 9. Dataset licensing and privacy gate + +No dataset enters preprocessing, a RuVector index, training, calibration, or +evaluation until a data steward records affirmative answers to every applicable +gate: + +| Gate | Required evidence | Automatic rejection examples | +|---|---|---| +| Origin | Named owner/provider, acquisition method, immutable source digest | Scrape or file of unknown origin | +| Training rights | Written right to use for ML training and the intended commercial purpose | Research-only, NC, no-ML, evaluation-only | +| Derivatives | Right to transform, derive windows/features, and create model artifacts | No-derivatives or ambiguous custom terms | +| Redistribution | Whether raw, transformed, manifests, and weights may be redistributed separately | Assumption that public access means redistribution | +| Attribution | Exact attribution and notice obligations | Missing author/source/version | +| Database rights | Jurisdiction and database-right review where applicable | Unreviewed EU database extraction | +| Consent and contract | Collection authority, participant/customer consent, purpose, controller/processor roles | Customer telemetry without explicit model-training authority | +| Sensitive inference | Classification of occupancy, routines, location, health-adjacent and biometric implications | Unbounded identity or health inference | +| Minimization | Required fields only, pseudonymization, retention and deletion schedule | Raw CSI/person identifiers retained without need | +| Split integrity | Site/subject/session/device/time-block lineage and overlap report | Contiguous or derived-window leakage across splits | +| Synthetic lineage | Generator, seed data, service terms, output rights, seeds/config digest | Restricted teacher or prohibited source data | + +Typical disposition guidance: + +- Owned data, CC0/public-domain data, and data under an explicit commercial ML + contract may pass after privacy and provenance review. +- CC BY data requires attribution plus database and downstream-weight review. +- MIT and Apache are software licenses and do not automatically license nearby + data. +- NC, ND, research-only, evaluation-only, no-ML, no-production, and unknown + terms fail unless a separate written commercial grant is obtained. + +Every transform is content-addressed. Normalizers, feature selectors, +calibrators, thresholds, and RuVector indexes fit training data only. Raw +sequence overlap and derived-window overlap are both checked. A parent record +in one split makes all overlapping descendants ineligible for another split. + +## 10. Hosted training boundary + +The Linux machine and fal.ai are execution environments, not sources of +authority. A hosted job is accepted only when: + +- the exact source commit, Cargo lockfile, compiler, container digest, data + manifests, configuration, seeds, and parent checkpoint digest are signed + before upload; +- provider terms, retention, region, subprocessors, logs, cache deletion, + confidentiality, output ownership, and provider-training reuse have been + reviewed for the data classification; +- credentials are short-lived, least-privilege, excluded from images and logs, + and rotated after suspected exposure; +- customer-derived data is not uploaded before controller/processor and + transfer requirements pass; +- emitted checkpoints and logs are hashed immediately and compared with the job + receipt; +- the downloaded artifact is scanned before entering the trusted release + boundary; +- a hosted provider cannot promote, sign, or activate a model. + +Hosted and local runs are comparable only when their governed input identities +match. Numerical nondeterminism must be documented; it does not permit an +unrecorded dependency, data, or configuration change. + +## 11. Provenance manifest + +The release bundle contains a machine-readable manifest with at least: + +```yaml +schema_version: +artifact: + id: + version: + git_commit: + source_spec_digest: + cargo_lock_digest: + rust_toolchain: + target_triple: + build_container_digest: + sbom_digest: + +contributors: + - pseudonymous_id: + role: + exposure_class: + attestation_digest: + signed_at: + +references: + - title: + canonical_uri: <url-or-doi> + publication_date: <date> + retrieved_at: <rfc3339> + content_digest: <sha256> + license_or_terms: <identifier> + allowed_use: <purpose> + reviewer: <id> + +datasets: + - dataset_id: <id> + version: <version> + owner: <owner> + origin: <uri-or-contract-id> + license_or_contract: <identifier> + commercial_ml_allowed: <true> + redistribution_allowed: <true|false> + consent_basis: <identifier> + privacy_class: <class> + jurisdictions: [<jurisdiction>] + retention_policy: <policy-id> + source_digest: <sha256-or-merkle-root> + transform_digest: <sha256> + split_digest: <sha256> + record_count: <count> + +training: + random_initialization: true + parent_checkpoint: null + code_commit: <sha> + config_digest: <sha256> + rng_seeds: [<seed>] + hardware: <inventory> + provider: <local-linux|approved-provider> + provider_job_id: <non-secret-id> + environment_digest: <sha256> + started_at: <rfc3339> + ended_at: <rfc3339> + forbidden_artifact_scan_digest: <sha256> + leakage_scan_digest: <sha256> + +evaluation: + frozen_manifest_digest: <sha256> + baseline_artifacts: [<id-and-digest>] + metric_schema: <version> + report_digest: <sha256> + calibration_report_digest: <sha256> + domain_holdouts: [<site|subject|session|device>] + +release: + weights_digest: <sha256> + model_card_digest: <sha256> + clean_room_report_digest: <sha256> + signature_key_id: <public-key-id> + rvf_signature: <signature> + source_license: <spdx-expression> + weights_license: <exact-identifier> + data_steward_approval: <signed-receipt> + security_approval: <signed-receipt> + legal_approval: <signed-receipt> +``` + +Secrets, raw personal identifiers, contract text, raw customer data, and +private contributor identities stay outside the public manifest. The manifest +references controlled records by digest or approval ID. + +## 12. Source, dependency, and artifact checks + +The custodian records tool versions, rules, timestamps, and complete findings. +At minimum: + +- search the working tree and Git history for prohibited model IDs, URLs, + filenames, license strings, binary signatures, and unexpected large files; +- scan source similarity against the prohibited implementation in an isolated + custodian environment; implementation contributors do not receive reference + fragments from the report; +- manually adjudicate every material similarity match and record independent + origin, generic necessity, rewrite, or reclassification; +- run dependency license, source, duplicate-version, and vulnerability policy + checks and generate a CycloneDX or SPDX SBOM; +- inspect container layers, CI and provider caches, mounted volumes, model/data + buckets, notebooks, logs, and local tool indexes for restricted artifacts; +- verify that every checkpoint parent is an approved RuView artifact or the + declared random initialization; +- verify that model loading performs no network access and accepts no embedded + executable operator or arbitrary path; +- verify that public packages, endpoints, docs, and metadata do not use Google + trademarks as a product identity. + +Keyword scans are a tripwire, not proof of independence. References in this +governance document are expected and must be path allowlisted. The final result +depends on provenance, exposure records, review, and the absence of prohibited +material in implementation/training paths. + +## 13. Model card and claim gate + +Every candidate copies and completes +[`../huggingface/RUVIEW_FORECAST_MODEL_CARD_TEMPLATE.md`](../huggingface/RUVIEW_FORECAST_MODEL_CARD_TEMPLATE.md). +No placeholder, `UNMEASURED`, unknown license, missing digest, or missing +approval may be silently deleted. It must instead be resolved or retained as an +explicit release blocker. + +All accuracy, latency, memory, power, cost, and generalization values are +labelled `MEASURED`, `SYNTHETIC`, `CLAIMED`, or `UNMEASURED` under repository +policy. `MEASURED` requires a reproducer, immutable inputs, exact artifact, and +named hardware/environment. A model-card benchmark does not create authority +for a medical, emergency, security, or autonomous-action claim. + +## 14. Naming, trademark, and comparative statements + +Approved project identities include `RuView Forecast` and crate names derived +from the RuView domain. Do not use `TimesFM`, `Google`, or a confusingly similar +mark in crate names, binaries, model slugs, endpoints, logos, icons, or product +headlines. Do not copy diagrams or visual branding. + +Comparative documentation may accurately identify an external model and its +version when necessary, with a statement that RuView Forecast is independently +developed and not affiliated with or endorsed by Google. Comparative claims +need identical datasets, metric definitions, permitted model use, and evidence +labels. + +Before public naming or commercial release, complete a professional trademark +clearance that includes federal, state, common-law, domain, and relevant +international sources. The [USPTO clearance guidance](https://www.uspto.gov/trademarks/search/comprehensive-clearance-search-similar-trademarks) +is a starting point, not a complete legal opinion. + +## 15. Residual patent and jurisdiction risk + +Clean-room evidence does not prevent patent infringement. Before production, +patent counsel performs a claim-level freedom-to-operate review covering at +least temporal patching, multivariate/variate attention or mixing, masked +horizon prediction, known-future covariates, probabilistic heads, normalization, +retrieval augmentation, and relevant training procedures. Searches include +assignees, inventors, continuations, unpublished timing uncertainty, and launch +jurisdictions. The [USPTO Patent Public Search](https://www.uspto.gov/patents/search/patent-public-search) +supports preliminary searching but is not a freedom-to-operate opinion. + +The Apache patent grant applies only within its stated scope. An independent +implementation must not assume it inherits that grant. Public papers may be +prior art yet still coexist with earlier, pending, territorial, or narrower +claims. + +## 16. Contamination incident response + +Treat any prohibited source, output, model, data, or unreviewed exposure as a +provenance incident: + +1. Stop affected implementation, training, evaluation feedback, and release. +2. Preserve hashes, timestamps, actors, locations, access records, and affected + lineage. Do not erase the audit trail. +3. Quarantine the material and revoke it from build, data, model, cache, and + retrieval paths. +4. Identify every affected commit, specification revision, dataset transform, + RuVector index, checkpoint, benchmark, and descendant artifact. +5. Rotate credentials if a provider, cache, or secret may be exposed. +6. Have the custodian and legal owner choose one disposition: proven + non-impact, clean rewrite by unexposed contributors, full retraining from the + last clean ancestor, or explicit Apache-derived reclassification. +7. Repeat all affected ADR-348 gates and record the incident and closure + receipts. + +If prohibited output becomes a label or tuning signal, all descendant weights +are affected. Code deletion alone cannot repair model lineage; retraining from +an approved clean initialization is required. + +## 17. Requirement-mapped acceptance checklist + +| Check | ADR-348 requirements | Gate | Acceptance condition | +|---|---|---|---| +| CR-01 source allowlist | RF-001, RF-002 | G0 | 100% of specification sources frozen, hashed, licensed/termed, and approved | +| CR-02 contributor exposure | RF-001, RF-002 | G0 | 100% of contributors have current intake and signed attestation; every disclosure has a disposition | +| CR-03 prohibited-artifact scan | RF-001 | G0 | Zero unresolved prohibited artifacts or outputs across source, history, caches, containers, jobs, data, indexes, and checkpoints | +| CR-04 similarity review | RF-001, RF-002 | G0 | Every material match adjudicated without exposing implementers to reference fragments | +| CR-05 dependency/SBOM | RF-002, RF-004 | G1, G2 | Zero unknown or denied dependency licenses; vulnerability policy passes; signed SBOM exists | +| CR-06 dataset rights | RF-003 | G1 | 100% of bytes map to approved manifests; zero NC, ND, research-only, no-ML, no-production, or unknown terms | +| CR-07 privacy | RF-003, RF-009 | G1, G5 | Consent/contract, minimization, tenant, retention, deletion, transfer, and sensitive-inference review approved | +| CR-08 checkpoint lineage | RF-001, RF-002, RF-010 | G1 | Every checkpoint reaches approved random initialization with no prohibited parent or label | +| CR-09 split isolation | RF-007, RF-008 | G3 | Zero raw/derived overlap across frozen site/subject/session/device/time splits; split-scoped RuVector indexes verified | +| CR-10 hosted parity | RF-010 | G1, G5 | Local/hosted receipts bind identical governed inputs; provider terms and deletion evidence approved | +| CR-11 model card | RF-002, RF-007, RF-011 | G3-G5 | Template complete; every value evidence-labelled and reproducible; all blockers explicit | +| CR-12 trademark and patent | RF-001, RF-011 | G5 | Naming clearance and claim-level freedom-to-operate disposition signed for launch jurisdictions | +| CR-13 rollback | RF-012 | G5 | Contamination and runtime rollback drills preserve observations and evidence while disabling forecast authority | +| CR-14 bounded forecast contract | RF-004, RF-005, RF-006 | G2 | Property/fuzz/replay evidence proves finite bounded inputs, abstention, offline execution, immutable observation linkage, and derived-only evidence labels | +| CR-15 downstream authority | RF-006, RF-009, RF-012 | G4, G5 | No sensing-server hook in the initial PR; any later bridge proves forecast/LLM output cannot mutate observations or acquire prohibited action authority | +| CR-16 claims and rollout | RF-011, RF-012 | G3-G5 | Every claim has an allowed evidence label and reproducer; mode transitions cannot outrun the highest passed gate | + +Any failed or missing check blocks the mapped gate. A maintainer waiver cannot +convert a prohibited license into permission or an unmeasured capability into a +measured claim. + +## 18. Release record + +The legal, data, security, model, and runtime owners sign the same immutable +release digest. Their approval covers the exact source, data, configuration, +weights, model card, SBOM, intended use, deployment mode, and jurisdictions. +Changing any governed input creates a new candidate and invalidates inherited +approval. + +The first production review must also answer one explicit question: does the +business value of the measured forecast exceed the added privacy, operational, +compute, and legal burden compared with deterministic baselines? If not, the +correct outcome is to keep forecasting offline. diff --git a/docs/security/ruview-forecast-threat-model.md b/docs/security/ruview-forecast-threat-model.md new file mode 100644 index 000000000..05552ba20 --- /dev/null +++ b/docs/security/ruview-forecast-threat-model.md @@ -0,0 +1,1027 @@ +# RuView Forecast security and privacy threat model + +- **Status:** normative implementation and release gate +- **Date:** 2026-09-01 +- **Scope:** `ruview-forecast-core`, `ruview-forecast-model`, + `ruview-forecast-train`, RuView API integration, RuVector retrieval, and hosted + training on fal +- **Extends:** ADR-260, ADR-262, ADR-271, ADR-295, ADR-299, ADR-304, ADR-305, + ADR-319, and ADR-348 +- **Evidence grade:** design review plus repository inspection; no production + forecast artifact or hosted training run was inspected + +## Security decision + +Forecasting may be implemented only as a bounded, advisory, derived-evidence +subsystem. Real RF data remains subject to RuView's person-data rules even when +it is converted into a time series. Hosted training is an external trust +boundary, not an extension of the RuView edge. + +The initial release MUST enforce these rules: + +1. **Hosted v1 is synthetic-pretraining only.** No real RuView RF-derived data + of any privacy class, external dataset path/bytes, `DataPolicy`, tenant/site/ + device/session identifier, split membership, or RuVector record may be sent + to fal. All real-data training runs stay on the operator-controlled Linux + host. +2. Fal receives only a bounded, server-authored synthetic generator recipe, + seed, model/optimizer profile, clean-room identity, and budget. Conditional + P1/P2 export requires a future privacy/DPA design decision and implementation; + it is ADR-only and MUST fail closed in v1 even when a receipt field is + present. The deployment build context is part of this egress boundary: it + MUST be produced from an exact-file/three-Rust-source-tree clean tracked-source archive and MUST + NOT upload the operator's working tree, untracked files, local requests, + datasets, credentials, or unrelated RuView fixtures. +3. A fal result is untrusted input. It MUST be downloaded into quarantine, + size-bounded, hashed, schema-checked, evaluated locally, and signed by a + RuView release key that never leaves RuView control before activation. +4. Forecasts MUST remain `DERIVED_FORECAST` evidence. They MUST NOT overwrite + observations, become `MEASURED`, raise the evidence level of an unauthenticated + RF source, or independently authorize a life-safety or physical action. +5. Every mutating route, including submit, cancel, delete, retrain, stage, + promote, activate, and rollback, requires `sensing:admin` by default. +6. Invalid or ambiguous security state fails closed. Missing history, stale or + OOD input, invalid model output, and unavailable retrieval cause abstention; + they never cause a synthetic/live fallback or reuse of a prior forecast as a + current observation. + +## Review and clean-room exposure statement + +This review inspected RuView repository policy, privacy, authentication, +attestation, evidence, witness, artifact-verification, dataset-ingest, and CI +code, together with fal's public operational documentation. It did **not** +inspect, copy, download, execute, or query Google TimesFM source code, +configuration, tests, checkpoints, weights, activations, or outputs. No Google +model output or similarity oracle was used. ADR-348's independently authored +RuView requirements are the forecasting specification for this review. + +The review was read-only until this document was requested. The checked base +state was commit `e04f269f1a76eb6ed4e8cd33e2ccfeda6fd391a3`; the reviewed +`v2/Cargo.lock` SHA-256 was +`ae160f76b1a0bfb2dd1917543c40003f7a2abd3599eee2340d92e143dca35c2c`. +No forecast crate existed in that base commit. New, uncommitted forecast files +appeared during the review and received a follow-up read-only pass. They were an +in-progress working-tree snapshot without a stable commit identity. The +implementation findings below must be rechecked against the final diff; no gate +is claimed to pass merely because a draft type or test exists. + +## In-progress implementation findings + +| ID | Severity | Snapshot evidence | Required closure | +|---|---|---|---| +| RFSEC-001 | **Critical** | `ruview-forecast-core/src/series.rs:147-210` carries evidence state but no ADR-260 privacy/tenant policy; `ruview-forecast-train/src/config.rs:218-359` binds dataset path/size/hash but no privacy class, tenant/account/workspace, purpose, consent/DPA, retention or export decision. | Bind real data to a validated local policy, but make hosted v1 a separate synthetic-recipe-only DTO. Reject every real/external dataset, including P1/P2, before network I/O. Evidence class is not privacy class. | +| RFSEC-002 | **Critical** | `ruview-forecast-model/src/artifact.rs:42-105,145-175` checks a BLAKE3 weights digest declared inside the same unsigned manifest. An attacker able to replace both weights and manifest satisfies that check. | Treat it only as an untrusted candidate, then require a domain-separated canonical manifest signature, local Ed25519 signer allowlist, compatibility/finite checks and a nonconstructible activated-artifact wrapper. Fal never receives the release key. | +| RFSEC-003 | **High** | `ruview-forecast-model/src/config.rs:126-217` has no hard dimension/activation caps, and expressions such as `3 * patch_len`, `6 * d`, `2 * d`, `3 * d`, `q * r` and `q * horizon` can overflow before the checked helpers. Patch/layer allocation is not bounded by parameter count alone. | Add per-dimension, parameter, patch/token, activation-byte and total-work caps. Use checked arithmetic for every intermediate before model allocation. | +| RFSEC-004 | **High** | `ruview-forecast-model/src/network.rs:43-60,278-361` validates tensor shapes but not batch/total cells, finite values, binary masks, bounded ages or finite outputs. | Require a validated input wrapper or adapter gate and post-inference finite/order validation; malformed data must return an error/abstention before backend panic or propagation. | +| RFSEC-005 | **High** | `ruview-forecast-train/src/artifact.rs:117-146,245-264` canonicalizes a path and later opens it; existing final paths are reopened without no-follow semantics. Directory or final symlinks can race the check. | Use capability/open-at/no-follow handling, verify the opened handle, and keep root/job directories mode `0700` with files `0600`. Do not trust a lexical/canonical path checked before open. | +| RFSEC-006 | **High** | `ruview-forecast-train/src/config.rs:396-421` checks path metadata and then calls `std::fs::read`, permitting replacement or growth between the check and unbounded read. | Open once, inspect the handle, read through `take(MAX+1)`, and reject any over-limit/trailing byte before parsing. | +| RFSEC-007 | **High** | `ruview-forecast-train/src/artifact.rs:77-90` directly deserializes an absolute `provider_path`, size and digest with public fields. | Return a bounded opaque artifact ID or validated provider-relative handle. Treat all fal descriptors as wire DTOs and revalidate before URL construction or file access. | +| RFSEC-008 | **Medium** | `ruview-forecast-train/src/config.rs:12-16,297-339` permits a 512 GiB dataset, 10,000 epochs and batch 8,192 without a tenant budget, wall-time or total-work estimate. | Apply deployment-lower byte limits plus checked work/GPU-hour/currency, attempt, wall-time and concurrency quotas before submission. | +| RFSEC-009 | **High** | `.github/workflows/ruforecast-ci.yml:206-232` exercises `cpu`/`cli`, while train's `server` and `fal-client` feature surfaces are not compiled, tested or linted. | Add mock-network jobs for `server,fal-client`, security-contract tests, blocking secret/privacy gates, and fuzz/property jobs before merging external integration. | + +### Closure re-review ledger + +The table above records the first implementation snapshot. The final PR pass +has the following disposition. “Addressed” means the reviewed software path is +present; it is not production approval and does not replace an operational +test on the exact release artifact. + +- **RFSEC-001 is addressed for the v1 software boundary.** Local series and + requests carry a constructor-validated `DataPolicy`. The hosted request is a + separate closed synthetic-only DTO that cannot serialize a policy, path, + dataset bytes, split, or tenant/site/device/session identity. Provider + retention, account isolation, and deletion remain operational release gates. +- **RFSEC-002 is partially addressed.** Candidate bytes have no activation + authority. Activation requires a domain-separated Ed25519 envelope, local + signer allowlist, compatibility checks, and nonconstructible activated type. + The current public activation policy still accepts caller-supplied epoch, + runtime, clock and schema values. Production remains blocked on a + service-owned policy constructor that binds the compiled runtime, trusted + clock and selected schema, plus a durable atomic store for + `{release_epoch, same_epoch_digest}` and the last-known-good model that + survives crash/restart tests. +- **RFSEC-003 is addressed for the two activatable v1 profiles.** Dimensions, + patch count, parameters, activation cells, input cells, and checked forward + multiply-adds are bounded. Only exact `tiny_ci` and `large_linux` profiles + validate; a low-parameter/high-attention adversarial configuration is + rejected. Batch work is checked before training allocation and again at the + model boundary. A future profile is a security-reviewed code change. +- **RFSEC-004 is partially addressed.** `ModelInput` has private fields and + validates batch, shapes, cells, finite values, masks, ages, validity rows, + activation work, and output finiteness/order. Local JSONL is byte- and + shape-gated before tensor creation. The library still accepts already-created + Burn tensors, so a production network adapter must add a bounded DTO-to-tensor + gate before exposing inference to untrusted input. No such route ships here. +- **RFSEC-005 and RFSEC-006 are addressed for local files.** Requests and + datasets use no-follow/nonblocking capability opens, regular/single-link + checks, bounded streaming reads, exact size/digest checks, private modes, and + atomic fixed-name publication. Every JSONL epoch re-hashes the exact bytes + consumed; publication failure removes the complete candidate set. +- **RFSEC-007 is addressed.** Provider responses carry only an opaque validated + job/kind identity and four fixed descriptors. URLs and local paths are built + from configured roots plus those types, then size/hash checked in quarantine. + The hosted receipt body currently receives only a minimal request-digest and + unsigned/untrusted projection check; strict full receipt schema and lineage + binding to the job, build, schema, manifest, candidate, checkpoint, terminal + status and reconciled cost remain a production import gate. This PR has no + promotion or import path, so provider bytes retain no activation authority. +- **RFSEC-008 is partially addressed.** Dataset, optimizer, activation, memory, + artifact, checkpoint, wall-time, billable-time, and request spend fields are + bounded; hosted execution is one-slot and does not automatically resubmit an + ambiguous submit. `max_micro_usd` is not provider-enforced, provider billing + is not reconciled, and account-wide concurrency/attempt reservation is an + external control. Hosted production remains blocked. +- **RFSEC-009 is addressed for PR CI.** The workflow separately compiles, + tests, and lints backend-free, CPU, RuVector, hosted `server,fal-client`, and + compile-only CUDA/WGPU surfaces, with clean-room, secret, and hosted-wire + scans. Fuzzing, container scanning, live Fal, GPU runtime, and measured + resource evidence remain release gates. + +The local policy retention timestamp is an admission and execution deadline, +not a deletion scheduler. Local JSONL, candidates, checkpoints, manifests and +receipts persist until the operator's storage policy removes them. A production +service remains blocked on authenticated lifecycle ownership, crash/restart +deletion tests and an auditable deletion receipt. + +Positive snapshot patterns worth preserving include private fields plus +constructor-validating custom `Deserialize` in forecast core, finite/shape +validation in `TimeSeries`, canonical domain-separated SHA-256 digests, +`deny_unknown_fields` on training requests, fixed typed devices/model profiles, +create-new temporary artifact files, streamed dataset hashing, fixed artifact +filenames, and the test that rejects a dataset symlink escaping its root. These +reduce risk but do not close the findings above. + +### RuVector feature release gate + +The optional RuVector adapter remains **blocked from production activation**, +but the evaluation-only construction path now pins `ruvector-core = 2.3.0` +with default features disabled and `memory-only,hnsw` enabled. It accepts at +most 1,024 elements and rejects an index before `VectorDB::new` unless checked +accounting for embedding copies, HNSW links/layers, forecast patches, record +metadata, and bounded search scratch fits a 128 MiB ceiling: + +```text +embedding bytes + HNSW links/levels + forecast-patch bytes + + opaque record/digest metadata + search scratch +``` + +Search fixes `ef_search=64`, validates bounded `k`, deduplicates provider +results, and rechecks finite distances and split scope. The constructor is +`pub(crate)`, so no downstream production service can instantiate the adapter +in this change. A production adapter still needs authenticated scope creation, +concurrency/deadline control, measured allocator/RSS evidence, and a deployment +memory reservation. + +Record identifiers MUST be fixed opaque digests (for v1, exactly 64 lowercase +hex SHA-256 characters) generated from a domain-separated local identifier. +Arbitrary caller strings such as names, email addresses, device IDs, or room +labels must never enter the vector database or an `AnalogMatch`. A public query +must not supply its own scope authority: the service derives tenant/account/ +workspace and split from an authenticated principal plus a locally selected +split, then hands the adapter a nonconstructible verified scope. + +The adapter no longer uses the unsafe `":memory:"` string. It creates a private +mode-0700 temporary directory and mode-0600 empty probe file, and fails closed +if the memory-only backend writes it. `clear()` drops the entire HNSW database +before rebuilding, because per-record removal retains vectors in the pinned +provider. Explicit close/drop removes the capability. This proves the reviewed +feature path did not write the probe; it does not promise memory zeroization or +crash cleanup. Persistent or production use still requires tenant-scoped +retention, deletion, audit, and residual-data tests. + +### Training runner release gate + +The runner output remains candidate evidence, not a release. Provider JSON is +decoded into a private strict wire and reconstructed against the configured +app plus expected request, job, worker build, build manifest, expiry, and four +artifact kinds. `production_signed` must be false. Downloads enter a +capability-confined quarantine and are re-hashed; no signing/promotion command +is included in this change. + +Preflight uses checked parameter, activation, forward-work, decoded-shuffle, +serialization, and overhead estimates, then enforces optimizer-step, +checkpoint, cumulative artifact, retention, and whole-invocation wall bounds. +The multiplier remains a conservative engineering estimate, not measured +allocator proof. Retention and wall bounds are checked around every durable +publication. Normal final publication also requires cancellation to remain +clear; an observed cooperative cancellation may intentionally publish only its +bounded checkpoint. A returned partial failure rolls back the fixed outputs, +while crash-atomic four-file publication remains a production gate. Peak RSS +and large-profile throughput remain unmeasured release evidence. + +The canonical run identity binds optimizer, checkpoint, model, device, budget, +source commit, lockfile, toolchain/target, container, data/generator, split, and +feature-schema identities. Hosted build values are compiled into the worker and +must match the request. Missing build environment values remain visibly +`UNVERIFIED`; they are not fabricated. A hosted candidate has no release epoch +or signing authority. + +## Repository security anchors + +The forecast implementation MUST preserve these existing RuView contracts: + +| Contract | Repository evidence | Forecast consequence | +|---|---|---| +| Raw CSI is person data | `docs/adr/ADR-299-csi-data-incident-repo-controls.md:10-17` records that raw CSI encodes breathing, movement, and presence. | Raw capture is never a normal fixture, log, hosted-training payload, or release artifact. | +| Only synthetic or expressly consented minimal CSI fixtures may be committed | `ADR-299:23-28`, `.github/workflows/csi-data-policy.yml:36-42`, and `scripts/csi-data-policy-check.sh:31-38,150-184`. | Forecast tests generate data in code or use an approved minimal fixture. | +| Privacy is semantic, not only a file format | `docs/adr/ADR-260-rufield-mfs.md:160-175` defines P0-P5 and requires provenance. | Derived windows are classified by information content and intended use, not merely relabelled P1. | +| Live egress is fail-closed | `docs/adr/ADR-262-rufield-ruview-integration.md:26-41` maps derived biometric content to P4/P5 and permits only P1/P2 on its unattended surface. | The feature exporter must reject a field whose effective privacy exceeds its destination policy. | +| The current live UDP source is not authenticated | `v2/crates/wifi-densepose-sensing-server/SECURITY.md:5-14,32-49`. | A capture from the current data plane is `LiveUnverified`; forecasting cannot mint L5 provenance from it. | +| Mutations are admin by default | `v2/crates/wifi-densepose-sensing-server/src/bearer_auth.rs:433-470` documents the prior training-route scope hole and now defaults every mutation to `sensing:admin`. | New forecast routes inherit this polarity; no route-prefix denylist is permitted. | +| Audience and scope are independent | `v2/crates/ruview-auth/src/verify.rs:40-53,165-246`; scopes are exact in `v2/crates/ruview-auth/src/principal.rs:78-86`. | Require a configured nonempty client allowlist, exact scope, account, and workspace binding. | +| Verified values should be constructed only after verification | `v2/crates/ruview-auth/src/principal.rs:28-30` and `v2/crates/ruview-attest/src/lib.rs:420-476`. | Remote DTOs must not deserialize directly into trusted forecast, artifact, attestation, or evidence types. | +| Artifacts load only after verification | `v2/crates/wifi-densepose-physics/src/learned/model.rs:58-66`. | Parse model tensors only after byte, signer, schema, and compatibility gates. | +| Production signature policy rejects unsigned input | `v2/crates/homecore-plugins/src/verify.rs:9-35,95-190`. | Forecast production has no `AllowUnsigned` path. | +| Parsers are bounded and panic-free | `v2/crates/wifi-densepose-train/src/dataset/widar.rs:8-17,105-116,175-250`. | All remote formats get byte, record, shape, and allocation caps before decoding. | + +## Assets and security objectives + +| Asset | Objective | +|---|---| +| Raw and derived RF windows | Confidentiality, tenant isolation, consent, minimization, bounded retention, lineage | +| Labels and future covariates | Integrity, rights provenance, no future leakage, no subject crossover | +| Fal API credential | Confidentiality, least scope, rotation, no client/log/artifact exposure | +| RuView release signing key | Local-only custody, non-exportability where practical, separate dev/prod keys | +| Training configuration and code | Immutable identity, clean-room provenance, reproducibility | +| Checkpoint and release model | Byte integrity, signer authenticity, schema compatibility, anti-rollback | +| Forecast and quantiles | Finite/bounded values, calibration identity, source/evidence honesty | +| RuVector analogue index | Split isolation, tenant isolation, deletion, neighbour-ID confidentiality | +| Job and promotion records | Attribution, idempotency, cost limits, append-only audit | + +## Data classification and permitted destinations + +Classification follows ADR-260 and is monotonic: a transform does not lower a +class merely because the output has fewer columns. A lower class requires a +documented privacy-reduction transform and a test demonstrating that prohibited +information is absent. When inputs differ, use the most restrictive applicable +source, semantic, consent, and destination class. + +| Data | Minimum class | Local Linux | Fal | Repository / CI artifact | +|---|---:|---:|---:|---:| +| Raw CSI, phase/amplitude frames, radar cube | P0 | Temporary, encrypted, authorized | **DENY** | **DENY** | +| Derived non-identity radio statistics with identifiers and precise time removed | P1 | Allow | **DENY in v1** | Synthetic/minimal only | +| Occupancy, motion, bed-exit, room routine series | P2 | Allow | **DENY in v1** | Synthetic/minimal only | +| Anonymous aggregates | P3 | Allow | **DENY in v1** | Aggregate reports only | +| Breathing, heart rate, gait, sleep, fall/health target | P4 | Explicit consent; restricted | **DENY by default** | **DENY** | +| Named/persistent person, face/re-ID/subject-linked series | P5 | Identity binding and audit | **DENY** | **DENY** | +| Hash/HMAC/pseudonym derived from a tenant, site, room, device, session, subject or local job label | At least source class | Policy-scoped only | **DENY in v1** | **DENY** unless an approved aggregate | +| RuVector neighbour IDs/distances | At least source class | Tenant/split scoped | **DENY by default** | Hashed aggregate metrics only | +| Model trained on customer-derived data | At least Restricted until leakage review | Quarantine/release store | **DENY in v1** | No weight blob in ordinary CI | +| Forecast of a P4/P5 target | P4/P5 | Restricted | Not a hosted-training response payload | No raw values in telemetry | +| Synthetic generated windows and labels | Content-dependent privacy; L0 evidence | Allow | Allow | Minimal deterministic fixtures allowed | + +There is no real-data fal exporter in v1. Local dataset manifests MUST retain +privacy, tenant/account, consent/purpose, schema, time-range, and provenance +bindings, but none of those records or dataset shards cross the hosted boundary. +The separate fal DTO carries only a server-approved synthetic generator ID and +version, bounded generator parameters, seed, model/optimizer profile, clean-room +identity and budget. It has no generic path, byte blob, URL, policy, source, +tenant or split field. + +Model weights can memorize training data. Until membership-inference, +canary-extraction, nearest-neighbour, and subject-linkage checks pass, a model +trained on customer-derived data inherits a restricted handling policy even if +its tensor format contains no explicit identifier. + +## Trust boundaries + +```mermaid +flowchart TD + S["RF sensor / capture"] --> I["RuView ingest + privacy gate"] + I --> L["Local dataset + Linux trainer"] + G["Synthetic recipe"] --> F["fal external processor"] + C["Clean source archive"] --> F + F --> Q["RuView quarantine + verification"] + L --> Q + Q --> R["Signed local runtime"] + R --> D["Evidence / RuVector / RuVLLM consumers"] +``` + +| Boundary | Untrusted side | Required gate | +|---|---|---| +| TB1 sensor to ingest | UDP frame/source | Source state, frame bounds, device attestation when available, replay/freshness; otherwise `LiveUnverified` | +| TB2 ingest to dataset | Feature producer and labels | Schema, privacy/consent, tenant, timestamp, finiteness, split and provenance validation | +| TB3 RuView to fal | Hosted provider, network, shared account resources | Synthetic-only DTO, approved app, least-scope key, storage/ACL/retention controls, cost authorization | +| TB4 fal webhook/result to RuView | HTTP headers/body/URLs/provider state | Raw-body signature, timestamp, expected request/user, replay/idempotency, body cap, fixed JWKS | +| TB5 model bytes to parser/runtime | Downloaded bytes and manifest | Streaming length/hash, canonical Ed25519 signature, signer allowlist, schema/tensor/finite checks | +| TB6 client to forecast/admin API | Bearer token and request | ES256 verifier, exact client/scope, tenant binding, rate/quota, strict DTO validation | +| TB7 forecast to downstream systems | Model output and optional explanation | Derived-evidence label, uncertainty/abstention, no numeric mutation, no autonomous action | +| TB8 API/service to RuVector | Caller identity, query, record ID, index capacity | Principal-derived verified scope, opaque digest IDs, pre-allocation byte reservation, bounded search | +| TB9 source tree to fal image builder | Docker context, package manager, base image and build logs | Clean tracked-file allowlist, no local data/secrets, digest-pinned inputs, SBOM/provenance, secret and CSI policy gates before upload | + +## STRIDE analysis + +| STRIDE | Threat | Boundary | Impact | Required control | +|---|---|---|---|---| +| Spoofing | Forged live CSI creates a plausible training trajectory | TB1 | Poisoned model and false provenance | Integrate ADR-305 attestation; until then retain `LiveUnverified`, device/network allowlist is not authentication | +| Spoofing | Forged fal callback or result for another job/account | TB4 | Attacker-selected artifact, cost/audit confusion | Verify fal Ed25519 raw-body signature, ±300 s timestamp, exact expected user/request ID and one-time nonce; idempotency ledger | +| Spoofing | Token minted for another Cognitum client or tenant | TB6 | Cross-product or cross-tenant access | Nonempty exact client allowlist; bind account/workspace from verified `Principal`, never from request JSON | +| Spoofing | Unknown artifact publisher | TB5 | Malicious model activation | Local release signer allowlist; no caller-constructible verification receipt | +| Tampering | Dataset, labels, split manifest, config, or seed changes | TB2/TB3 | Poisoning, leakage, irreproducible claims | Content-address every input; canonical signed run manifest; verify before training and again in the receipt | +| Tampering | Model or normalization is changed independently of manifest | TB5 | Silent wrong inference or parser attack | Signature covers canonical manifest and hashes of all payloads; verify actual bytes before decode | +| Tampering | Witness/evidence JSON forges a higher evidence level | TB4/TB7 | False assurance | Deserialize bounded wire DTOs and reconstruct via validated constructors; never trust serialized `VerifiedMeasurement`/ledger objects | +| Repudiation | User disputes a costly run, cancellation, or promotion | TB3/TB6 | Spend and governance dispute | Append actor `sub/account/workspace/jti`, request/idempotency IDs, code/data/config digests, provider job ID, state transition, time and reason | +| Repudiation | Provider result cannot be linked to submitted inputs | TB3/TB4 | Unverifiable checkpoint | Signed training receipt binds request ID, source commit, lock/container, data/config hashes, hardware and output digests | +| Information disclosure | Real RF-derived data or identifiers reach request history, fal CDN, logs, or shared `/data` | TB3 | Person/health/identity data incident | Synthetic-only v1 DTO; private app auth; no input upload or corpus in `/data`; bounded synthetic metadata; redaction; candidate quarantine and deletion reconciliation | +| Information disclosure | Broad Docker context uploads untracked local data, requests, credentials, or approved-but-real fixtures | TB9 | Hosted data incident before the runtime request exists | Build from a generated clean tracked-source allowlist or a locally built digest-pinned image; enumerate and scan the exact context before upload | +| Information disclosure | Forecasts reveal routines or RuVector neighbour identities | TB6/TB7/TB8 | Occupancy/health inference and tenant leak | `sensing:read`, principal-derived tenant/split scope, fixed opaque neighbour digests, field allowlist, aggregate telemetry only | +| Information disclosure | Artifact memorizes individual samples | TB5/TB7 | Model inversion/membership inference | Restricted artifact policy, privacy tests, canaries, aggregation, no raw-output debug endpoint | +| Denial of service | Huge context/horizon/series/quantile/shape values | TB2/TB5/TB6 | OOM, overflow, CPU starvation | Hard caps, checked products/byte counts before allocation, bounded worker pool, deadlines and quotas | +| Denial of service | NPY/safetensors/archive length or decompression bomb | TB2/TB5 | OOM/disk exhaustion | Header/file/entry/ratio caps; streaming parser; reject archives by default | +| Denial of service | Webhook retry storm or bogus JWKS key IDs | TB4 | Request/egress amplification | Body cap, verify before parse, idempotent `2xx` after durable terminal state, cached fixed JWKS with fetch floor/timeouts | +| Denial of service | Unlimited hosted jobs/retries | TB3/TB6 | Financial exhaustion | Per-tenant concurrency, daily GPU-hour/currency cap, explicit max attempts, no automatic resubmit from unknown state | +| Elevation of privilege | Read token calls train/promote/delete route | TB6 | Model replacement or spend | All non-GET/HEAD/OPTIONS admin by default, with no forecast mutation allowlist | +| Elevation of privilege | User controls path, URL, operator, or deserialized trusted type | TB3/TB5/TB6 | SSRF, arbitrary overwrite, verification bypass | Server-selected endpoints/paths/operators; capability directory; bounded DTO; no embedded code or pickle | +| Elevation of privilege | Release key exists on fal worker | TB3 | Provider/job compromise can sign production model | Fal emits an unsigned candidate; local promotion service alone owns release signing capability | + +## API and in-process validation limits + +The first implementation SHOULD use the following hard ceilings. Deployment +configuration may lower them. Raising them is a security-sensitive change that +requires memory/latency measurements and review. All products are computed with +`checked_mul`/`checked_add` before reserving or allocating. + +| Field / resource | Initial hard limit | Validation | +|---|---:|---| +| HTTP JSON request body | 1 MiB | Reject at transport before buffering/deserialization | +| Fal Direct Server synthetic body | 64 KiB | Body layer rejects before `serde_json`; the closed DTO is revalidated before work | +| Fal queue/status/result JSON | 1 MiB | Stream chunks into a bounded buffer; provider error bodies are discarded | +| Local training request file | 1 MiB | Open once with no-follow/nonblocking flags, require a regular file, read through `take(MAX+1)` | +| Local JSONL shard | 8 GiB | Size and SHA-256 bind the already-open capability; no whole-shard buffering | +| One JSONL window | 8 MiB and 2,000,000 value/target cells plus equal-shape binary masks | Bound the aggregate line before serde, then validate exact model-derived shapes, masks, finite values and train split | +| In-memory JSONL shuffle | 64 decoded windows | Include worst-case decoded allocation in the request memory reservation | +| Webhook raw body | 256 KiB | Reject before hashing/parsing; fal artifact bytes are fetched separately | +| Activation/local manifest or metadata document | 1 MiB | UTF-8 plus strict schema and `deny_unknown_fields`; the quarantined hosted receipt's full strict schema/lineage validation remains an open production import gate | +| Series/targets | 256 | Unique bounded identifiers; release target is smaller (ADR-348 G5 uses 32) | +| Context steps | 16,384 | Strictly monotonic timestamps, exact cadence contract or explicit missing mask | +| Forecast horizon | 4,096 | Nonzero and supported by the artifact/model card | +| Past/future covariates | 256 each | Allowlisted schema fields; future source and availability receipt required | +| Quantiles | 32 | Finite, strictly increasing, unique, each strictly inside `(0,1)` | +| Batch size | 64 in-process; 1 for remote interactive API initially | Enforced before tensor creation | +| Transport/API aggregate numeric cells (future inference adapter) | 4,194,304 | A production DTO-to-tensor route must count values, masks, covariates, retrieval additions, and outputs together before allocation; this PR ships no such untrusted route and its libraries retain per-object caps | +| Identifier | 128 UTF-8 bytes | Reject empty security IDs; normalize only where the schema specifies it | +| RuVector record ID | 64 lowercase hexadecimal bytes | Domain-separated opaque SHA-256 digest only; never a subject, tenant, device, room, or path string | +| Free-text reason/label | 1,024 UTF-8 bytes | Never interpreted as path, URL, format string, or log template | +| Candidate model bytes | 256 MiB | Stream to quarantine with a byte ceiling and expected length | +| Optimizer updates / wall time | 2,000,000 / 24 hours | Deployment lowers both; check at every batch and before checkpoint/serialization | +| Local training memory request | 96 GiB maximum | Conservative checked estimate must cover parameters, optimizer, autodiff, decoded shuffle, tensors and serialization; measured RSS must stay below it | +| Tensor count | 4,096 | Unique canonical names; exact expected set for production model | +| Tensor rank / dimension | 8 / 1,048,576 | Shape product and byte range checked; no overlap or out-of-bounds offsets | +| Concurrent local inference | bounded pool, default `min(physical_cores, 8)` | Queue cap and rejection/backpressure, not unbounded task creation | +| Concurrent hosted training | 1 per tenant, deployment-configured global cap | Idempotency key and budget reservation before submission | +| Redirects for dataset/artifact fetch | 0 | A new URL is a new authorization decision; default is no redirects | + +Signed envelopes are length-gated and signature-checked through a borrowed +candidate slice. The implementation does not copy or parse the candidate model +record until the signer is allowlisted and the Ed25519 signature succeeds. + +Additional validation is mandatory: + +- Reject NaN, positive/negative infinity, denormals if the model contract + disallows them, and values outside per-feature physical/normalization ranges. + If `-0.0` and `+0.0` are semantically equal, canonicalize them before hashing; + otherwise reject the noncanonical representation. +- Reject duplicate, descending, or far-future timestamps; integer arithmetic + must not wrap duration or index calculations. +- Reject duplicate target/covariate names, target/covariate aliasing that leaks + a future target, and masks whose shape differs from their values. +- Reject unsupported schema/model/runtime versions rather than coercing them. +- Do not silently skip malformed training rows. A configured small error budget + may quarantine bad records, but exceeding it fails the job and the receipt + records every exclusion. +- Treat empty, stale, low-coverage, OOD, or provenance-incomplete context as an + abstention state. Do not auto-fill with simulated sensing. +- Validate quantile ordering after inference. A documented crossing correction + may run, but non-finite or structurally invalid output is an abstention. +- Bound every string, vector, map, log field, metric label cardinality, and + RuVector result count before allocation. + +These limits are deliberately much tighter than the dimension-only guards in +`v2/crates/wifi-densepose-train/src/config.rs:35-69`, whose individually large maxima can +still produce unsafe products. The existing `from_json` at `config.rs:256-271` +also reads the whole file before parsing; the forecast boundary must cap file +metadata and body bytes first. + +## Authentication, authorization, tenancy, and spend + +### RuView callers + +- Reuse `ruview-auth`; do not add a second JWT implementation. +- Fix ES256 as the accepted algorithm and require an access token, matching + `v2/crates/ruview-auth/src/verify.rs:10-25,174-217`. +- `allowed_client_ids` MUST be nonempty in a production forecast deployment. +- Exact `sensing:read` authorizes reading a tenant-scoped forecast and job + status. It does not authorize exporting a dataset or seeing raw provider logs. +- Exact `sensing:admin` authorizes dataset export, job submit/cancel/delete, + candidate download, evaluation, stage, promote, activate, and rollback. +- Account/workspace comes only from the verified principal. If a resource ID + resolves to another account/workspace, return a flat not-found/unauthorized + response and do not disclose its existence. +- A new mutating endpoint is admin-gated automatically. This preserves the + fail-closed route policy documented in `bearer_auth.rs:433-470`. + +### Fal credentials and budget + +- Use a dedicated fal account/app for forecast training and a dedicated + **API-scoped** `FAL_KEY` for run submission. Fal documents that API scope can + call models/deployed endpoints, while ADMIN additionally controls deployments + and apps. Keep a separate short-lived ADMIN key only in the deployment + workflow. +- Load `FAL_KEY` from a secret manager/environment into the server process. Do + not accept it in API input, put it in a command line, serialize it, print it, + attach it to an error, or send it to a browser. +- Redact authorization headers, presigned query strings, CDN tokens, JWTs, + cookies, and key-like environment values in `Debug`, tracing, panic, and CI + output. +- Reserve a tenant budget before submission. Bind currency/GPU-hour ceiling, + machine class, maximum wall time, maximum attempts, and expected data/artifact + bytes into the authorized run manifest. +- Cancellation that cannot be confirmed enters `CancellationUnknown`; it is not + automatically resubmitted and does not release the full budget reservation + until provider state is reconciled. + +## Fal privacy, storage, and callback policy + +Fal is an untrusted processor even when its cryptographic callback verifies. +The provider signature authenticates delivery; it does not approve data use, +prove training correctness, or sign a RuView release artifact. + +Hosted v1 deliberately retains only the bounded synthetic request and result +metadata needed by Fal's queue/status/result protocol. It does not set +`X-Fal-Store-IO: 0`, because doing so would remove the stored result that the +typed reconciliation flow must retrieve. No customer-derived data or identity +is permitted in that metadata. Every submit MUST instead set +`X-Fal-No-Retry: 1` and a bounded request timeout, and a connection loss during +that one send becomes `REMOTE_UNKNOWN` rather than an automatic resubmit. + +Both ephemeral and deployed Direct Server commands MUST select the exact +`fal_app.py::run_server` symbol and pass `--auth private`. The checked-in Fal +app configuration also declares private auth. A live acceptance test MUST +prove an unauthenticated request receives 401/403 before hosted use is approved. +The launcher enforces these argv values locally, but no live Fal probe was run +for this PR. + +Hosted v1 performs no input upload. The synthetic corpus is generated inside +the worker from the bounded recipe. Any future input-upload design requires a +new ADR and must apply private ACL and expiry **to the upload itself**; fal's +request lifecycle header does not retroactively cover a previously uploaded +input. + +Training datasets MUST NOT use `/data` as ordinary working storage. Fal +documents `/data` as shared across all apps/runners in the account and persistent +until manual deletion. Hosted v1 generates its corpus in bounded process memory +and writes only four fixed candidate outputs below a fresh opaque `/data` +namespace. The worker schedules best-effort deletion at the digest-bound +expiry, but a crash can defeat it. The candidate must be copied into RuView +quarantine before expiry, and provider-side lifecycle/deletion reconciliation +is a blocking production gate. + +The deployed Direct Server has a 3,600-second provider timeout, while the +closed hosted DTO permits at most 3,300 seconds of wall and billable work. The +remaining 300 seconds is reserved for response handoff and best-effort cleanup; +it is not available to the optimizer. + +Customer-derived P1/P2 upload is unimplemented and denied in v1. A future ADR +must resolve the applicable contract/DPA, region, subprocessors, +retention/deletion capability, incident channel, data-steward approval, +purpose/consent authority, and an authenticated non-caller-constructible export +decision before any implementation work begins. + +Webhook handling MUST: + +1. Read no more than 256 KiB of raw body. +2. Require all four fal webhook headers. +3. Reject timestamps outside ±300 seconds. +4. Hash the exact raw body with SHA-256 and verify the documented newline-bound + message with Ed25519 against the fixed + `https://rest.fal.ai/.well-known/jwks.json` key set. +5. Require the expected fal user, submitted request ID, tenant-bound local job, + and current non-terminal state. +6. Atomically record the request ID/body hash as processed before returning a + terminal `2xx`; duplicate deliveries return the already recorded outcome. +7. Treat URL strings and artifact metadata inside the signed body as untrusted + values requiring their own validation. + +Fal currently documents retrying failed/non-2xx deliveries with backoff, up to +31 retries while the result remains stored. Idempotency is therefore a required +correctness and availability property, not an optimization. + +Polling the fixed queue endpoint is acceptable for an initial deployment and +reduces inbound surface. It does not remove result validation, tenant binding, +idempotency, or artifact quarantine requirements. + +## SSRF and outbound network controls + +The safest API accepts no caller-provided URL. A request contains an opaque +dataset/artifact ID, and the server resolves it to a configured object store or +known fal endpoint. + +If URL fetching remains necessary, enforce all of the following before opening +a socket: + +- exact `https` scheme, exact allowlisted ASCII host, port 443, no username, + password, fragment, embedded credentials, `data:`, `file:`, or alternate + scheme; +- no suffix matching such as `ends_with("fal.ai")`; `fal.ai.attacker.example` + is not fal; +- resolve every address and reject loopback, private, link-local, multicast, + unspecified, reserved, carrier-grade NAT, and cloud metadata destinations for + IPv4 and IPv6; +- no redirects by default. If enabled later, apply the full authorization check + to every hop and cap hop count; +- protect against DNS rebinding by connecting only to an address from the + validated resolution while preserving TLS hostname verification; +- fixed connect/read/total timeouts, response-header cap, declared and streamed + byte cap, and no transparent decompression unless separately bounded; +- stream into an application-created quarantine file while hashing; never load + a remote artifact body fully into memory; +- do not expose `ruview-auth`'s generic JWKS URL constructor to API input. The + current `UreqFetcher` performs a GET against the supplied URL at + `v2/crates/ruview-auth/src/jwks.rs:265-267,342-350`; forecast JWKS endpoints are fixed + operator configuration with scheme/host validation. + +## Filesystem and path traversal controls + +- Requests contain logical IDs, not dataset roots, checkpoint paths, output + names, archive entry paths, or temporary directories. +- Create a private per-job directory under a configured capability root. Use a + random server-generated name, mode `0700`, files mode `0600`, create-new + semantics, and no symlink following. +- Write to a new temporary file, `fsync` when durability is required, verify, + then atomically rename to an immutable content-addressed path. Never activate + or parse the remote download in place. +- Reject absolute paths, `..`, root/prefix components, NUL, platform device + names, alternate data streams, symlinks, hardlinks, sockets, FIFOs and device + files. Canonicalize the existing parent and prove it remains below the + capability root. +- Do not extract archives in the first release. If an archive is later required, + cap entries, per-entry and total bytes, compression ratio and nesting; reject + duplicate normalized names, links and special files before writing anything. +- Dataset discovery must not repeat the permissive pattern in + `v2/crates/wifi-densepose-train/src/dataset.rs:337-432`, which accepts a caller + path and lacks explicit canonical-root/symlink and checked-prefix-sum controls. + +## Deserialization and trusted-type controls + +Serde derives do not enforce Rust constructors. This matters at a remote +boundary: + +- `ruview-attest::VerifiedMeasurement` derives `Deserialize` and has public + fields at `v2/crates/ruview-attest/src/lib.rs:258-274`; deserializing it + directly would bypass signature, payload, sequence, and freshness checks + performed at `:420-476`. +- `ruview-evidence::EvidenceContext` and `AccuracyMetrics` derive `Deserialize` + even though constructors/validation bound IDs and finite ranges at + `v2/crates/ruview-evidence/src/lib.rs:85-145,157-213`. +- `EvidenceLedger` derives `Deserialize` at + `v2/crates/ruview-evidence/src/lib.rs:368-377`, so direct decoding can bypass + append-time sequence/capacity discipline and allocate an attacker-sized + record vector. +- `WitnessChain` derives `Deserialize` at + `v2/crates/ruview-witness/src/lib.rs:673-685`; `verify()` at `:773-821` checks + hash links and order, not the original RF signature behind a deserialized + `VerifiedMeasurement`. + +Forecast code MUST therefore use small untrusted wire structs with +`#[serde(deny_unknown_fields)]`, bounded strings/collections, and no security +defaults. Apply the transport byte cap before `serde_json`; deserialize, then +reconstruct domain objects through validating constructors and cryptographic +verification. A type named `Verified*`, `Trusted*`, `Principal`, `Evidence*`, or +`Activated*` must not implement a public untrusted decode path that can create +the trusted state directly. + +Model payloads MUST be data-only. Reject pickle, Python objects, executable +plugins, arbitrary operator graphs, shell snippets and provider-supplied code. +Safetensors or another non-executable tensor container is still untrusted: cap +its header before allocation, require the exact tensor set, reject duplicate or +overlapping ranges, check dtype/rank/shape/product/offset, and reject non-finite +parameters before construction. + +## Security-relevant Rust crates + +Prefer workspace-pinned dependencies with `default-features = false` and only +the features required by the owning crate. Every addition expands the advisory, +license, source and build-script surface and therefore passes cargo-deny review. + +| Crate | Security role and constraint | +|---|---| +| Existing `ruview-auth` | Cognitum ES256 access-token verification and `Principal`; do not duplicate JWT policy in forecast crates | +| Existing `ruview-attest`, `ruview-evidence`, `ruview-witness` | Provenance/evidence integration after local validation; do not deserialize their trusted domain objects from fal/API input | +| `sha2` | SHA-256 for streamed artifact bytes, webhook body and interoperable manifest digests | +| `ed25519-dalek` | Fal webhook and local release-signature verification; use strict fixed-width key/signature parsing and a signer allowlist | +| `blake3` | Optional internal content/witness identity where the existing RuView contract already uses BLAKE3; do not silently substitute it for a declared SHA-256 field | +| `subtle` | Constant-time comparison for secret-dependent tags where an upstream verification API does not already provide it | +| `secrecy`, `zeroize` | Reduce accidental `FAL_KEY` exposure in `Debug` and memory; this does not guarantee erasure of copies held by HTTP/env libraries | +| `url`, `ipnet` | Parse once and enforce exact scheme/host/port plus IPv4/IPv6 deny ranges; URL parsing alone is not SSRF protection | +| Repository-approved HTTP client with `rustls` | Fixed endpoints, no redirects, timeouts, streamed byte ceilings and explicit proxy policy; avoid adding a second client stack without justification | +| `tempfile`, optionally `cap-std` | Private create-new quarantine files and capability-scoped filesystem access; still validate links and final location | +| `serde`, `serde_json`, `serde_path_to_error` | Small `deny_unknown_fields` wire DTOs and diagnosable rejection after a transport byte cap; serde is not an allocation bound by itself | +| `safetensors` | Non-executable tensor data; wrap it with independent header, tensor-count, range, shape, finite-value and total-byte validation | +| `tokio` synchronization/time primitives | Bounded semaphores, queue backpressure and deadlines; do not spawn unbounded blocking inference/training work | +| `proptest`, `cargo-fuzz` (dev tooling) | Boundary/state/overflow properties and parser fuzzing with synthetic-only corpora | + +Use a custom, versioned canonical manifest encoder like the length-prefixed +canonical bytes in `v2/crates/ruview-attest/src/lib.rs:188-223`. Do not add a +general canonicalization package merely to sign arbitrary input JSON. + +## Artifact integrity, authenticity, and promotion + +### Canonical manifest + +The signed release manifest MUST contain at least: + +- manifest/schema version and a domain string; +- artifact byte length and SHA-256 of the actual model payload; +- model ID/version, architecture/config digest, runtime compatibility range, + and monotonic release epoch; +- feature schema, cadence, context, horizon, target, quantile, normalization, + calibration and OOD-policy digests; +- exact tensor names, dtypes, shapes, offsets, lengths and optional per-tensor + hashes; +- training source commit, `Cargo.lock` digest, compiler/toolchain, container + digest, target/hardware, seeds, dataset/split/transform manifest digests, and + privacy/consent decision digest; +- local/fal run identifier, candidate hash, evaluation report/reproducer digest, + clean-room attestation digest, license/SPDX inventory, and SBOM digest; +- signer key ID, issued-at, optional expiry, and signature algorithm/version. + +Use one deterministic canonical encoding. The Ed25519 signature covers a +domain-separated message such as: + +```text +"ruview.forecast.artifact.v1\0" || SHA256(model_bytes) || canonical_manifest_without_signature +``` + +The manifest fields and payload digest must both be signed; a signature over +only the weight hash permits a compatible-looking manifest substitution. Hash +the actual streamed bytes and compare in constant time where the library makes +that available. Sign with a local release key after local evaluation. Fal never +receives that private key and never produces the production release signature. + +The strongest in-repository precedent is +`v2/crates/homecore-plugins/src/verify.rs:95-190`: hash actual bytes, verify an +Ed25519 signature, then apply a publisher-key allowlist, rejecting unsigned or +incomplete inputs in production. The forecast loader should also preserve +`v2/crates/wifi-densepose-physics/src/learned/model.rs:58-66`, which deserializes only a +previously verified artifact. + +`v2/crates/wifi-densepose-physics/src/learned/artifact.rs:41-105` is useful for size, +manifest and byte-hash compatibility, but its `SignatureVerification` is only a +receipt supplied by the caller; it is not itself cryptographic verification. +Do not expose the same caller-constructible gap at the remote forecast boundary. + +### Verification and activation order + +1. Authorize the local job/tenant and reserve byte budget. +2. Fetch from an allowlisted source into quarantine with no redirects. +3. Enforce streamed size and expected length; compute SHA-256. +4. Parse only the bounded manifest envelope. +5. Verify domain, manifest canonical form, Ed25519 signature and signer + allowlist. +6. Compare model and auxiliary-file hashes and lengths. +7. Enforce anti-rollback epoch and runtime/schema compatibility. +8. Parse the tensor header and validate exact tensor table/ranges/products. +9. Validate every parameter is finite and run deterministic load/self-tests. +10. Run the frozen local held-out evaluation and evidence/reproducer gates. +11. Sign the accepted release manifest locally. +12. Stage by content hash; atomically switch the active pointer while retaining + the last known-good artifact. + +Any failure leaves the candidate quarantined/rejected and the previous active +model unchanged. Startup with no valid artifact disables forecasting but does +not disable observed RuView sensing. + +## Fail-closed state machines + +### Hosted training + +```text +DRAFT -> AUTHORIZED -> SUBMITTED -> RUNNING -> RETURNED_UNTRUSTED + -> VERIFIED_CANDIDATE -> LOCALLY_EVALUATED -> LOCALLY_SIGNED -> STAGED + -> ACTIVE +``` + +Invalid transitions are errors. Authentication, privacy, budget or manifest +failure before submission returns to `DRAFT/DENIED`. Provider ambiguity enters +`REMOTE_UNKNOWN`; it does not auto-submit. Verification/evaluation failure +enters `QUARANTINED`. Only an admin action with the recorded gates transitions +`STAGED` to `ACTIVE`. + +### Runtime inference + +| Condition | Fail-closed result | +|---|---| +| No active, valid artifact | Forecast subsystem `OFF`; sensing continues | +| Unsupported schema, shape, horizon, cadence | Typed rejection; no allocation beyond bounded error | +| Missing/stale/low-coverage/OOD context | Signed abstention/unknown derived record | +| Retrieval unavailable or tenant/split receipt invalid | Run without retrieval only if the model card authorizes that ablation; otherwise abstain | +| Panic/cancel/timeout/non-finite model output | Abstain, increment bounded metric, keep observation untouched | +| Signature/hash/compatibility/anti-rollback failure | Reject candidate and preserve last known-good active artifact | +| Auth verifier unavailable before any trusted key | Deny request | +| Stale cached auth key after a prior successful fetch | Follow existing bounded `ruview-auth` cache policy; do not accept an unknown key | +| Evidence or provenance incomplete | Cap at the supported lower grade or reject; never synthesize a higher grade | +| Downstream RuVLLM unavailable | Numeric signed forecast remains unchanged; no generated substitute | + +## Required tests + +The test names below are contract IDs. The owning crate may choose idiomatic +Rust test function names, but every contract must be traceable in CI. + +The experimental PR's hosted boundary regressions use descriptive names rather +than these IDs. Their presence in CI is not contract closure: every ID remains +`OPEN` in the requirements-evidence matrix until the complete semantics and +retained machine evidence specified below exist. + +### Privacy and tenancy + +- `PRIV-001`: P0 raw CSI export to fal is rejected. +- `PRIV-002`: every real RF-derived P1-P5 feature, label, forecast, aggregate, + dataset path, or dataset byte payload is rejected by hosted v1. +- `PRIV-003`: a P1/P2 receipt cannot enable hosted export in v1; the + conditional future design remains ADR-only and unimplemented. +- `PRIV-004`: a transform cannot lower privacy by changing an enum alone; + malformed privacy payload fails rather than being relabelled and forwarded. +- `PRIV-005`: tenant A cannot address tenant B dataset, job, artifact, + RuVector neighbour, forecast, deletion, or provider log. +- `PRIV-006`: logs/metrics/errors contain no raw feature arrays, precise room or + person ID, FAL key, bearer token, presigned query, CDN token, or signature + private material. +- `PRIV-007`: tracked-file policy passes with only synthetic minimal fixtures; + test the existing `scripts/csi-data-policy-check.sh` self-test and tracked + check. +- `PRIV-008`: caller-controlled `synthetic`, `deidentified`, privacy-class, or + opaque receipt fields cannot select the hosted path. The exact serialized fal + payload contains no external dataset URL/path/bytes, tenant/account/workspace, + site/room/device/session identifier, `DataPolicy`, raw split membership, or + other field absent from the synthetic recipe allowlist. +- `PRIV-009`: a hosted run ID is a fresh synthetic nonce allocated by the local + reservation ledger. Changing any caller-supplied tenant/site/device/session, + subject, or local job label cannot change any hosted byte; a plain digest of a + low-entropy local identifier is still pseudonymous data and is rejected. + +### Authentication and state + +- `AUTH-001`: every forecast mutation defaults to `sensing:admin`; a newly + added POST route cannot fall through to read. +- `AUTH-002`: empty/other client ID, wrong scope, missing account, setup/workload + token, wrong algorithm, malformed token and unknown key all fail closed. +- `AUTH-003`: principal account/workspace overrides or conflicts in request JSON + are rejected. +- `AUTH-004`: a request ID or other caller-chosen correlation header is not + authentication. Direct Server train/cancel routes reject a valid-looking + request ID without the independently authenticated provider/service identity; + wrong, missing, replayed, and cross-app credentials all fail closed. +- `AUTH-005`: the hosted worker enforces an in-process live-job semaphore and + deployment-lower work/memory limits after authentication but before spawning + blocking work; retained terminal-state count is not treated as a concurrency + limit. +- `STATE-001`: every illegal job transition fails and is audit-recorded. +- `STATE-002`: duplicate submit idempotency key creates one remote job and one + budget reservation. +- `STATE-003`: ambiguous cancellation becomes `REMOTE_UNKNOWN` and does not + trigger resubmit/promotion. +- `STATE-004`: activation failure leaves the previous model active; rollback is + atomic and produces an audit record. + +### API and resource bounds + +- `BOUND-001`: property-test every dimension at `0`, max, max+1, and + `usize::MAX`; every product uses checked arithmetic and fails before + allocation. +- `BOUND-002`: reject NaN/Inf, bad ranges, duplicate/descending/future + timestamps, shape/mask mismatch, duplicate targets, invalid quantiles, and + unsupported versions. +- `BOUND-003`: a 24-hour accelerated replay has bounded RSS, queue, cache, + RuVector result count, log cardinality, and latency with no panic. +- `BOUND-004`: fuzz API JSON, manifest, safetensors header, webhook headers/body, + and any retained dataset parser. The invariant is no panic, hang, excessive + allocation, path escape, or partial trusted object. +- `BOUND-005`: malformed training rows exceed the configured error budget and + fail the run instead of silently producing a smaller biased dataset. +- `BOUND-006`: the accepted training configuration's measured peak stays within + its conservative preflight memory estimate; parameters, gradients, optimizer + state, all saved layer activations, batch tensors and serialization scratch + are included, and max+1 is rejected before backend allocation. +- `BOUND-007`: cumulative model/checkpoint/manifest/receipt bytes and checkpoint + count never exceed the request budget, including cancellation and partial + failure paths. + +### RuVector retrieval + +- `RVEC-001`: maximum dimension and element count whose product exceeds the + embedding-byte reservation is rejected before `VectorDB::new`; the estimate + also includes configured HNSW graph, patch, metadata and search overhead. +- `RVEC-002`: overflow at every intermediate in the index byte/work estimate + fails closed before allocation; boundary tests cover `0`, max, max+1 and + `usize::MAX`. +- `RVEC-003`: only exactly 64 lowercase hexadecimal record digests are accepted; + names, email addresses, tenant/device/room labels, whitespace, uppercase, + path-like values and control characters are rejected and never returned. +- `RVEC-004`: a caller cannot construct or deserialize scope authority. Tenant, + account and workspace come from the authenticated principal and split comes + from server state; cross-principal and cross-split queries fail before search. +- `RVEC-005`: search enforces bounded `k`, `ef_search`, concurrent work and a + deadline/cancellation path. Timeout or poisoned/unavailable index causes the + documented no-retrieval ablation or abstention, never cross-scope fallback. +- `RVEC-006`: production configuration rejects any adapter merely claimed to be + memory-only until non-persistence is proved; no persistent index can be + enabled without tenant-scoped expiry, deletion and audit tests. A provider + string such as `":memory:"` is not accepted as evidence: the test asserts no + file is created in the working directory or any other path and scans the + private temp root for residual record bytes after clear/drop. + +### Fal, SSRF, and filesystem + +- `FAL-001`: hosted submit carries only the closed bounded synthetic DTO, sets + `X-Fal-No-Retry: 1` plus the reviewed timeout, and intentionally leaves + `X-Fal-Store-IO` absent so queue result reconciliation works. Any real-data or + identity field is a local pre-submit failure. +- `FAL-002`: hosted v1 performs no input upload and writes no generated corpus + to shared persistent `/data`; a request containing an input URL/path/blob is + locally rejected before network I/O. +- `FAL-003`: webhook signature covers exact raw body; altered whitespace/body, + header, user, request ID, timestamp, or signature fails. +- `FAL-004`: timestamps at the documented boundary are tested; stale/future and + replayed callbacks fail or return the stored idempotent result. +- `FAL-005`: 31 duplicate deliveries yield one state transition, one artifact + fetch and no duplicate charge/promotion. +- `FAL-006`: a caller-supplied or merely syntax-valid fal owner/app is rejected; + submission is bound to a server-configured approved app and API-scoped key. + Result metadata and every artifact download stage fail closed at the exact + digest-bound expiry; status and cancellation remain available afterward only + for reconciliation/cleanup and cannot restore result authority. +- `FAL-007`: a submit timeout or connection loss enters `REMOTE_UNKNOWN`, + consumes one attempt reservation, and cannot be retried until the idempotency + ledger reconciles the original logical job. +- `FAL-008`: provider error JSON is discarded without storing or formatting its + body; only a bounded allowlisted error class may reach logs or API output. +- `FAL-009`: provider JSON cannot deserialize a trusted training outcome or set + `production_signed`; app/request/job/kind substitution and missing/extra + artifacts fail before download or promotion. +- `FAL-010`: the exact hosted image context is enumerated before upload and + contains only the three crate manifests/source trees, lockfile/build script, + and exact Fal launcher/Dockerfile inputs; tests, benches, documentation, an + untracked request, dataset, `.env`, credential, raw/minimal CSI fixture, or + unrelated repository path cannot enter the archive. +- `FAL-011`: worker build identity is selected from immutable deployment + configuration and verified by the worker against its own embedded identity; + request text cannot supply or encode that value. +- `FAL-012`: `fal run` and `fal deploy` select the exact Direct Server symbol + and private auth; an unauthenticated live probe receives 401/403 before the + hosted operational gate may close. +- `SSRF-001`: reject HTTP, file/data schemes, credentials, non-443 ports, + suffix-confusion hosts, redirect hops, loopback/private/link-local/metadata, + IPv4-in-IPv6, integer/encoded IPs and DNS rebinding. +- `PATH-001`: reject absolute paths, `..`, mixed separators, NUL, symlink/hardlink + escapes, special files, duplicate normalized archive entries, and races. +- `PATH-002`: truncated/oversized download leaves no activatable partial file; + cleanup remains below the per-job capability directory. +- `PATH-003`: artifact download accepts only a capability-scoped quarantine + handle and fixed filename; absolute/caller destination paths, symlink parents, + FIFOs, devices, sockets and cross-job descriptor substitution are rejected. + +### Artifact, deserialization, and evidence + +- `ART-001`: one changed model byte, changed manifest field, wrong payload + length/hash, unknown signer, bad signature, missing signature, wrong domain, + expired manifest and rollback epoch all reject. +- `ART-002`: reject missing/extra/duplicate tensor, wrong dtype/rank/shape, + overflowed product, overlapping/out-of-range offset, huge header, truncation, + NaN/Inf parameter, and unsupported runtime version. +- `ART-003`: fal can produce a candidate but cannot produce an activatable + production signature; assert the release private key is absent from fal and + ordinary PR CI configuration. +- `ART-004`: crash at every verification/rename/activation boundary preserves + either the previous valid state or the new fully verified state, never a + partial state. +- `ART-005`: candidate generation cannot select a production release epoch; + local promotion atomically allocates the next persisted epoch and binds the + same-epoch digest across restart. +- `DE-001`: direct JSON cannot mint `Principal`, `VerifiedMeasurement`, trusted + artifact, active model, measured evidence, or an arbitrary-capacity evidence + ledger. +- `EVID-001`: hosted completion is `CLAIMED` candidate provenance; only the + frozen local reproducer can attach measured evaluation, and an unauthenticated + RF root caps downstream evidence. +- `EVID-002`: forecast and RuVLLM explanation cannot mutate numeric observation, + forecast, quantiles, provenance hash, evidence level, or action authority. + +## CI and release gates + +Keep checks scoped to the new packages where the repository has inherited +format/advisory debt; do not claim unrelated workspace debt is green. + +```bash +cd v2 + +cargo +1.89.0 fmt \ + -p ruview-forecast-core -p ruview-forecast-model -p ruview-forecast-train \ + -- --check + +cargo +1.89.0 test --locked -p ruview-forecast-core \ + --no-default-features --lib --tests +cargo +1.89.0 test --locked -p ruview-forecast-model \ + --no-default-features --features ruvector --lib --tests +cargo +1.89.0 clippy --locked -p ruview-forecast-model \ + --no-default-features --features ruvector --all-targets -- -D warnings + +FAL_KEY='' cargo +1.89.0 test --locked -p ruview-forecast-train \ + --no-default-features --features cli,server,fal-client --lib --bins --tests +FAL_KEY='' cargo +1.89.0 clippy --locked -p ruview-forecast-train \ + --no-default-features --features cli,server,fal-client --all-targets -- -D warnings + +cargo +1.92.0 test --locked -p ruview-forecast-model \ + --no-default-features --features cpu --lib --tests +cargo +1.92.0 test --locked -p ruview-forecast-train \ + --no-default-features --features cpu,cli,server,fal-client --lib --bins --tests +cargo +1.92.0 clippy --locked -p ruview-forecast-model \ + --no-default-features --features cpu --all-targets -- -D warnings +cargo +1.92.0 clippy --locked -p ruview-forecast-train \ + --no-default-features --features cpu,cli,server,fal-client \ + --all-targets -- -D warnings +``` + +Do not replace this matrix with `--all-features`: that would conflate mutually +different backend/toolchain prerequisites and could imply CUDA validation on a +runner without the pinned CUDA 12 build environment. The dedicated CUDA job +must compile and test only after its image, toolkit, driver floor, and GPU +capability are pinned. + +Dependency and repository policy gates: + +```bash +cargo audit --file v2/Cargo.lock --json +cargo deny --manifest-path v2/Cargo.toml --config v2/deny.toml \ + check advisories bans licenses sources +bash scripts/csi-data-policy-check.sh --self-test +bash scripts/csi-data-policy-check.sh --tracked +``` + +`.github/workflows/security-scan.yml:17-42` already hard-gates the checked-in +lockfile with pinned cargo-audit `0.22.2`. Secret scanners at `:277-314` and the +Python license scan at `:316-351` are currently `continue-on-error`; forecast +paths need a blocking changed-files secret gate for `FAL_KEY`, private keys, +JWTs, CDN tokens and presigned URLs. The repository also records inherited +advisory/yanked dependency debt, so cargo-deny MUST use exact, reviewed baseline +exceptions with owner, reason and expiry, while blocking every new advisory, +unapproved source and unapproved license. + +Add these release jobs: + +- libFuzzer/cargo-fuzz targets for request, manifest, tensor header, webhook and + any archive/dataset parser; preserve crashing inputs as non-person synthetic + regression fixtures; +- proptest state machines for dimensions/products, job transitions, tenant + isolation, quantile order, path normalization and artifact crash consistency; +- container scan for the pinned fal/local training image, locked Python worker + dependencies if any, CycloneDX SBOM, and signed SLSA-style provenance; +- artifact negative-test matrix (`ART-*`) before any release-signing job; +- a clean-room manifest/contributor/source scan and assertion that no prohibited + source, weight, output or derived artifact entered code, tests, training or + evaluation; +- a data-egress test that captures the complete fal request/upload and proves + prohibited privacy classes, identifiers, tokens and missing retention/ACL + headers never cross TB3; +- a secret-isolation assertion that the release signing key is unavailable to + fal, untrusted fork PRs, test logs and artifact-upload jobs; +- image and dependency digests pinned rather than mutable tags/ranges, with + SBOM/license/advisory reports retained beside the signed release manifest. + +Production promotion is blocked unless every privacy, auth, SSRF/path, +deserialization, artifact, evidence, clean-room and CI contract above is green. +Passing synthetic tests remains `SYNTHETIC`; it is not a measured security or +accuracy claim for real RF deployments. + +## Validation performed for this review + +The deterministic repository guard was executed against the reviewed tree: + +```text +scripts/csi-data-policy-check.sh --self-test -> 6 passed, 0 failed +scripts/csi-data-policy-check.sh --tracked -> OK, no violations +``` + +The validated Ruflo CLI `3.25.6` ran read-only `--depth deep --type all` scans +against each of the three forecast crates and a separate `--type deps` scan +against `v2`; all four runs reported zero signals. Its secret command reported +zero findings but also reported scanning only four training-crate files, so +that result is coverage-limited and does not replace the PR's explicit tracked +secret/privacy scans. Ruflo did not expose an advisory-feed timestamp. The +final `v2/Cargo.lock` SHA-256 for these runs was +`78c223d4db3bfd43f0748c7baddce45df9af1357cabe5912d9cb62a2213c1957`. +Scanner output was treated as an untrusted signal and no active probe or exploit +was run. `cargo-audit`, `cargo-deny`, and `gitleaks` were not installed locally; +therefore no fresh ecosystem vulnerability result is claimed and the release +dependency gate remains open. + +## External operational references + +Fal documentation was read on 2026-09-01; provider behavior must be rechecked +before production because retention, retries, hosts and APIs can change. + +- [Fal data retention and storage](https://fal.ai/docs/documentation/model-apis/media-expiration) +- [Fal file access controls](https://fal.ai/docs/documentation/model-apis/file-access-controls) +- [Fal persistent `/data` storage](https://fal.ai/docs/documentation/development/use-persistent-storage) +- [Fal working with files](https://fal.ai/docs/documentation/development/working-with-files) +- [Fal webhook verification and retry policy](https://fal.ai/docs/documentation/model-apis/inference/webhooks) +- [Fal API key scopes](https://fal.ai/docs/documentation/setting-up/authentication) diff --git a/docs/validation/ruforecast-requirements-evidence.md b/docs/validation/ruforecast-requirements-evidence.md new file mode 100644 index 000000000..f8f6a09e9 --- /dev/null +++ b/docs/validation/ruforecast-requirements-evidence.md @@ -0,0 +1,206 @@ +# RuForecast requirement to evidence matrix + +## Authority and current state + +This matrix operationalizes ADR-348 and its ADR-349/350 children without +changing them. The ADRs own the requirements and gates. Rust tests, immutable +reports, signed receipts, and explicit human approvals supply evidence. A +checked box or green build is not permission to deploy, publish a checkpoint, +upload customer-derived data, or advance the rollout mode. + +Current state: all release and operational gates are open. No benchmark result, +trained checkpoint, external training receipt, or production authority is +recorded by this document. + +## Focused verification commands + +The feature-off contract remains on the workspace Rust 1.89 line: + +```bash +cd v2 +cargo +1.89.0 test --locked -p ruview-forecast-core --no-default-features --lib --tests +cargo +1.89.0 test --locked -p ruview-forecast-model --no-default-features --lib --tests +cargo +1.89.0 check --locked -p ruview-forecast-model -p ruview-forecast-train --no-default-features --all-targets +cargo +1.89.0 test --locked -p ruview-forecast-model --no-default-features --features ruvector --lib --tests +cargo +1.89.0 clippy --locked -p ruview-forecast-core -p ruview-forecast-model -p ruview-forecast-train --no-default-features --all-targets -- -D warnings +cargo +1.89.0 clippy --locked -p ruview-forecast-model --no-default-features --features ruvector --all-targets -- -D warnings +``` + +Burn 0.21 CPU activation uses the explicitly separate Rust 1.92 line: + +```bash +cd v2 +cargo +1.92.0 test --locked -p ruview-forecast-model --no-default-features --features cpu --lib --tests +cargo +1.92.0 test --locked -p ruview-forecast-train --no-default-features --features cpu,cli --lib --bins +cargo +1.92.0 test --locked -p ruview-forecast-train --no-default-features --features cpu --test local_jsonl_smoke -- --exact local_hash_addressed_jsonl_executes_one_real_optimizer_step +cargo +1.92.0 test --locked -p ruview-forecast-train --no-default-features --features cpu,cli --test cli_smoke -- --exact cli_smoke_trains_and_writes_the_complete_candidate_set +cargo +1.92.0 test --locked -p ruview-forecast-train --no-default-features --features cpu,cli,server,fal-client --lib --bins +cargo +1.92.0 clippy --locked -p ruview-forecast-model --no-default-features --features cpu --all-targets -- -D warnings +cargo +1.92.0 clippy --locked -p ruview-forecast-train --no-default-features --features cpu,cli,server,fal-client --all-targets -- -D warnings +``` + +The CUDA line is compile-only. It proves that the explicitly gated types build +on Rust 1.92; it is not a GPU execution, training, latency, or compatibility +claim: + +```bash +cd v2 +cargo +1.92.0 check --locked -p ruview-forecast-model --no-default-features --features cuda --lib +cargo +1.92.0 check --locked -p ruview-forecast-train --no-default-features --features cuda,cli,server --lib --bins +``` + +The hosted boundary stays backend-free and is exercised on Rust 1.89 with no +provider credential. Tests must use local mocks; compiling these features does +not authorize network use or a hosted training run: + +```bash +cd v2 +cargo +1.89.0 tree --locked -e normal,build -p ruview-forecast-train --no-default-features --features cli,server,fal-client > forecast-hosted-feature-tree.txt +! grep -Eiq '(^|[[:space:]])(burn|cubecl)(-|[[:space:]])' forecast-hosted-feature-tree.txt +FAL_KEY='' cargo +1.89.0 test --locked -p ruview-forecast-train --no-default-features --features cli,server,fal-client --lib --bins --tests +FAL_KEY='' cargo +1.89.0 test --locked -p ruview-forecast-train --no-default-features --features cli,server,fal-client privacy_external_dataset_payload_is_denied +FAL_KEY='' cargo +1.89.0 clippy --locked -p ruview-forecast-train --no-default-features --features cli,server,fal-client --all-targets -- -D warnings +``` + +The broader repository gate remains: + +```bash +cd v2 +cargo +1.89.0 test --locked --workspace --no-default-features +cargo +1.89.0 bench --locked --workspace --no-default-features --no-run +``` + +No CUDA runtime result is implied by these commands. CUDA execution needs a +separately identified GPU, driver, toolkit, source commit, container digest, +and signed training or inference receipt. + +The repository-wide security workflow separately hard-gates the checked-in +lockfile with `cargo audit --file v2/Cargo.lock --json`. The forecast workflow +also runs both modes of `scripts/csi-data-policy-check.sh`, rejects forbidden +clean-room source/import names and model blobs, checks the hosted DTO field +surface, and blocks common literal private-key, FAL-key, JWT, cloud-key and +presigned-URL patterns. These focused checks do not close dependency-license or +complete secret-scanning evidence: the repository has no committed +`v2/deny.toml`, inherited advisory/yanked-version debt needs an +owner/date/expiry baseline, and the existing general secret scanners are +non-blocking. Until dedicated retained reports exist, those parts of G0, G1, +and G5 remain open. + +### Supply-chain and secret release prerequisites + +| Check | Reproducer | Present authority | +|---|---|---| +| Advisory database | `cargo audit --file v2/Cargo.lock --json` | Existing blocking workflow; report retained | +| Advisory warnings | `cargo audit --file v2/Cargo.lock --deny warnings` | Not green by assertion; inherited warning debt needs an expiring reviewed baseline | +| Rust sources/licenses/bans | `cargo deny --manifest-path v2/Cargo.toml --config v2/deny.toml check advisories bans licenses sources` | Blocked until a reviewed `v2/deny.toml` is committed and CI pins `cargo-deny` | +| Forecast-path secrets | `gitleaks detect --no-banner --redact --source .` | Focused CI blocks common literal patterns; the broader GitLeaks job/report is still non-blocking | +| Tracked sensing data | `bash scripts/csi-data-policy-check.sh --self-test && bash scripts/csi-data-policy-check.sh --tracked` | Blocking in the focused hosted-boundary job | + +`cargo vet check` becomes a release prerequisite only if the project commits a +vet policy and review process; listing it without that policy would be a false +supply-chain claim. + +### Existing baseline debt and scope + +- [`../benchmarks/physics-pose-refinement.md`](../benchmarks/physics-pose-refinement.md) + records unrelated workspace rustfmt/warning debt, an incomplete full-workspace + test run on that authoring host, existing RustSec debt, and yanked + `spin 0.9.8` in the optional Burn graph. Forecast CI therefore uses focused + warnings-denied gates and does not describe the whole workspace as green. +- [`.github/workflows/bench-regression.yml`](../../.github/workflows/bench-regression.yml) + records an upstream stable-Rust failure in optional `ruvector-crv`; the + focused forecast benchmarks neither enable nor inherit a CRV claim. +- The current ADR corpus contains pre-existing duplicate ADR-263 and ADR-264 + numbers. The focused contract rejects a duplicate ADR-348, ADR-349 or ADR-350 + without falsely asserting that the historic corpus already passes a global + uniqueness lint. +- Shared GitHub-hosted timing is noisy by repository policy. Only benchmark + compilation gates the PR; timing logs are informational until repeated on a + named, controlled host. + +## Requirements + +| Requirement | Machine evidence | Human or external evidence | Acceptance authority | Current state | +|---|---|---|---|---| +| RF-001 | Forecast-path source and artifact scan; forbidden model/blob fixture scan; clean-room manifest schema and tamper tests | Current contributor attestations and clean-room custodian adjudication | ADR-348 G0 | OPEN | +| RF-002 | Canonical receipt round trip; digest/signature tamper negatives; source, config, dataset and checkpoint identifiers required | Reviewer verifies every dependency, reference, job and checkpoint lineage | ADR-348 G0 and G1 | OPEN | +| RF-003 | Dataset manifest rejects missing license, incompatible use, unknown privacy class and unresolved bytes | Data steward and legal approval for every immutable dataset digest | ADR-348 G1 | OPEN | +| RF-004 | Feature-off dependency boundary excludes Burn/CubeCL; fixed artifact/input CPU replay; offline load tests; bounded property and fuzz corpus | Named platform-class review and 24-hour accelerated replay | ADR-348 G2 | OPEN | +| RF-005 | Shape/product overflow, finite-value, timestamp, mask, schema, horizon, quantile and resource-limit tests | Review of production input caps and abstention thresholds | ADR-348 G2 | OPEN | +| RF-006 | Forecast evidence label is immutable; observation hash round trips; confidence cannot increase; derived values cannot overwrite observations | Evidence-engine and downstream schema review | ADR-348 G2 and G4 | OPEN | +| RF-007 | Frozen split manifest; train-only normalization/calibration; quantile order; interval coverage and loss by horizon/domain | Independent evaluation report on untouched site/session/device holdouts | ADR-348 G3 | OPEN | +| RF-008 | Per-split RuVector index isolation; no overlapping horizon or holdout neighbour; paired retrieval-off/retrieval-on report | Reviewer verifies index manifest and ablation comparability | ADR-348 G3 | OPEN | +| RF-009 | Capability tests prove the forecast and RuVLLM explanation have no actuator, spending, access-control, emergency or model-promotion authority | Downstream policy owner approves any advisory consumer | ADR-348 G4 | OPEN | +| RF-010 | Local and hosted receipts compare source, lock, container, data, config and initial-weight digests; provider result is untrusted until verified | Provider retention, log, credential and data-processing review | ADR-348 G1 and G5 | OPEN | +| RF-011 | Benchmark report binds commit, lock, toolchain, host, backend, config, corpus, command and evidence label | Maintainer adjudicates claim label and model card language | ADR-348 G3 through G5 | OPEN | +| RF-012 | Mode transition and rollback state-machine tests; failed activation retains the prior artifact; raw sensing continues | Signed rollback drill and authorized mode-transition record | ADR-348 G5 | OPEN | + +## Governed training requirements + +These rows trace ADR-349. A mock-backed test can close a software subcondition, +but cannot close the hosted operational evidence named in the ADR. + +| Requirement | Machine evidence | Human or external evidence | Acceptance authority | Current state | +|---|---|---|---|---| +| FT-001 | Synthetic hosted DTO and reconstructed local request bind the same generator, model, optimizer and build identities; external manifests are rejected | Reviewer compares immutable local and provider receipts | ADR-349 and G1 | OPEN | +| FT-002 | Unknown-field and arbitrary command/image/URL/environment/path negatives | Endpoint capability and worker-image review | ADR-349 and G2 | OPEN | +| FT-003 | Concurrent/retry/lost-response idempotency state-machine tests | Real provider ambiguous-retry drill and cost reconciliation | ADR-349 and G1/G5 | OPEN | +| FT-004 | Budget boundary and over-budget rejection tests | Provider price/bill fixture review and bounded real-run receipt | ADR-349 and G5 | OPEN | +| FT-005 | Authenticated cancellation, checkpoint and terminal-state property tests | Real hosted cancellation and orphan-job drill | ADR-349 and G2/G5 | OPEN | +| FT-006 | Missing/duplicate/truncated/tampered fixed-kind export matrix | Quarantine/export receipt review | ADR-349 and G1/G2 | OPEN | +| FT-007 | Hosted-signing denial, local verification and atomic promotion/rollback tests | Release-key isolation approval and rollback drill | ADR-349 and G1/G5 | OPEN | +| FT-008 | Captured egress and log-redaction tests for credentials/privacy/retention fields | Provider DPA, region, retention, reuse and deletion review | ADR-349 and G1/G5 | OPEN | + +## Predictive-memory and explanation requirements + +These rows trace ADR-350. This PR creates no sensing-server, RuVector or RuVLLM +runtime authority; the rows remain open until a separately reviewed bridge +provides the evidence. + +| Requirement | Machine evidence | Human or external evidence | Acceptance authority | Current state | +|---|---|---|---|---| +| PM-001 | Cross-tenant/split/version/time-scope query negatives | Signed index-manifest and tenancy review | ADR-350 and G3/G5 | OPEN | +| PM-002 | Property tests reject holdout identities and overlapping contexts/horizons | Frozen leakage-report review | ADR-350 and G3 | OPEN | +| PM-003 | Bounded zero/error/success retrieval receipts and tamper tests | Retrieval receipt schema approval | ADR-350 and G2/G3 | OPEN | +| PM-004 | Same-example, same-artifact paired retrieval-off/on evaluator | Frozen paired ablation and overhead report | ADR-350 and G3 | OPEN | +| PM-005 | Envelope signature mutation, expiry, tenant, key and replay negatives | Local signing/key-rotation receipt | ADR-350 and G2/G5 | OPEN | +| PM-006 | Evidence-monotonicity property tests reject measured-observation promotion | Evidence-engine owner review | ADR-350 and G2/G4 | OPEN | +| PM-007 | Numeric/unit/provenance mutation corpus fails closed | RuVLLM adapter review | ADR-350 and G4 | OPEN | +| PM-008 | Default-deny capability and attempted-escalation tests | Downstream policy/capability review | ADR-350 and G4/G5 | OPEN | +| PM-009 | Tenant deletion, retention and access-audit tests | Privacy and membership/extraction-risk approval | ADR-350 and G5 | OPEN | + +## Threat-model contract coverage + +The IDs below are defined by +[`../security/ruview-forecast-threat-model.md`](../security/ruview-forecast-threat-model.md). +The feature job compiles and runs the current mock-backed suite, but an ID stays +open until the named negative test and its retained CI report exist. + +| Contract IDs | Required machine evidence | Current state | +|---|---|---| +| PRIV-001 through PRIV-009 | Egress-denial, tenant-isolation, log-redaction, nonce-isolation and tracked-data-policy negatives | OPEN | +| AUTH-001 through AUTH-005; STATE-001 through STATE-004 | Fail-closed identity/scope/service-auth tests and property-tested idempotent transition machine | OPEN | +| BOUND-001 through BOUND-007 | Boundary/property cases, parser fuzzing, cumulative resource limits and bounded accelerated replay | OPEN | +| RVEC-001 through RVEC-006 | Scope-authority construction denial, identifier/privacy bounds, isolation, cancellation and non-persistence evidence | OPEN | +| FAL-001 through FAL-012; SSRF-001; PATH-001 through PATH-003 | Mock request capture, exact-body webhook/replay cases, app/build/expiry binding, URL/DNS/redirect denial and filesystem escape negatives | OPEN | +| ART-001 through ART-005; DE-001; EVID-001 through EVID-002 | Artifact tamper/rollback/crash matrix, trusted-type construction denial and evidence-authority invariants | OPEN | + +## Gate evidence bundles + +| Gate | Minimum bundle before review | Status | +|---|---|---| +| G0 | Clean-room manifest, contributor attestations, repository/artifact scan report, adjudication log | OPEN | +| G1 | Dataset rights manifests, byte-level lineage report, random-initialization receipt, local/hosted digest comparison | OPEN | +| G2 | Focused tests, property/fuzz reports, deterministic replay hash, dependency-boundary tree, 24-hour replay report | OPEN | +| G3 | Frozen split manifest, baseline/model/retrieval rows, weighted quantile loss, interval calibration, reproducer and report digests | OPEN | +| G4 | Fourteen-day shadow report, empty-room/occupied-room confusion matrices, drift/abstention slices, authority audit | OPEN | +| G5 | Dedicated-host latency and process-RSS report, signed model card, SBOM/provenance, rollback drill, security/privacy/legal approvals | OPEN | + +## Evidence record template + +Copy one row per immutable evidence artifact. Never replace a failed result. + +| Evidence ID | Requirement/gate | Commit | Lock/config/data digest | Environment | Command | Result | Evidence label | Artifact digest | Reviewer/date | +|---|---|---|---|---|---|---|---|---|---| + +No evidence artifacts have been accepted. diff --git a/scripts/run-ruforecast-benchmarks.sh b/scripts/run-ruforecast-benchmarks.sh new file mode 100755 index 000000000..b5612bd1e --- /dev/null +++ b/scripts/run-ruforecast-benchmarks.sh @@ -0,0 +1,288 @@ +#!/usr/bin/env bash +# Reproducible CPU benchmark evidence runner for ADR-348. +# +# This script never downloads a model or dataset and never enables CUDA. The +# benchmark fixtures are generated by the checked-in Rust benches, so the +# source commit and Cargo.lock digest identify the fixture implementation. + +set -Eeuo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" +REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd -P)" + +TOOLCHAIN="${RUFORECAST_TOOLCHAIN:-1.92.0}" +THREADS="${RUFORECAST_THREADS:-1}" +WARM_UP_SECONDS="${RUFORECAST_WARM_UP_SECONDS:-3}" +MEASUREMENT_SECONDS="${RUFORECAST_MEASUREMENT_SECONDS:-10}" +SAMPLE_SIZE="${RUFORECAST_SAMPLE_SIZE:-30}" +CPUSET="${RUFORECAST_CPUSET:-}" +ALLOW_DIRTY="${RUFORECAST_ALLOW_DIRTY:-0}" +INCLUDE_LARGE="${RUFORECAST_BENCH_LARGE:-0}" + +die() { + printf 'ruforecast benchmark: %s\n' "$*" >&2 + exit 1 +} + +require_command() { + command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" +} + +require_uint() { + local name="$1" + local value="$2" + [[ "$value" =~ ^[0-9]+$ ]] || die "${name} must be an unsigned integer" +} + +[[ "$TOOLCHAIN" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] \ + || die "RUFORECAST_TOOLCHAIN must be an exact Rust release such as 1.92.0" +require_uint RUFORECAST_THREADS "$THREADS" +require_uint RUFORECAST_WARM_UP_SECONDS "$WARM_UP_SECONDS" +require_uint RUFORECAST_MEASUREMENT_SECONDS "$MEASUREMENT_SECONDS" +require_uint RUFORECAST_SAMPLE_SIZE "$SAMPLE_SIZE" +(( THREADS >= 1 )) || die "RUFORECAST_THREADS must be at least 1" +(( WARM_UP_SECONDS >= 1 )) || die "RUFORECAST_WARM_UP_SECONDS must be at least 1" +(( MEASUREMENT_SECONDS >= 1 )) || die "RUFORECAST_MEASUREMENT_SECONDS must be at least 1" +(( SAMPLE_SIZE >= 10 )) || die "RUFORECAST_SAMPLE_SIZE must be at least 10 for Criterion" +[[ "$ALLOW_DIRTY" == "0" || "$ALLOW_DIRTY" == "1" ]] \ + || die "RUFORECAST_ALLOW_DIRTY must be 0 or 1" +[[ "$INCLUDE_LARGE" == "0" || "$INCLUDE_LARGE" == "1" ]] \ + || die "RUFORECAST_BENCH_LARGE must be 0 or 1" + +if [[ -n "$CPUSET" ]]; then + [[ "$CPUSET" =~ ^[0-9,-]+$ ]] || die "RUFORECAST_CPUSET contains unsupported characters" + require_command taskset +fi + +for command_name in cargo rustc git jq sha256sum; do + require_command "$command_name" +done + +cd "$REPO_ROOT" + +for manifest in \ + v2/crates/ruview-forecast-core/Cargo.toml \ + v2/crates/ruview-forecast-model/Cargo.toml \ + v2/crates/ruview-forecast-train/Cargo.toml; do + [[ -f "$manifest" ]] || die "forecast crate is missing: $manifest" +done + +WORKTREE_CLEAN=true +if [[ -n "$(git status --porcelain)" ]]; then + WORKTREE_CLEAN=false + [[ "$ALLOW_DIRTY" == "1" ]] \ + || die "worktree changes detected; commit them or set RUFORECAST_ALLOW_DIRTY=1 for diagnostic evidence" +fi + +COMMIT="$(git rev-parse HEAD)" +SHORT_COMMIT="$(git rev-parse --short=12 HEAD)" +LOCK_SHA256="$(sha256sum v2/Cargo.lock | awk '{print $1}')" +RUN_UTC="$(date -u +'%Y%m%dT%H%M%SZ')" +SOURCE_DATE_EPOCH="$(git show -s --format=%ct HEAD)" + +DEFAULT_OUTPUT="${REPO_ROOT}/target/ruforecast-evidence/${RUN_UTC}-${SHORT_COMMIT}" +OUTPUT_DIR="${RUFORECAST_OUTPUT_DIR:-$DEFAULT_OUTPUT}" +if [[ "$OUTPUT_DIR" != /* ]]; then + OUTPUT_DIR="${REPO_ROOT}/${OUTPUT_DIR}" +fi +if [[ -d "$OUTPUT_DIR" && -n "$(find "$OUTPUT_DIR" -mindepth 1 -maxdepth 1 -print -quit)" ]]; then + die "output directory is not empty: $OUTPUT_DIR" +fi +mkdir -p "$OUTPUT_DIR" + +finalize_evidence() { + local exit_status=$? + local sums_status=0 + local sums_tmp="" + trap - EXIT + set +e + + if [[ -d "${OUTPUT_DIR:-}" ]]; then + if (( exit_status == 0 )); then + printf 'status=PASSED\nexit_code=0\n' >"${OUTPUT_DIR}/run-status.txt" + else + printf 'status=FAILED\nexit_code=%d\n' "$exit_status" \ + >"${OUTPUT_DIR}/run-status.txt" + fi + + sums_tmp="$(mktemp "${OUTPUT_DIR}.SHA256SUMS.XXXXXX")" || sums_status=$? + if (( sums_status == 0 )); then + ( + cd "$OUTPUT_DIR" || exit + find . -maxdepth 1 -type f ! -name SHA256SUMS -printf '%P\0' \ + | sort -z \ + | xargs -0 sha256sum + ) >"$sums_tmp" || sums_status=$? + fi + if (( sums_status == 0 )); then + mv "$sums_tmp" "${OUTPUT_DIR}/SHA256SUMS" || sums_status=$? + elif [[ -n "$sums_tmp" ]]; then + rm -f "$sums_tmp" + fi + fi + + if (( exit_status == 0 && sums_status != 0 )); then + exit_status=$sums_status + fi + exit "$exit_status" +} +trap finalize_evidence EXIT + +export RAYON_NUM_THREADS="$THREADS" +export OMP_NUM_THREADS="$THREADS" +export OPENBLAS_NUM_THREADS="$THREADS" +export MKL_NUM_THREADS="$THREADS" +export NUMEXPR_NUM_THREADS="$THREADS" +export CARGO_PROFILE_BENCH_DEBUG=0 +export CARGO_PROFILE_RELEASE_DEBUG=0 +export CARGO_INCREMENTAL=0 +export RUST_BACKTRACE=1 +export SOURCE_DATE_EPOCH +export TZ=UTC +export LC_ALL=C +export RUFORECAST_BENCH_LARGE="$INCLUDE_LARGE" + +CARGO=(cargo "+${TOOLCHAIN}") +if [[ -n "$CPUSET" ]]; then + CARGO=(taskset -c "$CPUSET" cargo "+${TOOLCHAIN}") +fi + +"${CARGO[@]}" --version >"${OUTPUT_DIR}/cargo-version.txt" +if [[ -n "$CPUSET" ]]; then + taskset -c "$CPUSET" rustc "+${TOOLCHAIN}" -vV >"${OUTPUT_DIR}/rustc-version.txt" +else + rustc "+${TOOLCHAIN}" -vV >"${OUTPUT_DIR}/rustc-version.txt" +fi +if command -v lscpu >/dev/null 2>&1; then + lscpu -J >"${OUTPUT_DIR}/cpu.json" +fi +uname -srmo >"${OUTPUT_DIR}/kernel.txt" +if [[ -r /etc/os-release ]]; then + cp /etc/os-release "${OUTPUT_DIR}/os-release.txt" +fi +if command -v free >/dev/null 2>&1; then + free -b >"${OUTPUT_DIR}/memory.txt" +fi + +printf \ + 'RUFORECAST_TOOLCHAIN=%q RUFORECAST_THREADS=%q RUFORECAST_CPUSET=%q RUFORECAST_WARM_UP_SECONDS=%q RUFORECAST_MEASUREMENT_SECONDS=%q RUFORECAST_SAMPLE_SIZE=%q RUFORECAST_ALLOW_DIRTY=%q RUFORECAST_BENCH_LARGE=%q scripts/run-ruforecast-benchmarks.sh\n' \ + "$TOOLCHAIN" \ + "$THREADS" \ + "$CPUSET" \ + "$WARM_UP_SECONDS" \ + "$MEASUREMENT_SECONDS" \ + "$SAMPLE_SIZE" \ + "$ALLOW_DIRTY" \ + "$INCLUDE_LARGE" \ + >"${OUTPUT_DIR}/reproducer.txt" + +EVIDENCE_LABEL="MEASURED" +EVIDENCE_SCOPE="LOCAL_CPU_SYNTHETIC_FIXTURE" +if [[ "$WORKTREE_CLEAN" != "true" ]]; then + EVIDENCE_LABEL="SYNTHETIC" + EVIDENCE_SCOPE="DIRTY_WORKTREE_DIAGNOSTIC_ONLY" +fi + +jq -n \ + --arg schema_version "1" \ + --arg evidence_label "$EVIDENCE_LABEL" \ + --arg evidence_scope "$EVIDENCE_SCOPE" \ + --arg commit "$COMMIT" \ + --arg lock_sha256 "$LOCK_SHA256" \ + --arg toolchain "$TOOLCHAIN" \ + --arg run_utc "$RUN_UTC" \ + --argjson source_date_epoch "$SOURCE_DATE_EPOCH" \ + --arg cpuset "$CPUSET" \ + --argjson include_large "$INCLUDE_LARGE" \ + --argjson worktree_clean "$WORKTREE_CLEAN" \ + --argjson threads "$THREADS" \ + --argjson warm_up_seconds "$WARM_UP_SECONDS" \ + --argjson measurement_seconds "$MEASUREMENT_SECONDS" \ + --argjson sample_size "$SAMPLE_SIZE" \ + '{ + schema_version: ($schema_version | tonumber), + evidence_label: $evidence_label, + evidence_scope: $evidence_scope, + claim_status: "UNREVIEWED", + input_class: "SYNTHETIC", + backend: "burn-ndarray-cpu", + cuda_enabled: false, + source: { + commit: $commit, + cargo_lock_sha256: $lock_sha256, + worktree_clean: $worktree_clean, + source_date_epoch: $source_date_epoch + }, + environment: { + rust_toolchain: $toolchain, + threads: $threads, + cpuset: (if $cpuset == "" then null else $cpuset end) + }, + criterion: { + warm_up_seconds: $warm_up_seconds, + measurement_seconds: $measurement_seconds, + sample_size: $sample_size, + include_large_profile: ($include_large == 1) + }, + run_utc: $run_utc, + caveat: "Criterion timing is host-specific. This report is not a RuView capability claim until reviewed against ADR-348." + }' >"${OUTPUT_DIR}/metadata.json" + +run_logged() { + local log_name="$1" + shift + "$@" 2>&1 | tee "${OUTPUT_DIR}/${log_name}.log" +} + +run_logged core-tests \ + "${CARGO[@]}" test --manifest-path v2/Cargo.toml --locked \ + -p ruview-forecast-core --no-default-features --lib --tests + +run_logged model-cpu-tests \ + "${CARGO[@]}" test --manifest-path v2/Cargo.toml --locked \ + -p ruview-forecast-model --no-default-features --features cpu --lib --tests + +run_logged train-cpu-tests \ + "${CARGO[@]}" test --manifest-path v2/Cargo.toml --locked \ + -p ruview-forecast-train --no-default-features --features cpu,cli --lib --bins + +run_logged local-jsonl-training-smoke \ + "${CARGO[@]}" test --manifest-path v2/Cargo.toml --locked \ + -p ruview-forecast-train --no-default-features --features cpu \ + --test local_jsonl_smoke -- \ + --exact local_hash_addressed_jsonl_executes_one_real_optimizer_step + +run_logged cli-smoke-idempotency \ + "${CARGO[@]}" test --manifest-path v2/Cargo.toml --locked \ + -p ruview-forecast-train --no-default-features --features cpu,cli \ + --test cli_smoke -- \ + --exact cli_smoke_trains_and_writes_the_complete_candidate_set + +run_logged forecast-inference-compile \ + "${CARGO[@]}" bench --manifest-path v2/Cargo.toml --locked \ + -p ruview-forecast-model --no-default-features --features cpu \ + --bench forecast_inference --no-run + +run_logged data-pipeline-compile \ + "${CARGO[@]}" bench --manifest-path v2/Cargo.toml --locked \ + -p ruview-forecast-train --no-default-features --features cpu \ + --bench data_pipeline --no-run + +run_logged forecast-inference \ + "${CARGO[@]}" bench --manifest-path v2/Cargo.toml --locked \ + -p ruview-forecast-model --no-default-features --features cpu \ + --bench forecast_inference -- \ + --warm-up-time "$WARM_UP_SECONDS" \ + --measurement-time "$MEASUREMENT_SECONDS" \ + --sample-size "$SAMPLE_SIZE" + +run_logged data-pipeline \ + "${CARGO[@]}" bench --manifest-path v2/Cargo.toml --locked \ + -p ruview-forecast-train --no-default-features --features cpu \ + --bench data_pipeline -- \ + --warm-up-time "$WARM_UP_SECONDS" \ + --measurement-time "$MEASUREMENT_SECONDS" \ + --sample-size "$SAMPLE_SIZE" + +printf 'RuForecast CPU outputs collected; finalizing hashes in %s\n' "$OUTPUT_DIR" +printf 'Review metadata.json and the two Criterion logs before accepting or publishing evidence.\n' diff --git a/v2/Cargo.lock b/v2/Cargo.lock index 2ff9ecb4d..72660aa40 100644 --- a/v2/Cargo.lock +++ b/v2/Cargo.lock @@ -1553,7 +1553,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2900,7 +2900,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3333,7 +3333,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5280,7 +5280,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.2", "system-configuration", "tokio", "tower-service", @@ -5549,7 +5549,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6719,7 +6719,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -7272,7 +7272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.45.0", + "windows-sys 0.61.2", ] [[package]] @@ -8051,7 +8051,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", - "itertools 0.10.5", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.117", @@ -8199,7 +8199,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.1", "rustls", - "socket2 0.5.10", + "socket2 0.6.2", "thiserror 2.0.18", "tokio", "tracing", @@ -8238,9 +8238,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -9104,7 +9104,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -9162,7 +9162,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -11116,7 +11116,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -13624,7 +13624,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/v2/Cargo.toml b/v2/Cargo.toml index 629bf0ee0..ec54e1a7b 100644 --- a/v2/Cargo.toml +++ b/v2/Cargo.toml @@ -123,6 +123,8 @@ members = [ # ADR-324 — clean-room Kooima off-axis (head-coupled perspective) projection. # Dependency-free native core; wasm-bindgen surface only on wasm32. "crates/ruview-offaxis", + # ADR-348 — RuForecast extracted to the ruvnet/RuForecast submodule + # (see crates/ruforecast in the exclude list below); path-deps only. ] # ADR-040: WASM edge crate targets wasm32-unknown-unknown (no_std), # excluded from workspace to avoid breaking `cargo test --workspace`. @@ -135,6 +137,12 @@ exclude = [ "crates/wifi-densepose-wasm-edge", "crates/homecore-plugin-example", "crates/worldgraph", # ruvnet/worldgraph submodule — its own workspace (geo/worldgraph/worldmodel) + "crates/ruforecast", # ruvnet/RuForecast submodule — its own workspace (ruforecast-core/model/train) + # LOCAL-DEV-ONLY: path-depends on an absolute, unpublished ruvnet/autogenous + # checkout that does not exist in CI. Build directly with + # `cargo build --manifest-path crates/ruforecast-autogenous-bridge/Cargo.toml` + # on a host with that checkout present. See its Cargo.toml/README. + "crates/ruforecast-autogenous-bridge", ] [workspace.package] @@ -220,6 +228,7 @@ walkdir = "2.4" # Hashing (for proof) sha2 = "0.10" +ed25519-dalek = "2.1" # CSV logging csv = "1.3" @@ -278,6 +287,9 @@ wifi-densepose-wasm = { version = "0.3.0", path = "crates/wifi-densepose-wasm" } wifi-densepose-mat = { version = "0.3.0", path = "crates/wifi-densepose-mat" } wifi-densepose-ruvector = { version = "0.3.0", path = "crates/wifi-densepose-ruvector" } wifi-densepose-worldmodel = { version = "0.3.0", path = "crates/worldgraph/wifi-densepose-worldmodel" } +ruforecast-core = { version = "0.1.0", path = "crates/ruforecast/crates/ruforecast-core" } +ruforecast-model = { version = "0.1.0", path = "crates/ruforecast/crates/ruforecast-model" } +ruforecast-train = { version = "0.1.0", path = "crates/ruforecast/crates/ruforecast-train" } [profile.release] lto = true diff --git a/v2/crates/ruforecast b/v2/crates/ruforecast new file mode 160000 index 000000000..448c035cc --- /dev/null +++ b/v2/crates/ruforecast @@ -0,0 +1 @@ +Subproject commit 448c035cc18f2fa7c05aab711c6ae0fd5cfc4e4b diff --git a/v2/crates/ruforecast-autogenous-bridge/Cargo.lock b/v2/crates/ruforecast-autogenous-bridge/Cargo.lock new file mode 100644 index 000000000..99b2d4ff8 --- /dev/null +++ b/v2/crates/ruforecast-autogenous-bridge/Cargo.lock @@ -0,0 +1,577 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "agl-types" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "antibody" +version = "0.1.0" +dependencies = [ + "agl-types", + "serde", + "serde_json", + "witness", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constitution" +version = "0.1.0" +dependencies = [ + "agl-types", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "envelope" +version = "0.1.0" +dependencies = [ + "agl-types", + "antibody", + "constitution", + "evaluator", + "serde", + "serde_json", + "sha2", + "witness", +] + +[[package]] +name = "evaluator" +version = "0.1.0" +dependencies = [ + "agl-types", + "antibody", + "serde", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "ruforecast-autogenous-bridge" +version = "0.1.0" +dependencies = [ + "agl-types", + "anyhow", + "clap", + "constitution", + "envelope", + "serde", + "serde_json", + "sha2", + "witness", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "witness" +version = "0.1.0" +dependencies = [ + "ed25519-dalek", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/v2/crates/ruforecast-autogenous-bridge/Cargo.toml b/v2/crates/ruforecast-autogenous-bridge/Cargo.toml new file mode 100644 index 000000000..04c997bfa --- /dev/null +++ b/v2/crates/ruforecast-autogenous-bridge/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "ruforecast-autogenous-bridge" +version = "0.1.0" +edition = "2021" +publish = false +description = "LOCAL-DEV-ONLY bridge: signs/verifies a Darwin-found RuForecast hyperparameter candidate through Autogenous's regression-candidate promotion path (ruvnet/autogenous, not yet published/vendored). Path-dependency on a sibling local checkout; not part of any release build." + +[[bin]] +name = "ruforecast-autogenous-bridge" +path = "src/main.rs" + +[dependencies] +anyhow = "1" +clap = { version = "4", features = ["derive"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +sha2 = "0.10" + +# LOCAL-DEV-ONLY path deps into a sibling, unpublished autogenous checkout. +# Not vendored/submoduled yet; this only builds on a host with this exact +# absolute checkout location (currently: ruvultra). See README.md. +envelope = { path = "/home/ruvultra/projects/autogenous/crates/envelope" } +agl-types = { path = "/home/ruvultra/projects/autogenous/crates/agl-types" } +constitution = { path = "/home/ruvultra/projects/autogenous/crates/constitution" } +witness = { path = "/home/ruvultra/projects/autogenous/crates/witness" } diff --git a/v2/crates/ruforecast-autogenous-bridge/examples/real_data_verify.rs b/v2/crates/ruforecast-autogenous-bridge/examples/real_data_verify.rs new file mode 100644 index 000000000..b94c15cf1 --- /dev/null +++ b/v2/crates/ruforecast-autogenous-bridge/examples/real_data_verify.rs @@ -0,0 +1,182 @@ +//! Independently verify the real-household-data WQL results (two real, +//! differently-split judges) through Autogenous's real regression-candidate +//! promotion path -- a genuine signed verdict, not a self-reported number. +//! LOCAL-DEV-ONLY, mirrors ruforecast-autogenous-bridge/src/main.rs's real +//! envelope::regression usage exactly, but consumes already-measured real +//! WQL numbers instead of running its own synthetic train_and_score. + +use agl_types::{Applicability, Authority, Genome, HardGates, Mutation, MutationScope}; +use anyhow::Result; +use constitution::{Constitution, RoleKeys}; +use envelope::regression::{ + artifact_hash, sign_regression_promotion, sign_regression_receipt, verify_regression_promotion, + MetricDirection, RegressionCandidateManifest, +}; +use witness::{content_hash, SigningAuthority}; + +struct RealJudge { + label: &'static str, + corpus_id: String, + sample_count: usize, + candidate_wql: f64, + parent_wql: f64, +} + +fn now_unix() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_secs() +} + +fn main() -> Result<()> { + // Real numbers from two genuinely independent temporal splits of the + // same 6390-sample real household vitals corpus (train_fraction 0.70 + // vs 0.50, both with a 90s embargo). "candidate" = the trained model's + // WQL; "parent" = the better of the two trivial baselines on that same + // judge's held-out set (min(last_value, seasonal_naive)), matching this + // session's established `primary` convention throughout. + let judges = vec![ + RealJudge { + label: "split-70-30", + corpus_id: "ruforecast-real-home-lab-split-70".into(), + sample_count: 27, + candidate_wql: 0.0513991104899006, + parent_wql: 0.05630054057848733_f64.min(0.10253572536393343), + }, + RealJudge { + label: "split-50-50", + corpus_id: "ruforecast-real-home-lab-split-50".into(), + sample_count: 46, + candidate_wql: 0.06704526404505064, + parent_wql: 0.0618358588096424_f64.min(0.054265071040759255), + }, + ]; + + let now = now_unix(); + let candidate_bytes = b"real-home-lab-default-optimizer-spec-v1".to_vec(); + let parent_bytes = b"trivial-baseline-min-last-value-seasonal-naive".to_vec(); + let parent_genome_hash = artifact_hash(&parent_bytes); + + let judge_keys: Vec<SigningAuthority> = (0..judges.len() as u64) + .map(|i| { + let mut seed = [0u8; 32]; + seed[0] = 2; + seed[1..9].copy_from_slice(&i.to_le_bytes()); + SigningAuthority::from_seed(&format!("ruforecast-real-judge-{i}"), seed) + }) + .collect(); + let controller_key = SigningAuthority::from_seed("ruforecast-real-controller", [8u8; 32]); + + let constitution = Constitution { + identity: "ruforecast-real-data-promotion".into(), + version: 1, + authority_ceiling: Authority::Governed, + prohibited_effects: vec!["pii_egress".into()], + hard_gates: HardGates::default(), + signers: vec!["ruforecast-real-bridge-operator".into()], + pinned_keys: RoleKeys { + judges: judge_keys.iter().map(SigningAuthority::public_hex).collect(), + controllers: vec![controller_key.public_hex()], + }, + effective_at: now.saturating_sub(1), + }; + + let parent_genome = Genome { + hash: parent_genome_hash.clone(), + identity: "ruforecast-real-data-parent".into(), + constitution: constitution.hash(), + capability_ceiling: Authority::Governed, + hard_invariants: vec![], + lineage: vec![], + }; + + let mutation = Mutation { + id: format!("ruforecast-real-data-candidate-{now}"), + parent_genome_hash: parent_genome.hash.clone(), + scope: MutationScope::ApplicationCode, + requested_authority: Authority::Governed, + applicability: Applicability::default(), + preserved_invariants: vec![], + rollback_target: Some(parent_genome.hash.clone()), + expires_at: Some(now + 3600), + signature: None, + }; + + let manifest = RegressionCandidateManifest::from_parts( + mutation, + &candidate_bytes, + "weighted_quantile_loss", + MetricDirection::LowerIsBetter, + vec![], + vec![], + vec![], + ); + let candidate_hash = manifest.candidate_hash(); + let parent_hash_for_receipts = content_hash(&parent_genome.hash); + + let receipts: Vec<_> = judges + .iter() + .zip(judge_keys.iter()) + .map(|(j, judge)| { + sign_regression_receipt( + judge, + &candidate_hash, + &parent_hash_for_receipts, + &j.corpus_id, + j.sample_count, + j.candidate_wql, + j.parent_wql, + "ruforecast-real-data-verify-v1", + now, + ) + }) + .collect(); + + let promotion_envelope = sign_regression_promotion( + &controller_key, + &constitution.hash(), + &candidate_hash, + &receipts, + &format!("ruforecast-real-data-{now}"), + now, + 3600, + ); + + let min_samples = judges.iter().map(|j| j.sample_count).min().unwrap_or(1); + let margin = 0.01_f64; + + let rejections = verify_regression_promotion( + &constitution, + &parent_genome, + &manifest, + &receipts, + &promotion_envelope, + &[], + judges.len(), + min_samples, + margin, + now, + ); + + let decision = if rejections.is_empty() { "PROMOTE" } else { "REJECT" }; + let report = serde_json::json!({ + "decision": decision, + "candidate_hash": candidate_hash, + "constitution_hash": constitution.hash(), + "judges": judges.iter().map(|j| serde_json::json!({ + "label": j.label, + "corpus_id": j.corpus_id, + "sample_count": j.sample_count, + "candidate_wql": j.candidate_wql, + "parent_wql": j.parent_wql, + "candidate_beats_parent_by": j.parent_wql - j.candidate_wql, + })).collect::<Vec<_>>(), + "min_judges": judges.len(), + "min_samples": min_samples, + "non_inferiority_margin": margin, + "rejections": rejections.iter().map(|r| format!("{r:?}")).collect::<Vec<_>>(), + }); + println!("{}", serde_json::to_string_pretty(&report)?); + Ok(()) +} diff --git a/v2/crates/ruforecast-autogenous-bridge/fixtures/bad.json b/v2/crates/ruforecast-autogenous-bridge/fixtures/bad.json new file mode 100644 index 000000000..7923aa504 --- /dev/null +++ b/v2/crates/ruforecast-autogenous-bridge/fixtures/bad.json @@ -0,0 +1 @@ +{"learning_rate": 0.0009293, "weight_decay": 0.01, "gradient_clip_norm": 0.1, "batch_size": 1, "epochs": 1} diff --git a/v2/crates/ruforecast-autogenous-bridge/fixtures/baseline.json b/v2/crates/ruforecast-autogenous-bridge/fixtures/baseline.json new file mode 100644 index 000000000..1b81bf030 --- /dev/null +++ b/v2/crates/ruforecast-autogenous-bridge/fixtures/baseline.json @@ -0,0 +1 @@ +{"learning_rate": 0.001, "weight_decay": 0.0001, "gradient_clip_norm": 1.0, "batch_size": 8, "epochs": 60} diff --git a/v2/crates/ruforecast-autogenous-bridge/fixtures/winner.json b/v2/crates/ruforecast-autogenous-bridge/fixtures/winner.json new file mode 100644 index 000000000..5a7c94d30 --- /dev/null +++ b/v2/crates/ruforecast-autogenous-bridge/fixtures/winner.json @@ -0,0 +1 @@ +{"learning_rate": 0.00002977567315122687, "weight_decay": 4.0937479376727585e-11, "gradient_clip_norm": 4.745827605560193, "batch_size": 26, "epochs": 356} diff --git a/v2/crates/ruforecast-autogenous-bridge/src/main.rs b/v2/crates/ruforecast-autogenous-bridge/src/main.rs new file mode 100644 index 000000000..76732b939 --- /dev/null +++ b/v2/crates/ruforecast-autogenous-bridge/src/main.rs @@ -0,0 +1,383 @@ +//! LOCAL-DEV-ONLY bridge from RuForecast's `evaluate` CLI into Autogenous's +//! regression-candidate promotion path (`envelope::regression`). +//! +//! Not a production path: it path-depends on a sibling, unpublished +//! `ruvnet/autogenous` checkout (see Cargo.toml), and the "judges" here are +//! real independent train+evaluate replays over genuinely distinct synthetic +//! corpora (different `--seed` per judge), not independent human/adversarial +//! review — see the module doc in `envelope::regression` for why that +//! distinction matters and why `min_judges`/`min_samples`/margin are passed +//! explicitly rather than assumed. +//! +//! Darwin's own promotion gate (`harness/ruview/flywheel/ruforecast/gate.mjs`) +//! still runs first and is unchanged; this is an additional, stronger, +//! cryptographically-checked verification step layered on top of a candidate +//! Darwin already found — defense in depth, not a gate replacement. + +use std::{ + path::{Path, PathBuf}, + process::Command, + time::{SystemTime, UNIX_EPOCH}, +}; + +use agl_types::{Applicability, Authority, Genome, HardGates, Mutation, MutationScope}; +use anyhow::{bail, Context, Result}; +use clap::Parser; +use constitution::{Constitution, RoleKeys}; +use envelope::regression::{ + artifact_hash, sign_regression_promotion, sign_regression_receipt, verify_regression_promotion, + MetricDirection, RegressionCandidateManifest, +}; +use serde::{Deserialize, Serialize}; +use witness::{content_hash, SigningAuthority}; + +#[derive(Parser, Debug)] +#[command( + name = "ruforecast-autogenous-bridge", + about = "LOCAL-DEV-ONLY: sign/verify a RuForecast hyperparameter candidate through Autogenous's regression-candidate promotion path" +)] +struct Cli { + /// Path to the built `ruforecast` binary (ruforecast-train's CLI). + #[arg(long)] + ruforecast_bin: PathBuf, + /// Candidate hyperparameter genome, JSON: {learning_rate, weight_decay, gradient_clip_norm, batch_size, epochs}. + #[arg(long)] + candidate_genome: PathBuf, + /// Parent (baseline) hyperparameter genome, same JSON shape. + #[arg(long)] + parent_genome: PathBuf, + /// Independent judges: full train+evaluate replays on genuinely distinct synthetic corpora. + #[arg(long, default_value_t = 2)] + judges: u64, + /// Synthetic training windows per corpus. + #[arg(long, default_value_t = 24)] + train_windows: u64, + /// Synthetic held-out windows per corpus. + #[arg(long, default_value_t = 8)] + test_windows: u64, + /// Scratch directory for per-judge corpora/artifacts (created fresh; must not already exist). + #[arg(long)] + work_dir: PathBuf, + /// Non-inferiority margin on weighted quantile loss (lower-is-better; a candidate must + /// beat its parent by at least this much on every judge's receipt to be admissible). + #[arg(long, default_value_t = 0.01)] + margin: f64, + /// Minimum held-out samples a receipt must report (floor stated honestly for this + /// fixture's scale, not the detector domain's unrelated 1000-sample bar). + #[arg(long, default_value_t = 4)] + min_samples: usize, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct HyperparamGenome { + learning_rate: f64, + weight_decay: f64, + gradient_clip_norm: f64, + batch_size: u16, + epochs: u16, +} + +struct JudgeMeasurement { + seed: u64, + corpus_id: String, + sample_count: usize, + candidate_metric: f64, + parent_metric: f64, +} + +fn now_unix() -> Result<u64> { + Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs()) +} + +fn run_ruforecast(bin: &Path, args: &[&str], label: &str) -> Result<String> { + let output = Command::new(bin) + .args(args) + .output() + .with_context(|| format!("spawning ruforecast for {label}"))?; + if !output.status.success() { + bail!( + "ruforecast {label} failed (exit {:?}):\nstdout: {}\nstderr: {}", + output.status.code(), + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } + Ok(String::from_utf8_lossy(&output.stdout).into_owned()) +} + +/// Prepare a synthetic corpus + train + evaluate one hyperparameter genome +/// against it, returning (weighted_quantile_loss, n_test_windows). +fn train_and_score( + bin: &Path, + genome: &HyperparamGenome, + corpus_dir: &Path, + seed: u64, + train_windows: u64, + test_windows: u64, +) -> Result<(f64, usize)> { + std::fs::create_dir_all(corpus_dir.parent().unwrap_or(corpus_dir))?; + run_ruforecast( + bin, + &[ + "prepare-synthetic-dataset", + "--directory", + corpus_dir.to_str().context("non-utf8 path")?, + "--seed", + &seed.to_string(), + "--train-windows", + &train_windows.to_string(), + "--test-windows", + &test_windows.to_string(), + "--learning-rate", + &genome.learning_rate.to_string(), + "--weight-decay", + &genome.weight_decay.to_string(), + "--gradient-clip-norm", + &genome.gradient_clip_norm.to_string(), + "--batch-size", + &genome.batch_size.to_string(), + "--epochs", + &genome.epochs.to_string(), + ], + "prepare-synthetic-dataset", + )?; + + let artifacts_dir = corpus_dir.join("artifacts"); + run_ruforecast( + bin, + &[ + "train-local", + "--request", + corpus_dir.join("train-local.toml").to_str().context("non-utf8 path")?, + "--dataset-root", + corpus_dir.to_str().context("non-utf8 path")?, + "--output", + artifacts_dir.to_str().context("non-utf8 path")?, + ], + "train-local", + )?; + + let candidate_mpk = artifacts_dir.join("synthetic-dataset").join("model.mpk"); + let eval_stdout = run_ruforecast( + bin, + &[ + "evaluate", + "--candidate", + candidate_mpk.to_str().context("non-utf8 path")?, + "--test-jsonl", + corpus_dir.join("test.jsonl").to_str().context("non-utf8 path")?, + "--seasonal-period", + "12", + ], + "evaluate", + )?; + let report: serde_json::Value = + serde_json::from_str(&eval_stdout).context("parsing evaluate JSON output")?; + let wql = report["model"]["weighted_quantile_loss"] + .as_f64() + .context("evaluate output missing model.weighted_quantile_loss")?; + let n = report["n_test_windows"] + .as_u64() + .context("evaluate output missing n_test_windows")? as usize; + Ok((wql, n)) +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + if cli.work_dir.exists() { + bail!( + "--work-dir {} already exists; pass a fresh directory", + cli.work_dir.display() + ); + } + if cli.judges == 0 { + bail!("--judges must be at least 1 (state min_judges: 1 honestly rather than 0)"); + } + std::fs::create_dir_all(&cli.work_dir)?; + + let candidate: HyperparamGenome = serde_json::from_str( + &std::fs::read_to_string(&cli.candidate_genome).context("reading --candidate-genome")?, + ) + .context("parsing --candidate-genome JSON")?; + let parent: HyperparamGenome = serde_json::from_str( + &std::fs::read_to_string(&cli.parent_genome).context("reading --parent-genome")?, + ) + .context("parsing --parent-genome JSON")?; + + let mut measurements = Vec::with_capacity(cli.judges as usize); + for judge_index in 0..cli.judges { + // Genuine corpus independence: each judge trains+evaluates BOTH + // genomes against its own freshly-generated synthetic corpus (same + // seed for candidate/parent within a judge, so the comparison is + // apples-to-apples on that judge's held-out set; different seed + // ACROSS judges, so disagreement between judges is a real signal + // about generalization, not a re-run of identical arithmetic). + let seed = 1000 + judge_index * 97; + let judge_dir = cli.work_dir.join(format!("judge-{judge_index}")); + let (candidate_wql, candidate_n) = train_and_score( + &cli.ruforecast_bin, + &candidate, + &judge_dir.join("candidate"), + seed, + cli.train_windows, + cli.test_windows, + ) + .with_context(|| format!("judge {judge_index}: scoring candidate genome"))?; + let (parent_wql, parent_n) = train_and_score( + &cli.ruforecast_bin, + &parent, + &judge_dir.join("parent"), + seed, + cli.train_windows, + cli.test_windows, + ) + .with_context(|| format!("judge {judge_index}: scoring parent genome"))?; + if candidate_n != parent_n { + bail!( + "judge {judge_index}: candidate/parent test-window counts disagree ({candidate_n} vs {parent_n}) — corpus was not actually shared" + ); + } + measurements.push(JudgeMeasurement { + seed, + corpus_id: format!("ruforecast-synthetic-seed-{seed}"), + sample_count: candidate_n, + candidate_metric: candidate_wql, + parent_metric: parent_wql, + }); + } + + let now = now_unix()?; + let candidate_bytes = serde_json::to_vec(&candidate)?; + let parent_bytes = serde_json::to_vec(&parent)?; + let parent_genome_hash = artifact_hash(&parent_bytes); + + let judge_keys: Vec<SigningAuthority> = (0..cli.judges) + .map(|i| { + let mut seed = [0u8; 32]; + seed[0] = 1; + seed[1..9].copy_from_slice(&i.to_le_bytes()); + SigningAuthority::from_seed(&format!("ruforecast-judge-{i}"), seed) + }) + .collect(); + let controller_key = SigningAuthority::from_seed("ruforecast-controller", [9u8; 32]); + + let constitution = Constitution { + identity: "ruforecast-hpo-promotion".into(), + version: 1, + authority_ceiling: Authority::Governed, + prohibited_effects: vec!["pii_egress".into()], + hard_gates: HardGates::default(), + signers: vec!["ruforecast-bridge-operator".into()], + pinned_keys: RoleKeys { + judges: judge_keys.iter().map(SigningAuthority::public_hex).collect(), + controllers: vec![controller_key.public_hex()], + }, + effective_at: now.saturating_sub(1), + }; + + let parent_genome = Genome { + hash: parent_genome_hash.clone(), + identity: "ruforecast-hyperparameter-genome".into(), + constitution: constitution.hash(), + capability_ceiling: Authority::Governed, + hard_invariants: vec![], + lineage: vec![], + }; + + let mutation = Mutation { + id: format!("ruforecast-hpo-candidate-{now}"), + parent_genome_hash: parent_genome.hash.clone(), + scope: MutationScope::ApplicationCode, + requested_authority: Authority::Governed, + applicability: Applicability::default(), + preserved_invariants: vec![], + rollback_target: Some(parent_genome.hash.clone()), + expires_at: Some(now + 3600), + signature: None, + }; + + let manifest = RegressionCandidateManifest::from_parts( + mutation, + &candidate_bytes, + "weighted_quantile_loss", + MetricDirection::LowerIsBetter, + vec![], + vec![], + vec![], + ); + let candidate_hash = manifest.candidate_hash(); + let parent_hash_for_receipts = content_hash(&parent_genome.hash); + + let receipts: Vec<_> = measurements + .iter() + .zip(judge_keys.iter()) + .map(|(m, judge)| { + sign_regression_receipt( + judge, + &candidate_hash, + &parent_hash_for_receipts, + &m.corpus_id, + m.sample_count, + m.candidate_metric, + m.parent_metric, + "ruforecast-autogenous-bridge-v1", + now, + ) + }) + .collect(); + + let promotion_envelope = sign_regression_promotion( + &controller_key, + &constitution.hash(), + &candidate_hash, + &receipts, + &format!("ruforecast-hpo-{now}"), + now, + 3600, + ); + + let min_samples = cli.min_samples.min( + measurements + .iter() + .map(|m| m.sample_count) + .min() + .unwrap_or(cli.min_samples), + ); + + let rejections = verify_regression_promotion( + &constitution, + &parent_genome, + &manifest, + &receipts, + &promotion_envelope, + &[], + cli.judges as usize, + min_samples, + cli.margin, + now, + ); + + let decision = if rejections.is_empty() { "PROMOTE" } else { "REJECT" }; + let report = serde_json::json!({ + "decision": decision, + "candidate_hash": candidate_hash, + "constitution_hash": constitution.hash(), + "judges": measurements.iter().map(|m| serde_json::json!({ + "seed": m.seed, + "corpus_id": m.corpus_id, + "sample_count": m.sample_count, + "candidate_wql": m.candidate_metric, + "parent_wql": m.parent_metric, + "candidate_beats_parent_by": m.parent_metric - m.candidate_metric, + })).collect::<Vec<_>>(), + "min_judges": cli.judges, + "min_samples": min_samples, + "non_inferiority_margin": cli.margin, + "rejections": rejections.iter().map(|r| format!("{r:?}")).collect::<Vec<_>>(), + }); + println!("{}", serde_json::to_string_pretty(&report)?); + if decision == "REJECT" { + std::process::exit(1); + } + Ok(()) +}