mirror of
https://github.com/ruvnet/RuView.git
synced 2026-10-21 14:12:34 -05:00
feat(forecast): add independent Rust multivariate training stack (#1766)
* feat(forecast): add independent Rust training stack Signed-off-by: Codex <codex@openai.com> * docs(forecast): record informal HPO exploration note (unaccepted) Records an exploratory Darwin Mode numeric-genome hyperparameter search against the tiny_ci synthetic accuracy protocol: WQL improved 0.257 -> 0.161 -> 0.153 over two search rounds, staying ahead of both baselines throughout. Explicitly scoped as informal/exploratory (not a frozen leakage-free report, not eligible for the evidence ledger, not validated for large_linux or any real dataset) per this doc's own evidence-tagging discipline. * feat(forecast-core): add per-horizon weighted quantile loss weighted_quantile_loss collapses the whole horizon into one aggregate number, hiding whether error grows with lead time. Add a sibling function using the identical per-cell pinball formula and domain checks, just reduced per horizon step instead of globally. A unit test reconstructs the aggregate from the per-step numerators/ denominators to prove the two functions agree exactly, not just approximately. * feat(forecast-model): add evaluation-only local activation Adds activate_for_evaluation and build_eval_input, gated behind the cpu feature. activate_for_evaluation self-signs an unsigned candidate with a fixed, publicly-known, non-secret Ed25519 key so a CLI operator can run inference against their own just-trained candidate without a real release signature -- explicitly never a production trust path, documented as such in the module doc comment. The schema-digest check in ArtifactActivationPolicy still applies in full; only signing is relaxed. build_eval_input constructs a single-window ModelInput for CPU inference from raw context-major values/mask arrays, matching the training batch builders exact time-feature/descriptor encoding. Six unit tests cover: malformed-candidate rejection, digest-mismatch rejection (proving the schema check has teeth even on this relaxed path), getting past the policy gate with a matching digest, and eval-input shape validation. * feat(forecast-train): add evaluate and prepare-synthetic-dataset CLI Turns this sessions throwaway HPO scripts into real, tested CLI surfaces, replacing two example binaries that bypassed the CLI/TOML training path entirely. prepare-synthetic-dataset generates a larger, configurable synthetic training shard (default 24 windows) plus a matching train-local.toml with the given OptimizerSpec hyperparameters baked in, and a separate held-out test.jsonl -- the same synthetic-only, local-validation-only posture as prepare-local-example, just bigger and configurable. evaluate scores a trained (unsigned) candidate against held-out windows and the LastValue/SeasonalNaive baselines: overall weighted quantile loss, a per-horizon breakdown, 80% interval coverage, and missingness, for the model and both baselines. Explicitly lists what docs/benchmarks/ruforecast.mds full accuracy protocol additionally asks for that this does not cover (abstention coverage, selective risk, site/device slices, interference regime, RuVector-retrieval ablation) rather than silently omitting them -- each needs infrastructure a single-entity synthetic fixture does not have. Together the three real commands (prepare-synthetic-dataset, train-local, evaluate) reproduce the exact WQL numbers the old library-bypassing example scripts produced for the same hyperparameters, confirmed by re-running the harness/ruview HPO dry-run end to end against the new CLI path. New integration test trains a real candidate via smoke, evaluates it against held-out synthetic windows, and asserts the full report shape and an empty-input rejection. * feat(forecast-train): add prepare-synthetic-dataset --seed + Autogenous bridge --seed (default 0, backward compatible) makes prepare-synthetic-dataset's generator deterministic per offset: same seed -> byte-identical corpus across runs (needed to train two genomes on the SAME corpus for a fair comparison); different seeds -> genuinely independent corpora (needed for honest multi-seed/multi-judge evaluation upstream, both in harness/ruview/flywheel/ruforecast/gate.mjs's fitness function and in the new v2/crates/ruforecast-autogenous-bridge crate added here). ruforecast-autogenous-bridge is a LOCAL-DEV-ONLY (excluded from the v2 workspace, publish = false) bridge from this crate's real evaluate CLI into ruvnet/autogenous's new regression-candidate promotion path (separate, unpushed branch feat/regression-candidate-kind): runs N independent train+evaluate judges on genuinely distinct synthetic corpora, signs receipts, and gets a real cryptographically-verified PROMOTE/REJECT decision -- defense in depth on top of, not a replacement for, Darwin's own promotion gate. Path-depends on a sibling autogenous checkout that does not exist in CI; build/run directly via `cargo build --manifest-path crates/ruforecast-autogenous-bridge/Cargo.toml`. cargo test -p ruview-forecast-train --no-default-features --features cpu,cli: all green (unaffected existing tests + this backward-compatible addition). * docs(forecast): retract round-2 HPO result, record honest multi-seed finding The earlier "Informal HPO exploration note" claimed a real improvement (WQL 0.257 -> 0.153) from a 3-round hyperparameter search. That search evaluated every candidate against one fixed synthetic corpus (seed 0) for every round -- textbook overfitting. Independent verification against two fresh corpora (via the new ruforecast-autogenous-bridge crate) showed the "winner" losing to the baseline on both. Fixed the root cause in harness/ruview/flywheel/ruforecast/gate.mjs: candidates are now scored against three independent corpora, worst-case across them, not one fixed corpus. Re-ran the search under the fix. It found a new winner that genuinely beat baseline on all three of its own search seeds -- and that winner ALSO lost independent verification on fresh seeds. Two independent search rounds, pre- and post-fix, both produced an illusory "winner." The honest reading: at this dataset scale (24 synthetic windows) held-out WQL varies enormously by which corpus is drawn, regardless of hyperparameters -- confirmed directly by the baseline genome's own primary swinging from 0.83 to a full regression across the three fixed search seeds with unchanged hyperparameters. No RuForecast hyperparameter configuration has been shown to reliably beat the trivial baselines out-of-sample at this scale. Also fixes a real design inconsistency surfaced by this exploration: the Autogenous regression-candidate promotion verifier (envelope::regression::verify_regression_promotion, separate unpushed ruvnet/autogenous branch feat/regression-candidate-kind) required all judges to share one corpus_id, which conflicts with this kind's intentional cross-corpus judge design. Corrected there (commit bfa4c48); did not change either REJECT verdict, which were already driven by the real NotBetterThanParent signal on their own. Append-only: the original round-2 row is kept, read together with this new amendment section, per this doc's own evidence-ledger discipline. * refactor(forecast): extract RuForecast to ruvnet/RuForecast submodule Mirrors the v2/crates/worldgraph pattern: RuForecast becomes its own independent public repo/workspace (ruforecast-core/model/train), mounted at v2/crates/ruforecast as a git submodule, with v2/Cargo.toml path-depping into its sub-crates and excluding the submodule from the v2 workspace. Full v2 --workspace check passes; the ruforecast CLI builds and runs correctly from its new location. Real git history for the extracted crates (7 commits) was preserved via git-filter-repo, not squashed. * feat(autogenous-bridge): real-data independent verification tool real_data_verify.rs: signs two real, temporally-independent judge measurements (different train/test split boundaries on the same real household vitals corpus, not synthetic seeds) through the real Autogenous regression-candidate promotion path, and gets a genuine cryptographically-checked PROMOTE/REJECT verdict. Result recorded (see docs/benchmarks/ruforecast.md): REJECT. One judge nominally beat the trivial baseline but by less than the 0.01 non-inferiority margin; the other judge lost outright. Consistent with every synthetic-data search this session -- no configuration has yet been shown to reliably beat trivial baselines out-of-sample, now including a real 6390-sample household corpus. Bumps the ruforecast submodule to pick up the evaluate real-gap fix. * docs(forecast): record real-household-data result (REJECT, signed) 6390 real vital-signs samples, two independent temporal splits, independently verified via the real Autogenous regression-candidate promotion path. Signed verdict: REJECT -- neither split cleared the non-inferiority margin. Same conclusion as every synthetic search this session, now confirmed with real data too: more real data is the credible next lever, not further search on this scale of fixture. * chore(forecast): point ruforecast submodule at published main (post gap-tolerance fix) * ci(forecast): fix ruforecast-ci.yml for the new v2/crates/ruforecast submodule layout The RuForecast crates were extracted into a standalone submodule (v2/crates/ruforecast, mirroring the existing v2/crates/worldgraph pattern) and are no longer members of the v2 workspace. The CI workflow still referenced the old in-tree package names/paths (ruview-forecast-core/model/train under v2/), which broke every forecast job with "cannot specify features for packages outside of workspace". Fixed every job to target the submodule: working-directory -> v2/crates/ruforecast, -p ruview-forecast-* -> -p ruforecast-*, all hardcoded Cargo.toml/source paths in the Python assertion and clean-room-scan blocks, the Swatinem/rust-cache workspaces input, the artifact-tree output paths, and the two levels of relative path (../ -> ../../) in the informational-benchmark steps whose working directory moved one level deeper. Also collapsed the paths: trigger filters internal-crate globs (v2/crates/ruview-forecast-*/**) down to the single v2/crates/ruforecast path, since GitHub Actions path filters only ever see a submodule gitlink change as one entry in the parent tree, never its internal file paths -- the old globs could never have matched anything. Includes a real cargo fmt fix (submodule commit d9902f9) for formatting drift left over from the earlier real-data gap-tolerance fix, which the contract job's fmt --check step would otherwise have failed on. All four forecast CI jobs' real commands verified locally before push: cargo test/check for CPU, CUDA/WGPU compile-check, the contract job's Python assertion block, the hosted-boundaries clean-room scan, and cargo fmt --check. * docs(forecast): record real BIDMC cross-entity holdout result (REJECT) Two independent real-patient partitions (34/16 and 24/26 splits, 53 total ICU patients from PhysioNet BIDMC, Open Data Commons Attribution License v1.0) both show the model losing to trivial baselines by a decisive margin (+69% and +601% worse WQL). First genuine cross-entity real-data test this project has run; same conclusion as every prior synthetic and single-household test. Full Autogenous signed verification was not run for this entry - flagged explicitly in the note. * chore(forecast): point ruforecast submodule at published main (example feature-gate fix) * chore(forecast): point ruforecast submodule at published main (clippy fix) --------- Signed-off-by: Codex <codex@openai.com>
This commit is contained in:
22 files changed
+5644
-15
No files matched your search
@@ -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<body>.*?)^\}}",
|
||||
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
|
||||
Reference in new issue
Block a user