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:
rUv authored and GitHub committed 2026-09-01 23:35:42 -04:00
1 parent e04f269f1a
commit e5bf0d4892
22 files changed
+5644 -15

No files matched your search

+627
View File
@@ -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