diff --git a/.github/workflows/ruforecast-ci.yml b/.github/workflows/ruforecast-ci.yml new file mode 100644 index 000000000..2f17c8ab9 --- /dev/null +++ b/.github/workflows/ruforecast-ci.yml @@ -0,0 +1,627 @@ +name: RuForecast Rust CI + +on: + push: + branches: [main, develop, 'feat/*', 'feature/*'] + paths: + - 'v2/crates/ruforecast' + - 'v2/Cargo.toml' + - 'v2/Cargo.lock' + - 'v2/rust-toolchain.toml' + - 'scripts/csi-data-policy-check.sh' + - 'scripts/run-ruforecast-benchmarks.sh' + - 'docs/adr/ADR-348-*.md' + - 'docs/adr/ADR-349-*.md' + - 'docs/adr/ADR-350-*.md' + - 'docs/adr/README.md' + - 'docs/benchmarks/ruforecast.md' + - 'docs/huggingface/RUVIEW_FORECAST_MODEL_CARD_TEMPLATE.md' + - 'docs/security/ruview-forecast-*' + - 'docs/validation/ruforecast-requirements-evidence.md' + - '.github/workflows/ruforecast-ci.yml' + pull_request: + branches: [main, develop] + paths: + - 'v2/crates/ruforecast' + - 'v2/Cargo.toml' + - 'v2/Cargo.lock' + - 'v2/rust-toolchain.toml' + - 'scripts/csi-data-policy-check.sh' + - 'scripts/run-ruforecast-benchmarks.sh' + - 'docs/adr/ADR-348-*.md' + - 'docs/adr/ADR-349-*.md' + - 'docs/adr/ADR-350-*.md' + - 'docs/adr/README.md' + - 'docs/benchmarks/ruforecast.md' + - 'docs/huggingface/RUVIEW_FORECAST_MODEL_CARD_TEMPLATE.md' + - 'docs/security/ruview-forecast-*' + - 'docs/validation/ruforecast-requirements-evidence.md' + - '.github/workflows/ruforecast-ci.yml' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ruforecast-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + CARGO_PROFILE_DEV_DEBUG: '0' + CARGO_PROFILE_TEST_DEBUG: '0' + CARGO_PROFILE_BENCH_DEBUG: '0' + CARGO_PROFILE_RELEASE_DEBUG: '0' + +jobs: + contract-rust-189: + name: Forecast contract and feature-off boundary (Rust 1.89) + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: Checkout recursively + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + submodules: recursive + + - name: Install Rust 1.89 with lint components + uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 + with: + toolchain: 1.89.0 + components: rustfmt,clippy + + - name: Cache Cargo artifacts + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + workspaces: v2/crates/ruforecast + key: ruforecast-rust-189 + + - name: Validate benchmark runner and evidence contract + shell: bash + run: | + set -euo pipefail + bash -n scripts/run-ruforecast-benchmarks.sh + test -x scripts/run-ruforecast-benchmarks.sh + test -f docs/benchmarks/ruforecast.md + test -f docs/huggingface/RUVIEW_FORECAST_MODEL_CARD_TEMPLATE.md + test -f docs/security/ruview-forecast-clean-room.md + test -f docs/security/ruview-forecast-threat-model.md + test -f docs/validation/ruforecast-requirements-evidence.md + + for adr_number in 348 349 350; do + mapfile -t adr_files < <( + find docs/adr -maxdepth 1 -type f -name "ADR-${adr_number}-*.md" -print + ) + if [[ "${#adr_files[@]}" -ne 1 ]]; then + echo "Expected exactly one ADR-${adr_number} file, found ${#adr_files[@]}" >&2 + exit 1 + fi + grep -q "^# ADR-${adr_number}:" "${adr_files[0]}" + grep -Eq '^[-*] \*\*Status\*\*: (Proposed|Accepted|Superseded|Deprecated)( |$|—)|^## Status' "${adr_files[0]}" + done + + grep -q '^## Requirements' docs/adr/ADR-348-*.md + grep -q '^## Acceptance gates' docs/adr/ADR-348-*.md + grep -q '^## Requirements and acceptance' docs/adr/ADR-349-*.md + grep -q '^## Requirements and acceptance' docs/adr/ADR-350-*.md + for adr_number in 348 349 350; do + grep -q "| \[ADR-${adr_number}\](ADR-${adr_number}-" docs/adr/README.md \ + || { echo "ADR-${adr_number} is missing from the ADR index" >&2; exit 1; } + done + + for number in $(seq 1 12); do + requirement="$(printf 'RF-%03d' "$number")" + grep -q "| ${requirement} |" docs/validation/ruforecast-requirements-evidence.md \ + || { echo "Missing evidence row for ${requirement}" >&2; exit 1; } + done + for number in $(seq 1 8); do + requirement="$(printf 'FT-%03d' "$number")" + grep -q "| ${requirement} |" docs/validation/ruforecast-requirements-evidence.md \ + || { echo "Missing evidence row for ${requirement}" >&2; exit 1; } + done + for number in $(seq 1 9); do + requirement="$(printf 'PM-%03d' "$number")" + grep -q "| ${requirement} |" docs/validation/ruforecast-requirements-evidence.md \ + || { echo "Missing evidence row for ${requirement}" >&2; exit 1; } + done + + python3 - <<'PY' + from pathlib import Path + import tomllib + + root = Path("v2/crates/ruforecast/crates") + core = tomllib.loads((root / "ruforecast-core/Cargo.toml").read_text()) + model = tomllib.loads((root / "ruforecast-model/Cargo.toml").read_text()) + train = tomllib.loads((root / "ruforecast-train/Cargo.toml").read_text()) + + assert core.get("features", {}).get("default", []) == [] + assert model["features"]["default"] == [], "model defaults must stay backend-free" + assert train["features"]["default"] == [], "train defaults must stay backend-free" + assert {"model", "cpu", "cuda", "wgpu", "ruvector"} <= model["features"].keys() + assert {"cpu", "cuda", "training", "cli", "server", "fal-client"} <= train["features"].keys() + assert {"model", "dep:burn-ndarray"} <= set(model["features"]["cpu"]) + assert {"model", "dep:burn-cuda"} <= set(model["features"]["cuda"]) + assert {"model", "dep:burn-wgpu"} <= set(model["features"]["wgpu"]) + assert "dep:ruvector-core" in model["features"]["ruvector"] + assert {"ruforecast-model/cpu", "training"} <= set(train["features"]["cpu"]) + assert {"ruforecast-model/cuda", "training"} <= set(train["features"]["cuda"]) + + bins = {entry["name"]: entry for entry in train.get("bin", [])} + assert bins["ruforecast"]["required-features"] == ["cli"] + + model_benches = {entry["name"]: entry for entry in model.get("bench", [])} + train_benches = {entry["name"]: entry for entry in train.get("bench", [])} + assert model_benches["forecast_inference"]["harness"] is False + assert model_benches["forecast_inference"]["required-features"] == ["cpu"] + assert train_benches["data_pipeline"]["harness"] is False + assert train_benches["data_pipeline"]["required-features"] == ["cpu"] + PY + + - name: Check formatting for forecast packages only + working-directory: v2/crates/ruforecast + run: >- + cargo +1.89.0 fmt + -p ruforecast-core + -p ruforecast-model + -p ruforecast-train + -- --check + + - name: Prove feature-off graph excludes Burn and CubeCL + working-directory: v2/crates/ruforecast + shell: bash + run: | + set -euo pipefail + : > forecast-feature-off-tree.txt + for package in ruforecast-core ruforecast-model ruforecast-train; do + cargo +1.89.0 tree --locked -e normal,build -p "$package" --no-default-features \ + >> forecast-feature-off-tree.txt + done + if grep -Eiq '(^|[[:space:]])(burn|cubecl)(-|[[:space:]])' forecast-feature-off-tree.txt; then + echo 'Burn/CubeCL entered the Rust 1.89 feature-off dependency graph.' >&2 + cat forecast-feature-off-tree.txt >&2 + exit 1 + fi + + - name: Check feature-off packages + working-directory: v2/crates/ruforecast + run: >- + cargo +1.89.0 check --locked + -p ruforecast-core + -p ruforecast-model + -p ruforecast-train + --no-default-features --all-targets + + - name: Test model-neutral core + working-directory: v2/crates/ruforecast + run: >- + cargo +1.89.0 test --locked + -p ruforecast-core + --no-default-features --lib --tests + + - name: Test model contracts without a model backend + working-directory: v2/crates/ruforecast + run: >- + cargo +1.89.0 test --locked + -p ruforecast-model + --no-default-features --lib --tests + + - name: Test RuVector retrieval boundary without a model backend + working-directory: v2/crates/ruforecast + run: >- + cargo +1.89.0 test --locked + -p ruforecast-model + --no-default-features --features ruvector --lib --tests + + - name: Clippy RuVector retrieval boundary + working-directory: v2/crates/ruforecast + run: >- + cargo +1.89.0 clippy --locked + -p ruforecast-model + --no-default-features --features ruvector --all-targets -- -D warnings + + - name: Clippy forecast packages without backend features + working-directory: v2/crates/ruforecast + run: >- + cargo +1.89.0 clippy --locked + -p ruforecast-core + -p ruforecast-model + -p ruforecast-train + --no-default-features --all-targets -- -D warnings + + - name: Upload feature-off dependency tree + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: ruforecast-feature-off-tree + path: v2/crates/ruforecast/forecast-feature-off-tree.txt + if-no-files-found: error + + hosted-boundaries-rust-189: + name: Forecast hosted boundaries and privacy contract (Rust 1.89) + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: Checkout recursively + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + submodules: recursive + + - name: Install Rust 1.89 with Clippy + uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 + with: + toolchain: 1.89.0 + components: clippy + + - name: Cache Cargo artifacts + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + workspaces: v2/crates/ruforecast + key: ruforecast-hosted-rust-189 + + - name: Enforce clean-room source, secret, and hosted-wire boundary + shell: bash + run: | + set -euo pipefail + python3 - <<'PY' + from pathlib import Path + import re + import sys + + roots = [ + Path("v2/crates/ruforecast/crates/ruforecast-core"), + Path("v2/crates/ruforecast/crates/ruforecast-model"), + Path("v2/crates/ruforecast/crates/ruforecast-train"), + ] + text_suffixes = {".json", ".py", ".rs", ".sh", ".toml", ".yaml", ".yml"} + forbidden_source = re.compile(r"(?i)\b(?:times[ _-]?fm|google[ _/-]?research)\b") + secret_patterns = { + "private key": re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"), + "AWS access key": re.compile(r"\bAKIA[0-9A-Z]{16}\b"), + "JWT": re.compile(r"\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b"), + "presigned URL": re.compile(r"(?i)[?&](?:X-Amz-Signature|X-Goog-Signature)="), + "literal FAL_KEY": re.compile(r"(?i)\bFAL_KEY\b\s*[:=]\s*['\"][^'$<{\"\n]{8,}['\"]"), + } + forbidden_blobs = {".ckpt", ".mpk", ".npy", ".npz", ".onnx", ".pt", ".pth", ".safetensors"} + + errors = [] + for root in roots: + for path in root.rglob("*"): + if not path.is_file() or "target" in path.parts: + continue + if path.suffix.lower() in forbidden_blobs: + errors.append(f"forbidden checked-in model/data blob: {path}") + continue + if path.suffix.lower() not in text_suffixes: + continue + text = path.read_text(encoding="utf-8") + if forbidden_source.search(text): + errors.append(f"forbidden clean-room source/import reference: {path}") + for label, pattern in secret_patterns.items(): + if pattern.search(text): + errors.append(f"possible {label} in {path}") + + fal_source = Path("v2/crates/ruforecast/crates/ruforecast-train/src/fal.rs").read_text() + for struct_name in ("HostedOptimizer", "HostedBudget", "HostedSyntheticPayload"): + match = re.search( + rf"pub struct {struct_name}\s*\{{(?P
.*?)^\}}", + fal_source, + flags=re.MULTILINE | re.DOTALL, + ) + if match is None: + errors.append(f"missing hosted wire struct: {struct_name}") + continue + body = match.group("body") + fields = re.findall(r"^\s*pub\s+([A-Za-z0-9_]+)\s*:", body, re.MULTILINE) + forbidden_fields = ( + "account", "data_policy", "dataset", "device", "identity", "path", + "person", "room", "session", "site", "split", "subject", "tenant", + "workspace", + ) + for field in fields: + lowered = field.lower() + if any(fragment in lowered for fragment in forbidden_fields): + errors.append(f"forbidden hosted field {struct_name}.{field}") + if "bytes" in lowered and field not in { + "max_artifact_bytes", + "max_memory_bytes", + }: + errors.append(f"unapproved hosted byte field {struct_name}.{field}") + if "DataPolicy" in body: + errors.append(f"DataPolicy entered hosted wire struct {struct_name}") + + if errors: + print("\n".join(errors), file=sys.stderr) + raise SystemExit(1) + PY + + - name: Enforce tracked-data policy + run: | + set -euo pipefail + bash scripts/csi-data-policy-check.sh --self-test + bash scripts/csi-data-policy-check.sh --tracked + + - name: Validate minimal fal deployment archive policy + run: >- + python3 + v2/crates/ruforecast/crates/ruforecast-train/deploy/fal/deploy.py + self-test + + - name: Prove hosted feature graph excludes Burn and CubeCL + working-directory: v2/crates/ruforecast + shell: bash + run: | + set -euo pipefail + cargo +1.89.0 tree --locked -e normal,build \ + -p ruforecast-train \ + --no-default-features --features cli,server,fal-client \ + > forecast-hosted-feature-tree.txt + if grep -Eiq '(^|[[:space:]])(burn|cubecl)(-|[[:space:]])' \ + forecast-hosted-feature-tree.txt; then + echo 'Burn/CubeCL entered the Rust 1.89 hosted dependency graph.' >&2 + cat forecast-hosted-feature-tree.txt >&2 + exit 1 + fi + + - name: Require named hosted boundary regressions + working-directory: v2/crates/ruforecast + shell: bash + env: + FAL_KEY: '' + run: | + set -euo pipefail + cargo +1.89.0 test --locked \ + -p ruforecast-train \ + --no-default-features --features cli,server,fal-client \ + --lib --bins --tests -- --list \ + | tee hosted-boundary-tests.txt + for regression in \ + fal_app_rejects_path_and_host_confusion \ + fal_urls_require_exact_origin_path_and_no_query \ + fal_submit_headers_disable_retry_and_omit_store_io \ + hosted_payload_dto_has_no_customer_fields \ + queue_status_discards_provider_error_body \ + result_url_accepts_exact_response_suffix \ + artifact_file_url_uses_expected_api_path \ + fal_key_debug_is_redacted \ + hosted_payload_rejects_app_field \ + hosted_reservation_cannot_exceed_source_retention \ + artifact_handoff_expiry_boundary_is_fail_closed \ + fal_result_enforces_cumulative_artifact_budget_boundary \ + fal_download_rejects_over_budget_without_writing \ + direct_server_train_requires_request_id_header \ + request_id_rejects_path_injection \ + unknown_cancel_is_not_success \ + privacy_external_dataset_payload_is_denied; do + grep -q "$regression" hosted-boundary-tests.txt \ + || { echo "Missing hosted boundary regression: $regression" >&2; exit 1; } + done + + - name: Exercise hosted external-data rejection + working-directory: v2/crates/ruforecast + env: + FAL_KEY: '' + run: >- + cargo +1.89.0 test --locked + -p ruforecast-train + --no-default-features --features cli,server,fal-client + privacy_external_dataset_payload_is_denied + + - name: Test server and fal client with mock integrations + working-directory: v2/crates/ruforecast + env: + FAL_KEY: '' + run: >- + cargo +1.89.0 test --locked + -p ruforecast-train + --no-default-features --features cli,server,fal-client + --lib --bins --tests + + - name: Clippy server and fal client boundaries + working-directory: v2/crates/ruforecast + env: + FAL_KEY: '' + run: >- + cargo +1.89.0 clippy --locked + -p ruforecast-train + --no-default-features --features cli,server,fal-client + --all-targets -- -D warnings + + - name: Clippy standalone CLI feature boundary + working-directory: v2/crates/ruforecast + env: + FAL_KEY: '' + run: >- + cargo +1.89.0 clippy --locked + -p ruforecast-train + --no-default-features --features cli + --bin ruforecast -- -D warnings + + - name: Upload hosted dependency tree + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: ruforecast-hosted-feature-tree + path: v2/crates/ruforecast/forecast-hosted-feature-tree.txt + if-no-files-found: error + + burn-cpu-rust-192: + name: Forecast Burn CPU tests and bench compile (Rust 1.92) + runs-on: ubuntu-latest + timeout-minutes: 90 + steps: + - name: Checkout recursively + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + submodules: recursive + + - name: Install Rust 1.92 with Clippy + uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 + with: + toolchain: 1.92.0 + components: clippy + + - name: Cache Cargo artifacts + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + workspaces: v2/crates/ruforecast + key: ruforecast-rust-192-cpu + + - name: Test model with Burn CPU backend + working-directory: v2/crates/ruforecast + run: >- + cargo +1.92.0 test --locked + -p ruforecast-model + --no-default-features --features cpu --lib --tests + + - name: Test trainer and CLI with Burn CPU backend + working-directory: v2/crates/ruforecast + run: >- + cargo +1.92.0 test --locked + -p ruforecast-train + --no-default-features --features cpu,cli --lib --bins + + - name: Run one-step local JSONL training smoke + working-directory: v2/crates/ruforecast + run: >- + cargo +1.92.0 test --locked + -p ruforecast-train + --no-default-features --features cpu + --test local_jsonl_smoke -- + --exact local_hash_addressed_jsonl_executes_one_real_optimizer_step + + - name: Run idempotent synthetic CLI training smoke + working-directory: v2/crates/ruforecast + run: >- + cargo +1.92.0 test --locked + -p ruforecast-train + --no-default-features --features cpu,cli + --test cli_smoke -- + --exact cli_smoke_trains_and_writes_the_complete_candidate_set + + - name: Test combined CPU and hosted feature surface + working-directory: v2/crates/ruforecast + env: + FAL_KEY: '' + run: >- + cargo +1.92.0 test --locked + -p ruforecast-train + --no-default-features --features cpu,cli,server,fal-client + --lib --bins + + - name: Clippy model with Burn CPU backend + working-directory: v2/crates/ruforecast + run: >- + cargo +1.92.0 clippy --locked + -p ruforecast-model + --no-default-features --features cpu --all-targets -- -D warnings + + - name: Clippy trainer and CLI with Burn CPU backend + working-directory: v2/crates/ruforecast + env: + FAL_KEY: '' + run: >- + cargo +1.92.0 clippy --locked + -p ruforecast-train + --no-default-features --features cpu,cli,server,fal-client + --all-targets -- -D warnings + + - name: Compile forecast inference benchmark + working-directory: v2/crates/ruforecast + run: >- + cargo +1.92.0 bench --locked + -p ruforecast-model + --no-default-features --features cpu + --bench forecast_inference --no-run + + - name: Compile forecast data-pipeline benchmark + working-directory: v2/crates/ruforecast + run: >- + cargo +1.92.0 bench --locked + -p ruforecast-train + --no-default-features --features cpu + --bench data_pipeline --no-run + + - name: Run quick inference benchmark for trend visibility + continue-on-error: true + timeout-minutes: 15 + working-directory: v2/crates/ruforecast + run: | + set -o pipefail + mkdir -p ../../bench-out/ruforecast + cargo +1.92.0 bench --locked \ + -p ruforecast-model \ + --no-default-features --features cpu \ + --bench forecast_inference -- \ + --warm-up-time 1 --measurement-time 2 --sample-size 10 \ + | tee ../../bench-out/ruforecast/forecast-inference.txt + + - name: Run quick data-pipeline benchmark for trend visibility + continue-on-error: true + timeout-minutes: 15 + working-directory: v2/crates/ruforecast + run: | + set -o pipefail + mkdir -p ../../bench-out/ruforecast + cargo +1.92.0 bench --locked \ + -p ruforecast-train \ + --no-default-features --features cpu \ + --bench data_pipeline -- \ + --warm-up-time 1 --measurement-time 2 --sample-size 10 \ + | tee ../../bench-out/ruforecast/data-pipeline.txt + + - name: Upload informational benchmark logs + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: ruforecast-informational-cpu-benchmarks + path: bench-out/ruforecast/ + if-no-files-found: warn + + burn-cuda-compile-rust-192: + name: Forecast Burn CUDA/WGPU compile checks only (Rust 1.92) + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout recursively + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + submodules: recursive + + - name: Install Rust 1.92 + uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 + with: + toolchain: 1.92.0 + + - name: Cache Cargo artifacts + uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 + with: + workspaces: v2/crates/ruforecast + key: ruforecast-rust-192-cuda-check + + - name: Check model CUDA feature + working-directory: v2/crates/ruforecast + run: >- + cargo +1.92.0 check --locked + -p ruforecast-model + --no-default-features --features cuda --lib + + - name: Check model WGPU feature + working-directory: v2/crates/ruforecast + run: >- + cargo +1.92.0 check --locked + -p ruforecast-model + --no-default-features --features wgpu --lib + + - name: Check trainer CUDA and hosted-server feature combination + working-directory: v2/crates/ruforecast + env: + FAL_KEY: '' + run: >- + cargo +1.92.0 check --locked + -p ruforecast-train + --no-default-features --features cuda,cli,server + --lib --bins diff --git a/.gitmodules b/.gitmodules index 39ef969a3..312c1b9bf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -33,3 +33,7 @@ path = vendor/metaharness url = https://github.com/ruvnet/metaharness branch = main +[submodule "v2/crates/ruforecast"] + path = v2/crates/ruforecast + url = https://github.com/ruvnet/RuForecast.git + branch = main diff --git a/docs/adr/ADR-348-independent-rust-multivariate-forecasting.md b/docs/adr/ADR-348-independent-rust-multivariate-forecasting.md new file mode 100644 index 000000000..50272fe23 --- /dev/null +++ b/docs/adr/ADR-348-independent-rust-multivariate-forecasting.md @@ -0,0 +1,336 @@ +# ADR-348: Independent Rust multivariate forecasting for RuView + +- **Status**: Proposed +- **Date**: 2026-09-01 +- **Deciders**: ruv +- **Owners**: RuView perception, model, security, and data-governance maintainers +- **Tags**: forecasting, rust, ruvector, temporal, uncertainty, clean-room, provenance +- **Extends**: ADR-016, ADR-020, ADR-145, ADR-273, ADR-282, ADR-295, + ADR-298, ADR-302, ADR-304, ADR-317, ADR-318, ADR-319 +- **Supersedes**: None + +## Executive decision + +RuView will develop an independently specified and independently trained Rust +multivariate forecasting subsystem. It will forecast bounded, versioned +temporal feature streams, publish calibrated quantiles and an explicit +abstention state, optionally use a split-safe RuVector analogue index, and +remain advisory until all release gates in this ADR pass. + +This is not a port, compatibility layer, distillation, or behavioural clone of +Google TimesFM. The implementation API is derived from RuView requirements. +Its weights must descend from an approved random initialization and approved +training data only. + +**Evidence status at proposal:** the forecasting accuracy, false-alert +reduction, calibration, CPU latency, memory use, cross-building generalization, +and operational value described below are all **UNMEASURED targets**. This ADR +authorizes implementation and evaluation; it makes no `MEASURED` capability +claim and approves no production model. + +## Context + +RuView currently observes present and recent RF state. Forecasting could add a +separate answer to questions such as whether a feature trajectory is consistent +with real occupancy, whether motion is likely to transition between zones, or +whether a radio link is degrading. A forecast is not a sensor observation. It +is derived evidence with uncertainty and must never overwrite the immutable +observation that produced it. + +TimesFM 3 is useful research context because its public description discusses +multivariate targets, past covariates, known-future covariates, probabilistic +outputs, temporal patching, and cross-series attention. Its licensing creates +two distinct surfaces: + +- the Google TimesFM source repository states that source code is Apache-2.0; +- the TimesFM 3 pretrained weights are distributed under a separate + non-commercial, non-production license that also restricts commercial + training, fine-tuning, and distillation from the model. + +An Apache-compliant Rust port would be a permissible but attributed derivative +source path, subject to its exact license obligations. It would not support the +strongest independent-development claim. Using the restricted TimesFM 3 model +or its outputs to create a RuView commercial model is outside this ADR. The +project therefore chooses the stricter clean-room protocol in +[`../security/ruview-forecast-clean-room.md`](../security/ruview-forecast-clean-room.md). + +Clean-room controls reduce copyright, contract, and provenance risk. They do +not establish freedom to operate against patents, clear trademarks, approve a +dataset, or replace legal review in a launch jurisdiction. + +## Options considered + +1. **Use the TimesFM 3 checkpoint in RuView.** Rejected for commercial and + production use under the current checkpoint license. +2. **Translate the Apache-licensed implementation into Rust.** Not selected. + This can be evaluated under a separate ADR, with attribution and license + compliance, but must not be mixed with the independent-development path. +3. **Build an independent Rust forecasting specialist for RuView.** Chosen. + This narrows model scope, keeps inference native, controls provenance, and + makes RF-specific evaluation the source of authority. +4. **Do not add forecasting.** Retained as the deployment baseline. Forecasting + must beat simpler deterministic and statistical baselines before acquiring + runtime authority. + +## Requirements + +| ID | Requirement | Verification authority | +|---|---|---| +| RF-001 | No TimesFM 3 code, configuration, weights, parameters, outputs, activations, or derivatives enter specification, implementation, training, evaluation feedback, or release artifacts. | Clean-room manifest, contributor attestations, repository and artifact scans | +| RF-002 | Every contributor, reference, dependency, dataset, transform, training job, and checkpoint has immutable provenance. | Signed provenance manifests and review receipts | +| RF-003 | Every training byte has documented rights for commercial machine-learning use and applicable privacy approval. | Dataset licensing gate and data-steward approval | +| RF-004 | The default runtime is native Rust, bounded, offline, deterministic for a fixed artifact/input within the declared platform class, and free of runtime model download. | Unit, property, fuzz, replay, and dependency tests | +| RF-005 | Inputs and outputs are versioned, finite, bounded, timestamped, mask-aware, and provenance-labelled. Invalid, stale, insufficient, or OOD inputs abstain. | Contract tests and malformed-input corpus | +| RF-006 | Forecasts remain derived evidence. They cannot rewrite observations, silently increase observation confidence, or be relabelled `MEASURED`. | Schema invariants and evidence-engine tests | +| RF-007 | Quantile ordering, interval calibration, missing-data behaviour, and abstention are evaluated on leakage-free site/session/device holdouts. | Frozen evaluation manifest and reproducible report | +| RF-008 | RuVector retrieval is optional, split-scoped, provenance-recorded, and evaluated against the same model without retrieval. | Retrieval isolation tests and paired ablation | +| RF-009 | RuVLLM may explain or summarize a signed forecast but cannot alter numeric forecasts, bypass policy, spend money, or trigger medical, emergency, access-control, or life-safety action. | Capability tests and downstream policy review | +| RF-010 | Training on local Linux or a hosted accelerator uses identical signed code, container, data, and configuration identities; the provider is an untrusted processor. | Training receipts and digest comparison | +| RF-011 | Accuracy, latency, memory, power, and operational claims remain `UNMEASURED` or `CLAIMED` until a named reproducer satisfies the repository evidence contract. | Documentation and release review | +| RF-012 | Rollout is reversible and cannot advance from offline evaluation to shadow or advisory operation without the mapped gates below. | Signed mode transition and rollback drill | + +## Architecture boundary + +The initial implementation is split across three crates: + +| Crate | Owned responsibility | Dependency/runtime rule | +|---|---|---| +| `ruview-forecast-core` | Backend-neutral schemas, invariants, metrics, forecast receipts, and the `Forecaster` trait | No Burn, CUDA, WGPU, provider SDK, sensing-server, or network dependency | +| `ruview-forecast-model` | Independent Burn 0.21 patch-mixer architecture and artifact execution | CPU, CUDA, and WGPU are explicit optional features; every backend feature is off by default | +| `ruview-forecast-train` | Dataset manifests/splits, trainer, evaluator, the `ruforecast` CLI, and Linux/fal.ai training assets | Training-only authority; no production activation or sensing-server mutation | + +This PR does not connect the forecaster to the sensing server. A shadow bridge +requires a follow-up change after the core contract and evidence receipt have +stabilized. Landing crates or passing synthetic tests therefore creates no +live RuView forecasting capability. + +Version one activates only the exact reviewed `tiny_ci` and `large_linux` +architecture profiles. The model and training boundaries enforce checked +dimension, parameter, activation-cell, input-cell, and forward multiply-add +limits per batch. Adding or changing a profile is therefore a reviewed code and +artifact-schema decision, not untrusted request configuration. + +The proposed logical pipeline is: + +```text +authenticated RF observations + | + v +versioned one-second feature windows + | + +----> split-scoped RuVector analogue retrieval (optional) + | + v +independent Rust temporal model + | + v +point forecast + ordered quantiles + validity mask + abstention + | + v +signed derived-evidence record + | + +----> RuView policy/evidence engine + +----> RuVLLM explanation with no numeric mutation authority +``` + +The initial feature contract should favour compact one-second summaries rather +than feeding an unbounded raw CSI stream. Candidate fields include motion +energy, Doppler summary, amplitude/phase dispersion, coherence, RSSI, packet +loss, channel utilization, current detector confidence, device temperature, +and source-validity masks. Each field needs a schema version, physical unit, +aggregation rule, validity semantics, and provenance. Adding a field is a +schema change, not an implicit positional extension. + +The model may independently combine normalization, temporal patch encoding, +temporal mixing, cross-stream fusion, and quantile heads. This is a functional +design space, not permission to reproduce protected source expression, +checkpoint dimensions, constants, tests, diagrams, or API choices. Exact +architecture and parameter count remain implementation decisions recorded in +the model card and training receipt. + +## Forecast contract + +For each request the runtime returns one typed result, including abstention and +error paths. The result must identify: + +- input schema, feature-window, model, configuration, and calibration digests; +- source time range, requested horizon, cadence, target streams, and masks; +- point estimate and declared quantiles for each target/horizon cell; +- whether quantiles were corrected for crossing and which method was used; +- OOD, insufficient-context, stale-input, and non-finite dispositions; +- optional RuVector index version, neighbour identifiers, distances, and + leakage-scope receipt; +- runtime platform class, duration, and peak-memory measurement when enabled; +- evidence label `DERIVED_FORECAST`, never `MEASURED_OBSERVATION`; +- deterministic content hash and optional RVF signature. + +The runtime rejects non-finite values, duplicate or non-monotonic timestamps, +unsupported schema versions, unbounded dimensions, invalid quantile requests, +future covariates without an allowed source, and payloads above configured +limits. It abstains rather than imputing an authoritative state when history or +validity coverage is below the model-card threshold. + +## RuVector integration + +RuVector supplies temporal memory, not ground truth. During training and +evaluation, each split has a separate index built only from records permitted +for that split. A test query may retrieve training analogues, but it may never +retrieve a window from the same subject/session/site holdout, an overlapping +target horizon, or any test record. Fitted normalizers and retrieval thresholds +come from training data only. + +Every release reports three comparable rows on identical examples: + +1. deterministic/statistical baseline; +2. forecasting model without retrieval; +3. forecasting model with RuVector retrieval. + +This prevents retrieval from hiding a weak model or leaking future state. + +## RuVLLM and action authority + +RuVLLM consumes an immutable, signed forecast record and may produce an +explanation referencing its uncertainty and provenance. Its prose is not the +forecast and receives no higher evidence grade. Numeric values exposed to APIs +come from the forecasting record, not regenerated text. + +No forecast or LLM explanation independently triggers a health alert, emergency +response, access decision, actuator, firmware change, model promotion, or +commercial spend. Such actions require a separate governed policy, explicit +capability, and appropriate observed evidence. + +## Training and artifact boundary + +Training jobs run from a pinned container or reproducible local environment. +The receipt binds the source commit, lockfile, compiler, container, datasets, +transforms, architecture, hyperparameters, seeds, hardware, provider job ID, +start/end time, and every emitted checkpoint. Hosted workers receive only the +least data and credentials necessary. Provider caches, logs, retention, and +reuse rights must pass security and data review before customer-derived data is +uploaded. + +A release artifact is immutable and hash addressed. It is loaded locally after +signature, schema, size, and compatibility verification. The inference process +does not fetch models, execute embedded code, accept arbitrary operators, or +contact the training provider. + +## Leakage and evaluation protocol + +- Split by deployment/site first, then subject, session, device, and contiguous + time block as applicable. No overlapping raw sequence or derived window may + cross a split. +- Freeze test manifests before tuning. A failed test result does not become a + new training target; material architecture changes require a new untouched + holdout. +- Fit normalization, calibration, feature selection, thresholds, and RuVector + index parameters on training/validation data only. +- Report missingness, abstention coverage, selective risk, per-horizon errors, + interval coverage, quantile loss, and results by site/device/interference + regime. Pooled performance cannot hide a failed domain. +- Compare against last value, seasonal naive, and at least one small classical + or recurrent baseline. TimesFM 3 output is not an acceptance oracle. +- Forecast evaluation does not prove presence, pose, fall, respiration, or + medical accuracy. Any downstream claim requires its own labelled protocol. + +## Acceptance gates and requirement mapping + +Targets below are release criteria, not current results. + +| Gate | Requirements | Pass condition | Current state | +|---|---|---|---| +| G0 independent-authoring boundary | RF-001, RF-002 | Approved source allowlist; 100% current contributor attestations; zero restricted artifacts or outputs; all similarity findings adjudicated by the clean-room custodian. | **OPEN / UNMEASURED** | +| G1 data and lineage | RF-002, RF-003, RF-010 | 100% of training/evaluation bytes resolve to approved manifests; zero unknown, noncommercial, research-only, no-ML, or no-derivatives sources; checkpoint lineage reaches approved random initialization; local/hosted receipts bind identical governed inputs. | **OPEN / UNMEASURED** | +| G2 bounded Rust contract | RF-004, RF-005, RF-006 | Unit/property/fuzz tests cover dimensions, non-finite values, timestamp order, masks, quantile order, stale/OOD/insufficient context, deterministic hashes, offline loading, evidence labels, and resource caps; 24-hour accelerated replay has no panic or unbounded growth. | **OPEN / UNMEASURED** | +| G3 leakage-free model evidence | RF-007, RF-008, RF-011 | Frozen site/session/device-disjoint report; all baseline and ablation rows present; nominal 80% interval coverage target is 75%-85%; weighted quantile loss target is at least 10% better than seasonal naive; every metric has a reproducer. | **OPEN / TARGETS UNMEASURED** | +| G4 RuView shadow value | RF-006, RF-007, RF-008, RF-009 | At least 14 days shadow-only; empty-room false-alert target is at least 50% relative reduction without more than 2 percentage points occupied-room recall loss; no safety-critical action authority; drift and abstention reported by deployment. | **OPEN / TARGETS UNMEASURED** | +| G5 deployment fitness | RF-004, RF-009, RF-010, RF-011, RF-012 | CPU p95 target is at most 1 second for 32 declared streams with peak process memory at most 4 GiB; signed model card/SBOM/provenance; privacy, security, trademark, dataset, and patent reviews; rollback drill; no open severity 1/2 issue. | **OPEN / TARGETS UNMEASURED** | + +Failure at any gate keeps the model offline or shadow-only. Passing a software +gate is not real-hardware evidence and does not convert an accuracy target into +a measurement. + +## Rollout and rollback + +The only permitted progression is: + +```text +OFF -> OFFLINE_EVAL -> SHADOW -> ADVISORY +``` + +This ADR does not authorize autonomous action. Each transition records actor, +old/new mode, artifact and configuration digests, evidence report, and reason. +Regression, provenance failure, calibration drift, security incident, or +licensing uncertainty returns immediately to `OFF` or `SHADOW` while observed +RuView sensing continues unchanged. + +Rollback deactivates the forecast artifact atomically, clears ephemeral model +and retrieval state, preserves signed aggregate evidence and incident records, +and never downgrades or rewrites the raw observation stream. + +## Security and privacy + +Forecasting can infer routines from occupancy and movement even without raw +CSI. Feature windows, neighbour identifiers, forecasts, and explanations are +therefore deployment data subject to purpose limitation, tenant isolation, +retention, deletion, access control, and audit. + +Model artifacts, feature schemas, calibration, indexes, and manifests are +untrusted until their signature and digest verify. Cardinality, dimensions, +horizon, context, allocations, execution time, and concurrent requests are +bounded. Metrics use allowlisted aggregate values and exclude raw CSI, precise +room coordinates, persistent person identifiers, and unrestricted feature +payloads. + +The detailed authoring, data, AI-tool, incident, trademark, and patent controls +are normative in +[`../security/ruview-forecast-clean-room.md`](../security/ruview-forecast-clean-room.md). + +## Consequences + +### Positive + +- RuView gains a provider-neutral predictive evidence primitive with explicit + uncertainty and abstention. +- Rust-native inference can be evaluated on CPU without requiring a Python or + cloud runtime in production. +- RuVector retrieval becomes a measurable ablation rather than an implicit + memory claim. +- Signed provenance supports reproducible Linux and hosted training. + +### Negative + +- Independent training needs substantial diverse, correctly licensed temporal + data and untouched deployment holdouts. +- Strict source separation and manifests increase contributor and review cost. +- A plausible forecast can make weak sensing look more authoritative unless + downstream evidence labels remain intact. +- Clean-room development does not remove patent, trademark, privacy, or dataset + risk. + +### Neutral + +- This ADR does not repair an inaccurate upstream presence or pose estimator. +- It does not approve TimesFM 3 for RuView use. +- It does not select a final model size, training budget, or hosted provider. +- No production checkpoint or measured benchmark is created by this decision. + +## References + +- [TimesFM 3 public research article](https://research.google/blog/timesfm-3-a-zero-shot-foundation-model-for-multivariate-forecasting/) +- [Google TimesFM repository and source-license notice](https://github.com/google-research/timesfm) +- [TimesFM 3 checkpoint license](https://huggingface.co/google/timesfm-3.0-pytorch/blob/main/LICENSE) +- [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) +- [17 U.S.C. section 102](https://www.copyright.gov/title17/92chap1.html) +- [USPTO Patent Public Search](https://www.uspto.gov/patents/search/patent-public-search) +- [USPTO comprehensive trademark clearance](https://www.uspto.gov/trademarks/search/comprehensive-clearance-search-similar-trademarks) +- [RuView Forecast clean-room protocol](../security/ruview-forecast-clean-room.md) +- [RuView Forecast model-card template](../huggingface/RUVIEW_FORECAST_MODEL_CARD_TEMPLATE.md) +- [ADR-145](./ADR-145-ablation-eval-harness-privacy-leakage.md) +- [ADR-282](./ADR-282-ruview-ecosystem-positioning.md) +- [ADR-295](./ADR-295-source-provenance-state-machine.md) +- [ADR-298](./ADR-298-model-release-sanity-gates.md) +- [ADR-302](./ADR-302-out-of-distribution-detection.md) +- [ADR-304](./ADR-304-evidence-engine.md) +- [ADR-317](./ADR-317-benchmark-multi-domain-scorecard.md) +- [ADR-318](./ADR-318-capability-certificates.md) +- [ADR-319](./ADR-319-witness-chain.md) diff --git a/docs/adr/ADR-349-governed-local-and-fal-forecast-training.md b/docs/adr/ADR-349-governed-local-and-fal-forecast-training.md new file mode 100644 index 000000000..caafd240f --- /dev/null +++ b/docs/adr/ADR-349-governed-local-and-fal-forecast-training.md @@ -0,0 +1,231 @@ +# ADR-349: Governed local and fal.ai forecast training + +- **Status**: Proposed +- **Date**: 2026-09-01 +- **Deciders**: ruv +- **Owners**: RuView forecast training, security, and release maintainers +- **Tags**: forecasting, training, fal, rust, receipts, idempotency, cost-control +- **Parent**: ADR-348 +- **Extends**: ADR-010, ADR-145, ADR-298, ADR-319, ADR-348 +- **Supersedes**: None + +## Decision + +RuView Forecast will use one Rust training engine behind two deliberately +different request boundaries. The local Linux request may bind a governed, +hash-addressed RuView JSONL shard. Hosted v1 accepts only a bounded, +deterministic synthetic generator recipe. It cannot serialize a local +`TrainSpec`, `DataPolicy`, path, dataset bytes, tenant/account/workspace/site/ +device/session identity, split receipt, or RuVector namespace. + +The fal.ai path is a bounded execution adapter, not a remote shell. It may +train the fixed synthetic profile and export fixed artifact kinds, but it may +not choose an arbitrary command, executable, container, URL, environment +variable, output path, or release action. A later real-data hosted path needs a +new accepted ADR, provider/privacy review, and an operator-signed export grant; +it is intentionally not implemented here. + +Every hosted result is untrusted and quarantined until it is exported with a +complete receipt, downloaded through the authenticated provider channel, +re-hashed locally, scanned, and bound to the original request. Only the local +release boundary may sign or activate it. Signing keys never enter fal.ai. + +This ADR delegates dataset rights, clean-room evidence, model-card, patent, +trademark, and production gates to [ADR-348](./ADR-348-independent-rust-multivariate-forecasting.md) +and the [clean-room protocol](../security/ruview-forecast-clean-room.md). + +**Evidence status:** typed local and hosted configuration, root-confined file +verification, atomic fixed-kind artifact publication, and cooperative +cancellation source surfaces exist. End-to-end local training and mock hosted +transport are software-testable in this change. Real fal.ai execution, +provider cancellation, cost reconciliation, quarantine promotion, and local +signature verification remain **UNMEASURED and unapproved** until their named +acceptance evidence exists. + +## Context + +The same training engine needs to run on a 128 GiB Linux workstation and on an +optional hosted accelerator without creating two architecture lineages. A +generic remote-command interface would allow request data to become executable +authority, make cost difficult to bound, and weaken cancellation. Sending a +general local request to fal would also expose governance and customer +identifiers that the hosted smoke path does not need. + +The initial `ruview-forecast-train` crate already separates three relevant +modules: + +| Module | Current contract | +|---|---| +| [`config.rs`](../../v2/crates/ruview-forecast-train/src/config.rs) | `TrainingRequest`, `ValidatedTrainingRequest`, bounded `JobId`, root-relative dataset path, exact size/SHA-256, named model profiles, typed CPU/CUDA choice, and bounded optimizer values; unknown fields are denied | +| [`artifact.rs`](../../v2/crates/ruview-forecast-train/src/artifact.rs) | verified open dataset handle, root confinement, fixed `Model`/`Manifest`/`Receipt`/`Checkpoint` kinds, atomic writes, content descriptors, and conflict-on-different-bytes idempotency | +| [`cancel.rs`](../../v2/crates/ruview-forecast-train/src/cancel.rs) | `Cancellation`, `CancelToken`, and cooperative cancellation checks for batch/checkpoint boundaries | + +The training crate features keep `cli`, `server`, `fal-client`, `cpu`, and +`cuda` explicit, with default features empty. WGPU remains a model-crate +inference backend and is not advertised as a training runner. The `ruforecast` +binary requires the `cli` feature. Source presence is not execution evidence. + +## Typed execution contracts + +The non-serializable trusted `TrainingRequest` is built locally from a +size-bounded `LocalTrainingRequestWire`. It contains: + +- stable job identity; +- a constructor-validated local split/horizon/normalization `TrainSpec`; +- either a root-relative dataset identity with exact byte count and SHA-256, + or a deterministic synthetic recipe; +- one allowlisted model capacity profile and compiled-in local backend; +- bounded optimizer, checkpoint cadence, seed, memory, step, time, checkpoint, + and artifact budgets. + +Local JSON and TOML deny unknown fields. Dataset paths resolve below an +operator-configured root, and the exact regular file remains open after +size/hash verification to avoid replacement between validation and use. JSONL +windows are decoded incrementally with per-line and per-window caps rather +than loading the full shard into memory. + +The distinct `HostedSyntheticRequestV1` contains only a schema version, +idempotency/request digests, a fixed model profile, deterministic generator +parameters and seed, bounded optimizer/resource caps, and public build +identities. Its constructor needs a non-serializable core governance authority +proving that the synthetic recipe is the approved hosted source. Hosted v1 has +no external dataset field. + +Neither request contains a shell string, argument vector, executable path, +container/image selector, package installer, arbitrary URL, callback URL, +environment map, secret, source fragment, or unrestricted output name. + +The local client maps `HostedSyntheticRequestV1` to one allowlisted, +process-configured fal app and pre-deployed content-addressed worker image. The +caller cannot override the app, machine, worker entry point, or artifact +destination. The endpoint returns typed status or artifact descriptors only. +Provider paths are derived from validated job identity and fixed artifact +kinds, then downloaded through the authenticated fal Platform Files API; they +are not accepted as remote URLs or local paths. + +The deploy wrapper builds from an allowlisted `git archive`, selects the exact +`fal_app.py::run_server` symbol, and passes `--auth private` for both `fal run` +and `fal deploy`; ephemeral Fal runs otherwise default to public. Hosted v1 +retains only bounded synthetic request/result metadata so its typed queue result +can be reconciled. It sets `X-Fal-No-Retry: 1` and a bounded timeout rather than +claiming that `X-Fal-Store-IO: 0` or an unverified lifecycle header protects +the result. A live unauthenticated 401/403 probe and provider retention/deletion +reconciliation remain blocking operational evidence. + +## Idempotency and cost authority + +The local effective idempotency identity is: + +```text +job_id + canonical request digest + source commit + lockfile digest ++ container digest + dataset/split digest + initial-weight digest +``` + +Hosted v1 replaces `dataset/split digest` with the canonical synthetic-recipe +digest. No local governance identifier is part of, or derivable from, the +hosted payload. + +Repeating an identical completed request returns the existing verified receipt +and descriptors. Reusing `job_id` with any different governed input fails as a +conflict. A retry after an ambiguous network failure must first query the same +provider job; it must not silently create a second billable run. + +Before hosted submission, the caller sets explicit maximum steps, wall-clock +time, memory, checkpoint count, export bytes, and billable units. Submission +also requires an explicit local spend-approval record bound to the request +digest and maximum units. The adapter rejects a request when its configured +cap exceeds that approval. The final receipt records estimated and actual +provider units/cost when the provider supplies them, the price source/time, +and whether the amount is final or provider-reported. A provider estimate is +`CLAIMED`, never `MEASURED`, until reconciled against a bill. + +No automatic retry may increase the approved budget. Budget changes create a +new signed authorization record. The initial wire carries operator ceilings +for wall time, billable seconds, and micro-USD, but it does not yet bind a +provider price quote or reconcile a final provider ledger. Monetary enforcement +therefore remains an operator/Fal-account control and the production hosted +spend gate stays open. + +## Cancellation and checkpoints + +Cancellation is cooperative and idempotent: + +1. Local signal or authenticated hosted cancel marks one `job_id` cancelled. +2. Training checks `Cancellation` at every batch boundary and before/after each + checkpoint/export boundary. +3. A cancellation checkpoint is committed atomically when safe and within the + artifact budget; a partial file never becomes a valid artifact. +4. The terminal receipt records `cancelled`, the last completed epoch/step, + checkpoint digest, provider state, elapsed resource units, and final known + cost. +5. Repeating cancel returns the same terminal state. + +Cancellation does not promise immediate GPU termination. Cancellation latency, +checkpoint durability, and residual provider billing are **UNMEASURED** until a +real fal.ai run supplies a receipt. A timeout or lost cancellation response +keeps the job and any output quarantined. + +## Export, quarantine, and local signing + +The worker may emit only the fixed artifact set from `ArtifactKind`: + +| Kind | Required purpose | +|---|---| +| `Model` | Burn model record bytes | +| `Manifest` | architecture, data/split, seed, build, and clean-room identity | +| `Receipt` | request, environment, metrics, cost, status, and export lineage | +| `Checkpoint` | final or cancellation-time model weights; v1 does not claim optimizer/cursor resume | + +Every descriptor includes kind, size, and SHA-256. Export acceptance requires +all mandatory descriptors, bounded lengths, exact hashes, the expected job and +request digest, no duplicate kind, and a terminal status consistent with the +artifact set. + +Downloaded files enter a non-executable quarantine outside the model search +path. Local verification repeats envelope/schema/size/hash checks, SBOM and +malware/policy scans, clean-room declarations, dataset/split lineage, metric +labels, and parent-checkpoint validation. No provider output may update a +symlink, current-model pointer, RuVector index, sensing server, or release tag. + +After quarantine passes, an authorized local signer binds the exact export +receipt and artifact digests to an `ArtifactReceipt` and release manifest. The +signature records algorithm, public key ID, signer capability, and time. Fal.ai +receives no private signing material and cannot promote its own output. The +current core supplies canonical digests and receipts, not a complete release +signature implementation; signature-backed activation remains open. + +## Requirements and acceptance + +| ID | Requirement | ADR-348 gate | Acceptance evidence | Current state | +|---|---|---|---|---| +| FT-001 | Local and hosted adapters invoke the same model/trainer implementation while using intentionally separate local-data and hosted-synthetic request schemas. | G1 | Shared-engine tests plus hosted DTO exclusion and recipe-digest binding tests | **OPEN / UNMEASURED** | +| FT-002 | No request field or endpoint can select arbitrary code, command, image, URL, environment, or output path. | G2 | Schema negatives, endpoint capability test, dependency review | **OPEN / UNMEASURED** | +| FT-003 | Job/request idempotency prevents duplicate execution and conflicts on changed governed input. | G1, G5 | Concurrent/retry/lost-response tests with provider job query | **OPEN / UNMEASURED** | +| FT-004 | Hosted execution has explicit non-escalating resource and monetary caps with estimated/actual cost receipts. | G5 | Over-budget rejection and reconciled provider-bill fixture plus real-run receipt | **OPEN / UNMEASURED** | +| FT-005 | Cancellation is authenticated, cooperative, idempotent, checkpoint-safe, and terminally receipted. | G2, G5 | Local property tests and a real hosted cancellation drill | **OPEN / UNMEASURED** | +| FT-006 | Export is fixed-kind, bounded, complete, hash-verified, and bound to request/job/environment. | G1, G2 | Missing/duplicate/truncated/tampered export tests | **OPEN / UNMEASURED** | +| FT-007 | Hosted outputs remain quarantined until local verification and local-only signing succeed. | G1, G5 | Promotion-denial tests, signing-key absence check, rollback drill | **OPEN / UNMEASURED** | +| FT-008 | Hosted v1 receives synthetic inputs only; provider credentials, logs, retention, region, output reuse, and deletion satisfy ADR-348 privacy/security approval before any real run. | G1, G5 | Hosted DTO exclusion tests, provider review, and deletion/export receipts | **OPEN / UNMEASURED** | + +No successful unit test closes a hosted gate. G5 requires one bounded real +fal.ai run, one cancellation drill, one ambiguous-retry/idempotency drill, cost +reconciliation, quarantine rejection of a tampered export, and local signature +verification over the accepted artifact. Until then fal.ai support is a typed +software surface, not an operational capability claim. + +## Consequences + +The design gives local and hosted training one auditable lineage and keeps +provider output below local release authority. It adds receipt, budget, +quarantine, and signing work, and cooperative cancellation may still incur +provider cost. If a hosted provider cannot support bounded idempotent status, +export, cancellation, and deletion, the Linux path remains the only approved +training environment. + +## References + +- [ADR-348](./ADR-348-independent-rust-multivariate-forecasting.md) +- [RuView Forecast clean-room protocol](../security/ruview-forecast-clean-room.md) +- [`ruview-forecast-train` manifest](../../v2/crates/ruview-forecast-train/Cargo.toml) +- [`ruview-forecast-core` receipts](../../v2/crates/ruview-forecast-core/src/receipt.rs) +- [`ruview-forecast-model` public artifact boundary](../../v2/crates/ruview-forecast-model/src/lib.rs) diff --git a/docs/adr/ADR-350-ruvector-predictive-memory-and-ruvllm-boundary.md b/docs/adr/ADR-350-ruvector-predictive-memory-and-ruvllm-boundary.md new file mode 100644 index 000000000..675eeeff0 --- /dev/null +++ b/docs/adr/ADR-350-ruvector-predictive-memory-and-ruvllm-boundary.md @@ -0,0 +1,217 @@ +# ADR-350: RuVector predictive memory and RuVLLM authority boundary + +- **Status**: Proposed +- **Date**: 2026-09-01 +- **Deciders**: ruv +- **Owners**: RuView forecast, RuVector, evidence, and RuVLLM integration maintainers +- **Tags**: forecasting, ruvector, ruvllm, retrieval, memory, receipts, authority +- **Parent**: ADR-348 +- **Extends**: ADR-004, ADR-010, ADR-016, ADR-145, ADR-261, ADR-295, + ADR-304, ADR-319, ADR-348 +- **Supersedes**: None + +## Decision + +RuVector may augment RuView Forecast by retrieving analogous historical +feature/forecast states from a tenant- and split-scoped predictive-memory +index. Retrieval is optional context, never ground truth. Every release must +report the same frozen examples with retrieval disabled and enabled so its +incremental value and leakage risk remain visible. + +Each forecast is published in an immutable, locally signed envelope binding the +model artifact, request, output, source evidence, and retrieval receipt. RuVLLM +may read that envelope to explain uncertainty and analogous history. It may not +rewrite numeric forecasts, create a stronger evidence class, select a model, +promote an artifact, spend money, invoke an actuator, or make medical, +emergency, access-control, or life-safety decisions. + +This child ADR delegates forecasting, clean-room, data, and general production +gates to [ADR-348](./ADR-348-independent-rust-multivariate-forecasting.md). It +does not authorize a sensing-server bridge. + +**Evidence status:** canonical forecast/receipt types and an indexable latent +state exist in source. A RuVector adapter, retrieval receipt, signature wrapper, +outcome reconciler, RuVLLM explanation adapter, paired retrieval evaluation, +latency, storage cost, accuracy lift, and operational value are all +**UNMEASURED and unapproved**. + +## Existing implementation boundary + +The current crates establish only the preconditions: + +| Module | Implemented surface used by this ADR | +|---|---| +| [`ruview-forecast-core/src/forecast.rs`](../../v2/crates/ruview-forecast-core/src/forecast.rs) | validated `ForecastRequest`, backend-neutral `Forecaster`, ordered finite `Forecast`, canonical output digest, and receipt verification | +| [`ruview-forecast-core/src/receipt.rs`](../../v2/crates/ruview-forecast-core/src/receipt.rs) | `SourceState`, `ArtifactReceipt`, and `ForecastReceipt`; derived forecasts cannot retain `MEASURED` merely because measured input existed | +| [`ruview-forecast-core/src/series.rs`](../../v2/crates/ruview-forecast-core/src/series.rs) | bounded feature schema, masked time series, canonical series digest, and training-only scaler fit | +| [`ruview-forecast-model/src/network.rs`](../../v2/crates/ruview-forecast-model/src/network.rs) | `ForecastModelOutput.state` with shape `[batch, variates, d_model]`, intended as a bounded representation candidate for indexing | + +`ForecastReceipt` is content-addressed but not itself a digital signature. +`ForecastModelOutput.state` is indexable but not automatically safe, private, +stable across model versions, or useful. Those distinctions remain release +gates. + +## Predictive-memory record + +RuVector stores a versioned `PredictiveMemoryRecord` containing: + +- tenant and index namespace; +- model/artifact/config/feature-schema digests; +- source series and forecast-request digests; +- split, site, subject-class, session, device, calibration, and bounded time + scope using pseudonymous identifiers; +- bounded latent-state or engineered-feature digest plus the approved vector; +- forecast envelope digest and horizon; +- observed validity mask and evidence class; +- optional reconciled outcome digest added only after the forecast horizon; +- retention/deletion class and creation/expiry time. + +Raw CSI, unrestricted feature windows, precise room coordinates, persistent +person identity, RuVLLM prompts, and explanation prose are excluded by default. +The vector is still potentially sensitive because it can encode routines or +location. Tenant isolation, encryption, access control, retention, deletion, +and membership-inference review apply. + +An outcome is appended through a new immutable record linked to the original; +the historical forecast is never rewritten. Outcome reconciliation can measure +forecast quality but cannot retroactively turn the forecast into a measured +observation. + +## Split-scoped analogue retrieval + +Every index is bound to an immutable corpus manifest, model version, feature +schema, preprocessing configuration, and split policy. These rules are +mandatory: + +1. Tenants never share an index or query result without a separately authorized + privacy-preserving federation protocol. +2. Training, validation, calibration, and test records have distinct + namespaces. A test query may retrieve approved training analogues only; it + may not retrieve test examples or validation/calibration records used to set + thresholds. +3. The same site, subject, session, device, calibration episode, overlapping + context, or overlapping target horizon is excluded when that dimension is a + holdout. +4. Index construction, distance metric, normalization, filter policy, `k`, and + score threshold are fitted on training data and frozen before test. +5. Missing, stale, mismatched, unauthorized, or unverifiable indexes disable + retrieval and return the no-retrieval forecast or abstention according to + the model card. They never trigger an implicit global-index fallback. +6. Every query returns a bounded `RetrievalReceipt` even when zero neighbours + qualify. + +The retrieval receipt binds query digest, index/corpus/policy digests, +namespace, exclusion filter, neighbour record IDs and distances, `k`, latency, +and disposition. It excludes raw neighbour payloads from ordinary logs. + +## Paired ablation + +Retrieval has no deployment authority without a paired evaluation on identical +frozen requests: + +| Row | Model and examples | Retrieval | +|---|---|---| +| A | exact candidate artifact and frozen examples | disabled | +| B | exact candidate artifact and frozen examples | enabled with frozen index/policy | + +The report includes weighted quantile loss, interval coverage/width, +abstention, per-horizon error, per-site/device/interference slices, retrieval +hit/filtered/empty rates, latency, and memory/storage overhead. It reports both +aggregate and paired deltas with uncertainty. A gain on pooled error cannot +hide a failed deployment domain, calibration regression, or neighbour leakage. + +The no-retrieval row remains a supported fallback. Retrieval is removed when +its lower confidence bound does not show useful improvement, when it weakens +calibration beyond the ADR-348 gate, or when its privacy/latency/storage cost +exceeds its measured value. No uplift is claimed today. + +## Immutable signed forecast envelope + +The signed envelope covers: + +```text +envelope schema/version ++ ArtifactReceipt canonical digest ++ ForecastRequest canonical digest ++ Forecast payload/output digest ++ ForecastReceipt canonical digest ++ RetrievalReceipt digest or explicit retrieval-disabled marker ++ policy/calibration/index digests ++ creation/expiry time and tenant namespace +``` + +Signing occurs at the trusted local boundary after artifact and retrieval +verification. The wrapper records algorithm, public key ID, signer capability, +and signature. Private keys never enter model artifacts, RuVector, RuVLLM, or a +hosted training worker. + +Consumers verify signature, expiry, tenant, schemas, every nested digest, and +the `Forecast::verify_receipt` invariant before using numeric values. Any +failure yields unavailable/abstain. Unsigned content-addressed receipts remain +useful for local tests but are not described as signed and cannot cross the +production trust boundary. + +## RuVLLM explanation boundary + +RuVLLM receives a read-only projection of the verified envelope: + +- exact point and quantile values as structured fields; +- units, horizons, calibration/OOD/abstention state; +- bounded analogue summaries and retrieval receipt references; +- evidence labels, model/version, and envelope digest; +- approved explanation policy and audience. + +Explanation prose is stored separately and linked to the envelope digest. It +is `CLAIMED_EXPLANATION`, not a replacement forecast. Numeric API fields are +copied from the verified envelope after generation, never parsed back from LLM +text. If prose contradicts a structured number, evidence state, unit, or +disposition, the response fails validation and the numeric envelope remains +authoritative. + +The explanation capability exposes no tools for artifact activation, model +promotion, retraining, spending, messaging, emergency dispatch, access control, +or actuation. A separate downstream policy may consume a forecast only under +its own ADR, capabilities, approvals, and observed-evidence requirements. The +LLM cannot grant itself that authority or lower an approval threshold. + +## Requirements and acceptance + +| ID | Requirement | ADR-348 gate | Acceptance evidence | Current state | +|---|---|---|---|---| +| PM-001 | Every predictive-memory query is tenant-, model-, schema-, corpus-, split-, and time-scope bound. | G3, G5 | Cross-tenant/split/version negative tests and signed index manifest | **OPEN / UNMEASURED** | +| PM-002 | Holdout identities, overlapping contexts, and target horizons cannot appear as neighbours. | G3 | Property tests plus frozen leakage report | **OPEN / UNMEASURED** | +| PM-003 | Every query, including disabled/empty/error, produces a bounded retrieval receipt. | G2, G3 | Receipt round-trip, tamper, bound, and zero-result tests | **OPEN / UNMEASURED** | +| PM-004 | Retrieval-disabled and retrieval-enabled rows use identical artifact/examples and report paired metrics and overhead. | G3 | Frozen paired ablation with reproducer | **OPEN / UNMEASURED** | +| PM-005 | Forecast envelope signatures bind all nested forecast/retrieval identities and fail closed on mutation, expiry, tenant, or key error. | G2, G5 | Signature/tamper/replay/cross-tenant tests and local signing receipt | **OPEN / UNMEASURED** | +| PM-006 | Derived forecast and explanation can never become `MEASURED_OBSERVATION` or exceed source evidence. | G2, G4 | Evidence-monotonicity schema/property tests | **OPEN / UNMEASURED** | +| PM-007 | RuVLLM cannot mutate structured numbers; contradictions fail validation and explanations remain separately labelled. | G4 | Numeric/unit/evidence mutation corpus and fail-closed integration tests | **OPEN / UNMEASURED** | +| PM-008 | RuVLLM explanation has no spending, promotion, messaging, safety, access-control, or actuator capability. | G4, G5 | Default-deny capability and attempted-escalation tests | **OPEN / UNMEASURED** | +| PM-009 | Vector/explanation privacy, retention, deletion, extraction, and membership risks are approved and operationally tested. | G5 | Privacy review, tenant deletion drill, and access audit | **OPEN / UNMEASURED** | + +G3 additionally requires the paired retrieval ablation to satisfy ADR-348's +forecast and calibration gates. G4 requires shadow evidence that retrieval and +explanation improve operator comprehension or forecast value without changing +numeric/action authority. G5 requires signed-envelope verification, rollback, +tenant deletion, and key-rotation drills. None has passed. + +## Consequences + +RuVector can evolve from retrospective search into outcome-linked predictive +memory while preserving a measurable no-retrieval baseline. RuVLLM can make +forecasts understandable without becoming the numeric or action authority. The +cost is additional index isolation, storage, signature, deletion, evaluation, +and policy complexity. If paired evidence does not justify that cost, the +correct release keeps retrieval and explanation disabled. + +## References + +- [ADR-348](./ADR-348-independent-rust-multivariate-forecasting.md) +- [ADR-349](./ADR-349-governed-local-and-fal-forecast-training.md) +- [RuView Forecast clean-room protocol](../security/ruview-forecast-clean-room.md) +- [RuView Forecast model-card template](../huggingface/RUVIEW_FORECAST_MODEL_CARD_TEMPLATE.md) +- [RuForecast benchmark protocol](../benchmarks/ruforecast.md) +- [ADR-016](./ADR-016-ruvector-integration.md) +- [ADR-145](./ADR-145-ablation-eval-harness-privacy-leakage.md) +- [ADR-261](./ADR-261-ruvector-graph-ann-index.md) +- [ADR-304](./ADR-304-evidence-engine.md) +- [ADR-319](./ADR-319-witness-chain.md) diff --git a/docs/adr/README.md b/docs/adr/README.md index 95a200e4f..d317a01c5 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -92,6 +92,9 @@ Statuses: **Proposed** (under discussion), **Accepted** (approved and/or impleme | [ADR-150](ADR-150-rf-foundation-encoder.md) | RF Foundation Encoder: pose-preserving, subject/room/device-invariant CSI embedding | Proposed | | [ADR-151](ADR-151-room-calibration-specialist-training.md) | Per-Room Calibration & Specialized Model Training (room-first → bank of small ruVector specialists) | Proposed | | [ADR-152](ADR-152-wifi-pose-sota-2026-intake.md) | WiFi-Pose SOTA 2026 Intake: geometry-conditioned calibration, external benchmarks, foundation-encoder recipe | Proposed | +| [ADR-348](ADR-348-independent-rust-multivariate-forecasting.md) | Independent Rust multivariate forecasting for RuView | Proposed | +| [ADR-349](ADR-349-governed-local-and-fal-forecast-training.md) | Governed local and fal.ai forecast training | Proposed | +| [ADR-350](ADR-350-ruvector-predictive-memory-and-ruvllm-boundary.md) | RuVector predictive memory and RuVLLM authority boundary | Proposed | ### Platform and UI diff --git a/docs/benchmarks/ruforecast.md b/docs/benchmarks/ruforecast.md new file mode 100644 index 000000000..a8393c53e --- /dev/null +++ b/docs/benchmarks/ruforecast.md @@ -0,0 +1,380 @@ +# RuForecast benchmark protocol + +## Evidence status + +No RuForecast runtime, accuracy, calibration, memory, or operational result is +recorded here yet. Every threshold below is an ADR-348 target, not a measured +claim. The first accepted row must identify a clean commit, the `Cargo.lock` +digest, the exact Rust toolchain, host, backend, model/configuration digest, +fixture or corpus digest, command, and evidence label. + +## Benchmark boundaries + +RuForecast uses two different forms of evidence: + +1. Deterministic correctness evidence comes from Rust unit, property, replay, + split-isolation, and artifact-tamper tests. A fixed input and artifact must + produce the same output within its declared platform class. +2. Runtime evidence comes from Criterion on a named host. Criterion inputs are + generated from fixed checked-in code and seeds, but elapsed time is not + deterministic. Timing from shared GitHub runners is informational only. + +The benchmark implementation must not download a model or dataset, read raw +CSI, use a hosted model output, or enable CUDA implicitly. CPU benchmarks use +the explicit `cpu` feature and the Burn ndarray backend. CUDA validation belongs +to a separately governed Linux or hosted-accelerator receipt. + +## Required benchmark targets + +| Package | Target | Purpose | CI authority | +|---|---|---|---| +| `ruview-forecast-model` | `forecast_inference` | Fixed-seed forward pass, batch and shape scaling, ordered-quantile output | Compile gate; shared-runner timing is informational | +| `ruview-forecast-train` | `data_pipeline` | Fixed generated records through validation, windowing, masking and batching | Compile gate; shared-runner timing is informational | + +Both targets must use code-generated synthetic inputs, fixed seeds, bounded +allocations, `criterion::black_box`, and `required-features = ["cpu"]`. Setup, +artifact construction, and dataset generation stay outside the timed region +unless a benchmark name explicitly says they are included. + +The model implementation owns structural parameter-count assertions. The +currently reviewed design values are 35,700 parameters for the tiny CI preset +and 20,285,108 for the large preset. These are design invariants, not benchmark +results, and must be derived by a test from the actual module graph before they +are quoted in a model card. + +## Local Linux reproducer + +Run from a clean checkout after installing Rust 1.92.0: + +```bash +RUFORECAST_CPUSET=0-7 \ +RUFORECAST_THREADS=8 \ +scripts/run-ruforecast-benchmarks.sh +``` + +The runner executes the focused contract/model/training tests, one real +optimizer step over a local hash-addressed synthetic JSONL shard, and the +idempotent synthetic CLI smoke. It then compile-checks both Criterion targets, +runs the targets, captures the CPU and toolchain metadata, and hashes every +output. A failed run retains its partial logs with `status=FAILED` and its exit +code rather than looking like a complete report. Results go under +`target/ruforecast-evidence/`, which is excluded from source control. + +For a conservative single-thread reproducibility check, omit both environment +variables. To run against an uncommitted tree for diagnosis only, set +`RUFORECAST_ALLOW_DIRTY=1`; the resulting metadata is labelled `SYNTHETIC` +with scope `DIRTY_WORKTREE_DIAGNOSTIC_ONLY` and cannot support a release claim. + +A clean run labels its host timing `MEASURED` and its input class `SYNTHETIC`, +but remains `UNREVIEWED`. Only a maintainer may append it to the accepted ledger +after checking the digests, shape, command, Criterion report and host scope. + +The runner intentionally has no CUDA option and does not parse Criterion output +into a pass/fail performance verdict. This prevents a noisy host result from +silently acquiring release authority. + +To compile the two benchmark targets without measuring them: + +```bash +cd v2 +cargo +1.92.0 bench --locked -p ruview-forecast-model \ + --no-default-features --features cpu --bench forecast_inference --no-run +cargo +1.92.0 bench --locked -p ruview-forecast-train \ + --no-default-features --features cpu --bench data_pipeline --no-run +``` + +For a short informational run, use the same targets without `--no-run`: + +```bash +cargo +1.92.0 bench --locked -p ruview-forecast-model \ + --no-default-features --features cpu --bench forecast_inference -- \ + --warm-up-time 1 --measurement-time 2 --sample-size 10 +cargo +1.92.0 bench --locked -p ruview-forecast-train \ + --no-default-features --features cpu --bench data_pipeline -- \ + --warm-up-time 1 --measurement-time 2 --sample-size 10 +``` + +Running these commands does not add a ledger row automatically. Preserve the +raw report and environment metadata, then have a maintainer assign its evidence +scope before publishing a number. + +The inference bench runs only `tiny_ci` by default so a routine CI trend step +cannot accidentally start the very expensive large CPU probe. Set +`RUFORECAST_BENCH_LARGE=1` only on a controlled host when intentionally +measuring the fixed deployment shape: + +```bash +RUFORECAST_BENCH_LARGE=1 scripts/run-ruforecast-benchmarks.sh +``` + +## Deployment measurement shape + +The initial CPU deployment probe is batch 1, context 1,024, 32 declared feature +streams, the fixed `large_linux` horizon of 300, and all seven declared +quantiles. Record at least 20 warmup +iterations and 200 measured iterations for a release candidate. Report p50, +p95, p99 or maximum, throughput, and the process peak resident set size. + +ADR-348 G5 currently targets p95 at or below one second for 32 declared streams +and peak process memory at or below 4 GiB. A Criterion result alone cannot close +the memory gate because Criterion and Cargo are not the production inference +process. G5 remains open until a standalone inference probe reports its own peak +resident set size. + +## Accuracy and calibration protocol + +Runtime speed never substitutes for forecasting quality. The frozen evaluation +manifest must report identical examples for: + +1. Last-value and seasonal-naive baselines. +2. RuForecast without RuVector retrieval. +3. RuForecast with split-scoped RuVector retrieval. + +Required report fields include weighted quantile loss by horizon, nominal 80% +interval coverage, missingness, abstention coverage, selective risk, site and +device slices, interference regime, and retrieval ablation. ADR-348 G3 targets +weighted quantile loss at least 10% better than seasonal naive and 80% interval +coverage between 75% and 85%. Those targets remain unmeasured until a frozen, +leakage-free report is attached. + +### Informal HPO exploration note (unaccepted, not a release claim) + +**2026-09-01.** An exploratory session ran the accuracy protocol above end to +end against a governed 24-window **synthetic** dataset (`tiny_ci` profile, +context 64 / horizon 12, temporal train/test split, not entity-holdout — +only one synthetic generator was used, so entity holdout does not apply) and +a small `OptimizerSpec` hyperparameter search (learning rate, weight decay, +gradient clip norm, batch size, epochs) using a new Darwin Mode numeric-genome +evolution engine (upstream: `ruvnet/metaharness` PR #260, not yet merged). +This is **exploratory evidence only** — not a frozen, leakage-free, +maintainer-reviewed report, and not eligible for the ledger below until one +is produced. + +Prior to this exploration, a **single real household window** (76 real +1 Hz vital-signs samples, one physical ESP32 sensor, temporal not entity +holdout) scored **worse than both baselines** (WQL 0.537 vs. last-value +0.106 and seasonal-naive 0.123) — consistent with a single training window +overfitting rather than generalizing. + +With a larger (still synthetic, still `tiny_ci`) 24-window training set and +three rounds of hyperparameter search, weighted quantile loss on the held-out +synthetic split improved and stayed ahead of both baselines throughout: + +| Round | learning_rate | weight_decay | grad_clip | batch | epochs | WQL (model) | WQL (last-value) | WQL (seasonal-naive) | +|---|---:|---:|---:|---:|---:|---:|---:|---:| +| Default config | 0.0010000 | 1.00e-4 | 1.000 | 8 | 60 | 0.257 | 0.277 | 0.514 | +| Search round 1 | 0.0002356 | 3.05e-6 | 0.100 | 27 | 195 | 0.161 | 0.277 | 0.514 | +| Search round 2 | 0.0000298 | 4.09e-11 | 4.746 | 26 | 356 | **0.153** | 0.277 | 0.514 | + +Round-2 gain over round 1 (−0.008) was much smaller than round-1's gain over +the default (−0.096) — a diminishing-returns signal consistent with a local +optimum for this model size and dataset, not a converged global result. +`gradient_clip_norm` landed at opposite bound extremes across rounds +(0.1 then 4.7), so no directional recommendation on that parameter should be +drawn from this exploration alone. + +**Explicit scope limits — do not generalize beyond these:** +- `tiny_ci` only. Nothing here has been run against `large_linux`; its far + larger parameter count and different compute profile mean these + hyperparameters are not a starting point for it without their own search. +- Synthetic dataset only (24 windows, one generator/seed family). Not + validated against any real corpus at this scale. +- Self-signed, evaluation-only model activation (a throwaway local Ed25519 + key, not a release signature) was used to run inference for scoring. +- No security/provenance/maintainer-approval gate has passed — the Darwin + Mode promotion rule correctly refused to promote any candidate here. + +Reproducer: `harness/ruview/flywheel/ruforecast/` (genome, gate, evaluator, +dry-run/`--confirm` driver) in the `ruvnet/RuView` repo, paired with +`ruvnet/metaharness` PR #260 (`evolve-numeric`) linked locally via +`npm link`. Neither the genome defaults here nor any repo default config +were changed by this note — it is a record of exploratory evidence, not a +committed recommendation. + +### Amendment (2026-09-01, later same day): the round-2 result above did not generalize -- retracted + +**The "Search round 2" row above (WQL 0.153, learning_rate=0.0000298 etc.) is +RETRACTED as a claim of improvement.** It is kept in the table (append-only, +never silently edit a prior measurement) but must be read together with this +amendment: independent verification, run the same day using a new +regression-candidate promotion path (`ruvnet/autogenous` PR-in-progress, +branch `feat/regression-candidate-kind`, not yet merged/pushed -- +`v2/crates/ruforecast-autogenous-bridge` in this worktree) against TWO FRESH +synthetic corpora that were never part of that search (seeds 1000/1097, vs. +the search's single fixed seed 0), showed that "winner" genome performing +**WORSE than the baseline on both**: + +| Judge corpus | Candidate WQL | Baseline WQL | Candidate beats baseline by | +|---|---:|---:|---:| +| seed 1000 | 0.155 | **0.099** | -0.056 (worse) | +| seed 1097 | 0.219 | **0.109** | -0.110 (worse) | + +**Root cause**: every evaluation in the three search rounds above (default, +round 1, round 2) trained and scored every candidate against the exact same +fixed synthetic corpus (`prepare-synthetic-dataset`'s implicit `--seed 0` +default). The search had learned to exploit that one corpus's specific +random windows -- textbook overfitting -- not found a genuinely better +hyperparameter configuration. This is a real, measured failure mode, not a +hypothetical caveat. + +**Fix applied**: `harness/ruview/flywheel/ruforecast/gate.mjs`'s +`evaluateGenome` now trains and scores every candidate against THREE +independent synthetic corpora (`DEFAULT_SEARCH_SEEDS = [11, 23, 47]`, none +of which overlap the retracted search's seed 0 or the verification seeds +1000/1097) and takes the WORST CASE `primary` across them, not an average -- +a candidate only counts as a win if it beats both baselines on every corpus. +See `harness/ruview/flywheel/ruforecast/README.md`'s "Multi-seed fitness" +section for the full account. + +**Re-running the search with the fix, still no verified improvement.** A +fresh search under the corrected multi-seed fitness (2 generations x 2 +children, cleared `.metaharness-numeric` archive so no stale pre-fix state +leaked in) found a genuine winner ON ITS OWN THREE SEARCH SEEDS -- +`learning_rate=0.008012, weight_decay=0.000159, gradient_clip_norm=0.267, +batch_size=24, epochs=20`, beating the baseline on all three (primary +0.099 / 0.556 / 0.576, worst-case 0.099). Independent verification against +the same fresh seeds 1000/1097 (never part of this candidate's own search) +again showed it losing to the baseline on both: + +| Judge corpus | Candidate WQL | Baseline WQL | Candidate beats baseline by | +|---|---:|---:|---:| +| seed 1000 | 0.178 | **0.099** | -0.079 (worse) | +| seed 1097 | 0.314 | **0.109** | -0.205 (worse) | + +**Honest conclusion**: two independent search rounds (pre- and post- the +multi-seed fitness fix) both produced a genome that looked like a real +improvement on its own search data and both failed independent out-of-sample +verification. This converges on a different, deeper explanation than "the +search methodology was broken" (that part IS fixed): at this dataset scale +(24 synthetic training windows), held-out WQL varies enormously by which +corpus is drawn REGARDLESS of hyperparameters -- the baseline genome itself, +evaluated with the corrected multi-seed fitness function, swings from +primary 0.83 (a strong win) to a full regression (primary 0, WQL worse than +last-value) purely from which of the three fixed search seeds was used, with +identical hyperparameters throughout. The corpus-noise floor at n=24 +windows appears to dominate any real hyperparameter effect. No RuForecast +hyperparameter configuration has been shown, by this exploration, to +reliably beat the trivial baselines out-of-sample at this scale. A larger +training corpus (more windows, ideally real governed data under the +`large_linux` profile) is the more promising next lever than further +hyperparameter search on this fixture. + +A related implementation-only fix: the promotion verifier +(`envelope::regression::verify_regression_promotion` in the Autogenous +branch above) initially required all judges' receipts to share one +`corpus_id`, inherited unreviewed from a same-evidence review model that +does not fit this kind's intentionally cross-corpus judge design. This was +corrected (`ReceiptCorpusMismatch` is no longer produced by that function); +it did not change either REJECT verdict above, both of which were already +correctly driven by the real `NotBetterThanParent` signal. + +Reproducer for both verification runs above: same as below, plus +`v2/crates/ruforecast-autogenous-bridge` (`cargo +1.92.0 run --manifest-path +crates/ruforecast-autogenous-bridge/Cargo.toml -- --ruforecast-bin +./target/debug/ruforecast --candidate-genome