diff --git a/.github/workflows/firmware-ci.yml b/.github/workflows/firmware-ci.yml
index 04f3ec53f..522ac845a 100644
--- a/.github/workflows/firmware-ci.yml
+++ b/.github/workflows/firmware-ci.yml
@@ -162,10 +162,14 @@ jobs:
mkdir -p release-staging
cp build/esp32-csi-node.bin release-staging/${{ matrix.artifact_app }}
cp build/partition_table/partition-table.bin release-staging/${{ matrix.artifact_pt }}
- if [ "${{ matrix.variant }}" = "8mb" ]; then
- cp build/bootloader/bootloader.bin release-staging/bootloader.bin
- cp build/ota_data_initial.bin release-staging/ota_data_initial.bin
- fi
+ cp build/bootloader/bootloader.bin release-staging/bootloader.bin
+ cp build/ota_data_initial.bin release-staging/ota_data_initial.bin
+ cp version.txt release-staging/version.txt
+ (cd release-staging && sha256sum \
+ "${{ matrix.artifact_app }}" \
+ "${{ matrix.artifact_pt }}" \
+ bootloader.bin ota_data_initial.bin version.txt \
+ > SHA256SUMS.txt)
ls -la release-staging/
- name: Check QEMU ESP32-S3 support status
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/README.md b/README.md
index 5a16412d3..38fd36672 100644
--- a/README.md
+++ b/README.md
@@ -2,20 +2,11 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
## **See through walls with WiFi** ##
**Turn ordinary WiFi into a spatial intelligence / sensing system.** Detect people, measure breathing and heart rate, track movement, and monitor rooms — through walls, in the dark, with no cameras or wearables. Just physics.
diff --git a/assets/ruview-hero-h3-v3.gif b/assets/ruview-hero-h3-v3.gif
new file mode 100644
index 000000000..9761dce03
Binary files /dev/null and b/assets/ruview-hero-h3-v3.gif differ
diff --git a/docs/adr/ADR-262-rufield-ruview-integration.md b/docs/adr/ADR-262-rufield-ruview-integration.md
index 64702aaf2..053a7d9c9 100644
--- a/docs/adr/ADR-262-rufield-ruview-integration.md
+++ b/docs/adr/ADR-262-rufield-ruview-integration.md
@@ -139,6 +139,32 @@ Implement the §3.3 mapping: `effective_class → PrivacyClass`, `cog-ha-matter`
Add an opt-in `/ws/field` endpoint (or a `field_events` array on `SensingUpdate` behind a flag) carrying the signed `FieldEvent` + a privacy badge. Add an ingest route to `rufield-viewer` (it has none today — `server.rs:63-72`) so it can replay RuView's live feed instead of only `SyntheticSim`. **Gate:** a WS integration test asserting a connected client receives a privacy-badged, signature-verifiable `FieldEvent`; a viewer test asserting the new ingest route renders a live event. The `cognitum` appliance can speak RuField by consuming this endpoint (it already runs `ruview-vitals-worker`); deferred to its own ADR.
**P4 — fusion composition + multi-modality (ARCHITECTURE, optional).**
+
+> **Update — second modality landed as a library.** Open question 5 below asked
+> whether the second modality should be `rvcsi`. It is **ultrasonic**, because
+> the cost collapsed: `rufield-adapters` now ships `UltrasonicReplayAdapter`,
+> the first adapter for `Modality::Ultrasonic` (registry code 7, empty since
+> v0.1), which parses, validates and signs [BatVu](https://github.com/ruvnet/batvu)
+> range profiles upstream. RuView only has to decide what it will put on a wire.
+>
+> `wifi-densepose-rufield::ultrasonic` is that decision, and it is expressed
+> structurally: the adapter is configured for its 32-bin coarse output (`P1`,
+> egress-safe) rather than its full per-bin frame (`P0`, edge-local), because a
+> consumer cannot un-coarsen a coarse profile whereas a runtime check can be
+> reordered. The `network_egress_allowed` gate still runs and is asserted to
+> drop nothing.
+>
+> Gates: `tests/ultrasonic_gates.rs`, 12 tests — round-trip, signature-verify,
+> fusion ingest, P1 on **both** tensor and observation, structural unreachability
+> of P4/P5, trust-tier refusal in both directions, determinism, whole-file
+> rejection of a malformed recording. Plus one asserting the honest negative
+> result: **an ultrasonic scan produces no fused inferences at all**, because the
+> adapter declines to populate `presence` (one transducer pair cannot tell a
+> person from a coat on a chair) and the engine's feature vocabulary is entirely
+> statements about a body. RuField v0.1 has no predicate for static geometry.
+>
+> Not wired into the running server. P1 shipped as a library before P3 wired it
+> in; this follows the same staging.
Wire a second modality (cheapest: an `rvcsi`-sourced event, or recorded mmWave) into `RuFieldFusion` alongside the WiFi event, proving cross-modality fusion above ruvsense. **Gate:** a fusion test with two modalities producing ≥1 cross-modal inference, with provenance coverage 100%.
---
diff --git a/docs/adr/ADR-346-fail-closed-edge-occupancy-evidence.md b/docs/adr/ADR-346-fail-closed-edge-occupancy-evidence.md
new file mode 100644
index 000000000..0148a2b82
--- /dev/null
+++ b/docs/adr/ADR-346-fail-closed-edge-occupancy-evidence.md
@@ -0,0 +1,52 @@
+# ADR 346: Fail closed ESP32 occupancy evidence
+
+## Status
+
+Accepted and implemented. Physical qualification is required after each firmware build.
+
+## Date
+
+2026 08 31
+
+## Context
+
+The ESP32 Tier 2 pipeline produces two different signals. Presence is a debounced room level decision. Person count is a bounded subcarrier diversity heuristic. A live four node installation emitted packets with `presence=false` and `n_persons=3` or `4`. The server eventually gated the aggregate room count, but raw WebSocket consumers and diagnostics could still treat the contradictory count as occupancy evidence.
+
+That contradiction is more dangerous than a missed optional count. It can contaminate empty room calibration, train a room model on false labels, and encourage a product claim that the firmware cannot support. The count is not identity, pose, or a validated multi person estimator.
+
+## Decision
+
+1. Firmware person slots are subordinate to the debounced presence gate.
+2. When presence is false, the firmware clears slot activity, slot history, candidate count, persistence streak, and stable count.
+3. The serialized person count is always zero when presence is false and is clamped to `EDGE_MAX_PERSONS` when presence is true.
+4. The sensing server repeats the invariant for older firmware. A contradictory or out of range count becomes zero and carries `person_count_valid=false`.
+5. Fused CSI plus mmWave packets use either CSI presence or mmWave presence as the supporting presence condition.
+6. The node inventory and WebSocket diagnostics expose person count validity. Consumers must not infer a person from an invalid count.
+7. No count accuracy claim is created by this change. The firmware output remains a heuristic until a leakage free, held out physical dataset demonstrates otherwise.
+8. OTA admission uses the selected update partition size rather than a stale fixed 900 KB ceiling. The status endpoint reports that same hardware bound, while image validation and authenticated OTA remain mandatory.
+
+## Security and privacy
+
+The change retains no raw CSI or personal data. It reduces authority by preventing a secondary heuristic from asserting occupancy after the primary gate has closed. The host validates packet length, magic, range, and logical consistency before using count evidence.
+
+## Consequences
+
+Older firmware remains wire compatible. Invalid count evidence becomes visibly unavailable instead of silently affecting calibration. A true multi person event can still be undercounted when the presence gate is false, which is the intended fail closed behavior. Current C6 images larger than 900 KB can use the installed 1,900,544 byte OTA slots after one serial upgrade, without weakening the OTA authentication gate.
+
+The largest uncertainty is whether the current presence gate itself generalizes across the installed rooms. The fix path is a room bound empty baseline plus the fixed room selective held out protocol, not a global threshold reduction.
+
+## Evidence and acceptance
+
+MEASURED before implementation on 2026 08 31: four live nodes streamed for 86 seconds with zero transport errors, while edge packets repeatedly contradicted `presence=false` with counts of three or four.
+
+Software acceptance requires:
+
+1. Host firmware tests prove absent plus four active slots serializes zero.
+2. Rust parser tests prove contradictory and out of range counts fail closed.
+3. The node API exposes count validity without breaking older firmware.
+
+Physical acceptance requires the updated firmware on a confirmed board, a captured boot log, five minutes of live packets, zero logical count contradictions, and no increase in transport errors. Accuracy remains unmeasured until labelled held out sequences are recorded.
+
+Physical occupancy qualification completed for ESP32 C6 node 4 on 2026 08 31. The five minute run observed 242 edge packets, including 61 absent packets, with zero logical count contradictions and zero parse errors. See `docs/validation/2026-08-31-esp32-c6-occupancy-integrity.md`.
+
+ESP32 C6 node 7 was subsequently identified, upgraded to firmware 0.8.8, and transport qualified for five minutes with zero fused presence count contradictions and zero steady state transport errors. Its controlled empty room sequence remains required before occupancy qualification. See `docs/validation/2026-08-31-esp32-c6-node7-rate-aware-sensing.md`. Other nodes remain unqualified until separately identified and upgraded.
diff --git a/docs/adr/ADR-347-rate-aware-esp32-temporal-sensing.md b/docs/adr/ADR-347-rate-aware-esp32-temporal-sensing.md
new file mode 100644
index 000000000..289011174
--- /dev/null
+++ b/docs/adr/ADR-347-rate-aware-esp32-temporal-sensing.md
@@ -0,0 +1,85 @@
+# ADR 347: Rate aware ESP32 temporal sensing
+
+## Status
+
+Accepted. Implemented in firmware 0.8.8. The timing and transport path is
+physically qualified on ESP32 C6. The independent raw transport path is also
+physically qualified on an ESP32 S3 running Tier 0. Held out inference accuracy
+remains required.
+
+## Context
+
+The ESP32 firmware creates CSI opportunities by sending one byte ICMP probes to
+the connected access point. The traffic source is configured for 50 Hz, but the
+delivered CSI cadence varies with channel contention and callback safety gates.
+A physical ESP32 C6 produced 28 to 37 callbacks per second during the baseline
+capture. Firmware 0.8.5 then exposed that the old per-interval estimator saw
+only 12 to 16 Hz because WiFi replies arrived in short bursts separated by
+longer gaps. The filters still consumed those burst frames, so excluding them
+from the clock estimate was incorrect.
+
+The edge DSP estimates its sample rate from timestamps so that breathing,
+heartbeat, motion, and future Doppler features stay in physical Hertz. That
+estimator was capped at 30 Hz. Once the actual cadence exceeded the cap, every
+temporal feature was scaled against the wrong clock.
+
+Physical firmware 0.8.5 validation corrected that initial diagnosis. Although
+the callback path received 26 to 40 frames per second, Tier 2 on the unicore C6
+processed an irregular subset that converged toward the 8 Hz estimator floor.
+The right design is not to force the edge DSP to match raw capture. The paths
+need independent, explicit cadence contracts.
+
+The device free gesture preprint at
+`https://www.preprints.org/manuscript/202602.0018` reinforces the importance of
+timestamp correct Doppler features, but its 100 Hz controlled link is not a
+safe firmware default for RuView. Existing S3 and C6 evidence records WiFi ISR
+and packet buffer failures under sustained callback pressure above 50 Hz.
+
+## Decision
+
+1. Make the connected STA probe rate a build time setting from 10 through 50
+ Hz, with a default and hard ceiling of 50 Hz.
+
+2. Track the delivered DSP cadence by counting every processed frame interval
+ over one second timestamp windows, then smooth successive windows in an 8
+ through 60 Hz estimator range. The 60 Hz estimator ceiling accommodates
+ timestamp jitter; it does not authorize more than 50 Hz callback processing.
+
+3. Reject incomplete windows below one second and stalled windows above three
+ seconds. Do not discard valid burst frames from the estimated clock.
+
+4. Surface the DSP rate in the one second controller diagnostic so hardware
+ validation can compare callback yield with the clock used by temporal
+ filters.
+
+5. Keep raw CSI on the wire at the independent network cadence. Rate-limit the
+ C6 on-device Tier 1 and Tier 2 DSP input to a uniform 8 Hz. Physical 0.8.7
+ evidence showed that a requested 10 Hz input still converged to 8.0 through
+ 8.4 Hz under Tier 2 load, while raw delivery remained 30 through 40 pps.
+ Eight hertz retains a 4 Hz Nyquist limit for the 0.1 through 2.0 Hz vital
+ bands without creating a backlog. The S3 default remains 20 Hz.
+
+6. STFT, spectrogram gating, and learned temporal
+ classification remain host or iPhone responsibilities where memory,
+ rollback, and held out evaluation are stronger.
+
+## Consequences
+
+Heartbeat, respiration, and motion features receive a stable timestamped clock
+instead of an accidental subset determined by C6 backlog. Operators can lower
+the probe or DSP load for constrained networks without editing source. The host
+still receives the higher-rate raw stream for richer Doppler processing.
+
+This does not prove vital sign accuracy or gesture recognition. Higher temporal
+fidelity only improves the representation available to a separately validated
+model. The 50 Hz ceiling also means the paper's 100 Hz results are not directly
+transferable.
+
+## Acceptance test
+
+On a physical C6, run at least five minutes after flashing. Pass when the boot
+log reports the configured probe and DSP rates, the controller converges within
+one hertz of the configured DSP cadence, raw callback yield remains at least 20
+pps, no steady-state ENOMEM, watchdog, panic, or reboot occurs, and the fail
+closed occupancy invariant remains zero contradictions for at least 30 absent
+packets.
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 77317d11e..293c514a5 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
@@ -106,6 +109,8 @@ Statuses: **Proposed** (under discussion), **Accepted** (approved and/or impleme
| [ADR-036](ADR-036-rvf-training-pipeline-ui.md) | Training Pipeline UI Integration | Proposed |
| [ADR-043](ADR-043-sensing-server-ui-api-completion.md) | Sensing Server UI API Completion (14 endpoints) | Accepted |
| [ADR-344](ADR-344-adaptive-local-installation-discovery.md) | Adaptive Local Installation Discovery | Accepted (local software path) |
+| [ADR-346](ADR-346-fail-closed-edge-occupancy-evidence.md) | Fail closed ESP32 occupancy evidence | Accepted (C6 occupancy integrity qualified) |
+| [ADR-347](ADR-347-rate-aware-esp32-temporal-sensing.md) | Rate aware ESP32 temporal sensing | Accepted (C6 timing and transport qualified) |
| [ADR-115](ADR-115-home-assistant-integration.md) | Home Assistant integration via MQTT auto-discovery + Matter bridge (HA-DISCO + HA-FABRIC + HA-MIND) | Accepted (MQTT track) / Proposed (Matter SDK P8b) |
| [ADR-169](ADR-169-adam-mode-light-theme.md) | adam-mode — light theme toggle for the three.js realtime demo | Proposed |
| [ADR-170](ADR-170-yoga-mode-pose-system.md) | yoga-mode — yoga pose detection, classification, and scoring for the three.js realtime demo | Proposed |
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 | `` | `