Cluster Data Contract

This contract keeps Kubernetes collection, VM transformation, and the hosted
website on the same NRL round and data generation.

Ownership

Component Responsibility Must not do
Kubernetes CronJobs Collect immutable raw odds artifacts and manifests into Spaces Write DuckDB or choose a round independently
VM operator workflow Scrape fixtures/results, reconcile raw runs, build dbt models, run DQ gates, publish snapshots Mutate raw bucket objects
Hosted website Download and read the verified immutable DuckDB snapshot Write to the analytics database

Round Control

scripts/publish_round_control.py publishes schema v2 of the active-round
control object after fixture ingestion and dbt build. It includes:

  • season and round;
  • canonical fixture identifiers and home/away teams;
  • kickoff timestamps where available;
  • publication time and source commit.

Bucket-first odds collectors require NRL_ROUND_CONTROL_KEY. When --round
is omitted, they resolve it from this object. Provider events are reconciled
against its fixture list before any normalized odds rows are accepted. Raw
provider responses remain archived unchanged.

The run manifest records expected, matched, missing, and unmatched event
counts. A collection with zero controlled fixtures matched is published as a
failed run and exits non-zero.

Expected polling no-ops, such as an already-captured opening market or no
fixture currently inside the close window, use status=skipped. Genuine empty
provider responses remain zero_rows and block promotion until investigated.

Snapshot Publication

The VM publishes:

  1. an immutable versioned DuckDB object;
  2. a stable manifest pointer containing the exact object key, checksum, size,
    run ID, and generation metadata.

The website downloads the object named by the manifest. It never assumes that
the stable manifest key is also the database object key. Before atomically
installing the file, it validates status, size, SHA256 checksum, and required
tables. The installed database receives a local
nrl_bucket.duckdb.manifest.json provenance sidecar.

Operator Sequence

# 1. Fixture-aware weekly ingest and dbt build; publishes round control.
bash scripts/run_cluster_task.sh weekly-ingest --season 2026 --round 21

# 2. Kubernetes schedules can now collect without a round argument.
bash scripts/run_cluster_task.sh odds-open --season 2026
bash scripts/run_cluster_task.sh odds-refresh --season 2026
bash scripts/run_cluster_task.sh odds-close --season 2026

# 3. VM reconciles raw manifests, builds, gates, predicts, and publishes.
bash scripts/run_cluster_task.sh recommend --season 2026 --round 21

# 4. Verify the hosted generation and recent-round coverage.
python scripts/check_hosted_ui.py \
  --base-url https://scrapers-ui.doks-ci-cd.hungee.cloud \
  --season 2026 \
  --round 21

For a historical replay, pass --round explicitly to the odds task. For
normal scheduling, publish round control first and let CronJobs use it.

Health Contract

  • /healthz proves the web process is alive.
  • /readyz proves the installed snapshot opens, passes checksum validation,
    has fixture, prediction, upcoming-match, and odds-snapshot tables, and
    contains current-season data.
  • /api/ops/data-health reports build and snapshot provenance plus fixture,
    prediction, DQ-gate, and reconciliation coverage for the selected round and
    the previous two rounds.

scripts/check_hosted_ui.py fails when readiness is not healthy, no fixture
or current-round prediction rows exist, a blocking gate is active, a
reconciliation failure exists, or an expected build/snapshot identity does not
match.

Recovery

  1. Check the stable round-control object and republish it from a verified
    DuckDB if fixtures are missing or wrong.
  2. Inspect raw _run.json manifests for missing or unmatched fixture counts.
  3. Reconcile and stage on the VM; do not stage inside the collector pod.
  4. Re-run dbt and DQ gates.
  5. Publish a new immutable UI snapshot and stable manifest.
  6. Restart or redeploy the Knative revision so it installs the new generation.
  7. Run scripts/check_hosted_ui.py before considering the incident closed.