Skip to content
13 changes: 8 additions & 5 deletions .github/workflows/publish_package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ name: Publish Package
# (create a Release from a vX.Y.Z tag). Auth is PyPI Trusted Publishing (OIDC) —
# no stored API token. See docs/guides/publishing-to-pypi.md.
#
# One-time PyPI setup (project Settings -> Publishing -> add a trusted publisher,
# or a *pending* publisher for the first-ever release before the project exists):
# One-time PyPI setup (already done for this project — kept for reference when
# bootstrapping a NEW package: a *pending* publisher covers the first-ever release):
# Publisher: GitHub | Owner: views-platform | Repo: views-baseline
# Workflow: publish_package.yml | Environment: (leave blank)
#
# NOTE: views-baseline depends on views-pipeline-core (>=3.0.0), which is not yet
# on PyPI — `pip install views-baseline` will not resolve until that dependency is
# published. Uploading here still succeeds and reserves the name/version.
# NOTE: views-pipeline-core (>=3.0.0) reached PyPI on 2026-08-03 and is now at 3.2.0,
# so `pip install views-baseline` resolves. 1.0.0 and 1.0.1 were published while it did
# not, and were installable in name only. 1.0.2 is therefore the FIRST release whose
# clean-room install-back can actually be exercised — do it (docs/guides/publishing-to-pypi.md
# §A). This note said the opposite until 2026-09-09; it was the third copy of that claim
# and the one the doc sweep missed.

on:
release:
Expand Down
43 changes: 35 additions & 8 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,22 @@ name: Lint and Test

# uv-based CI (mirrors the views-frames / views-datafactory / views-reporting pattern).
#
# NOTE: `uv sync` cannot currently resolve the dependency graph because
# views-pipeline-core (>=3.0.0) is not on PyPI yet, so this workflow is expected to
# be red until that dependency is published. The editable `views_pipeline` conda env
# is the working test gate meanwhile (README / CONTRIBUTING).
# This workflow RUNS and passes again as of 2026-09-09 (issue #90). It is NOT yet a
# blocking gate: neither `main` nor `development` declares required status checks
# (verified via the branch-protection API), so a red run here still does not stop a
# merge. Turning these into required checks is a repository-settings change for the
# maintainer — worth doing now that the job is reliably green, and tracked in C-49.
# If you enable it, use the CURRENT job name `check (3.11)`; the pre-2026-09-09 runs
# report `check (3.12)`/`check (3.13)`, which no longer exist and would block every PR.
#
# History, so the next person does not re-diagnose it: it was red from 2026-08-02 and the
# note here blamed views-pipeline-core not being on PyPI. pipeline-core 3.0.0 published on
# 2026-08-03 and the workflow was never re-run, so the note went stale and the real cause
# went unseen for five weeks — which is also how the #84 outage survived, since nothing was
# gating merges. The actual blocker was different and would have outlived the publish:
# uv resolves a universal lockfile including win32, and
# pipeline-core -> viewser -> docker<6 -> pywin32==227 has no wheels above cp39. Fixed by
# `[tool.uv] environments = ["sys_platform == 'linux'"]` in pyproject.toml.

on:
push:
Expand All @@ -19,9 +31,8 @@ on:
workflow_dispatch:

jobs:
# Lint runs project-independently (uvx), so it stays a reliable green signal even
# while `uv sync` cannot resolve views-pipeline-core (see the header note) — the
# `check` job below is gated behind that resolution and is expected red until then.
# Lint runs project-independently (uvx), so it does not depend on the project
# resolving. Kept separate for that reason, though `check` now resolves too.
lint:
runs-on: ubuntu-latest
steps:
Expand All @@ -34,7 +45,23 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13"]
# 3.11 only, and not by preference (issue #95).
#
# views-pipeline-core -> viewser 6.6.4 -> `pandas<2.0.0,>=1.4.0`, and pandas 1.5.3
# (the last 1.x) publishes wheels for cp38-cp311 only. On 3.12/3.13 the resolver
# must build pandas from source, which fails outright:
# ModuleNotFoundError: No module named 'pkg_resources'
# So views-baseline is not installable on 3.12/3.13 at all — a transitive
# constraint two hops upstream, not something this repo can fix, and true long
# before this matrix was written.
#
# `pyproject.toml` still declares `requires-python = ">=3.11,<3.14"` and 3.12/3.13
# classifiers. That claim is currently false; it is tracked in the register (C-47)
# rather than quietly narrowed here, because what the package claims to support is
# a distribution decision, not a CI one.
#
# Restore the full matrix when viewser drops the `pandas<2` pin.
python-version: ["3.11"]
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ Key characteristics:
Input DataFrame:

* Must be indexed by `(time, entity)` as a MultiIndex
* Must contain target columns specified in `config['targets']`
* Must contain target columns specified in `config['regression_targets']` (the `targets` key was retired upstream in views-pipeline-core #380 and is now *rejected* — do not add it back)

Example index:

Expand Down
22 changes: 13 additions & 9 deletions docs/ADRs/009_boundary_contracts_and_configuration_validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,16 @@ Each module boundary is documented with an explicit contract. Where validation e
`MODEL_GENOMES` encodes the required keys per model:

```python
MODEL_GENOMES = {
"ZeroModel": [],
"LocfModel": [],
"AverageModel": ["window_months"],
"ConflictologyModel": ["window_months", "n_samples"],
"MixtureBaseline": ["window_months", "lambda_mix", "n_samples"],
CORE_GENOME = ["steps", "time_steps", "prediction_format", "regression_targets", "level"]

MODEL_GENOMES = { # alias of ReproducibilityGate.Config.ALGORITHM_GENOMES
"ZeroModel": [],
"LocfModel": [],
"AverageModel": ["window_months"],
"ConflictologyModel": ["window_months", "n_samples", "seed"],
"MixtureBaseline": ["window_months", "lambda_mix", "n_samples", "seed"],
"ParametricConflictology": ["window_months", "n_samples", "seed", "family", "transform"],
"ParametricHurdleConflictology": ["window_months", "n_samples", "seed", "family", "transform"],
}
```

Expand All @@ -50,7 +54,7 @@ All model-specific parameters are required and must be explicitly declared in th
**What is not validated:**

- Value ranges: `window_months=0`, `window_months=-1`, `lambda_mix=2.0`, `n_samples=0` all pass validation.
- `targets` is required by all models but is not listed in `MODEL_GENOMES` (it is assumed to always be present). If `targets` is missing from config, construction of the model object will raise a `KeyError` from within the factory method, not from the validation check.
- ~~`targets` is required by all models but is not listed in `MODEL_GENOMES`~~ — **closed 2026-09-09 (issue #85).** This bullet is retained rather than deleted, because what it recorded is worth remembering: the gap was written down as accepted debt in March 2026, and it is exactly what the August outage was made of. views-pipeline-core retired the synthesised `targets` key on 2026-08-02 (#380); the catalog kept reading it; every baseline model raised the predicted `KeyError` from inside the factory method for five weeks, with the audit reporting success one line earlier. The catalog now reads `regression_targets`, and both it and `level` are declared in `CORE_GENOME`, so absence is a `MissingHyperparameterError` naming the key at the boundary. **Accepted debt in a boundary contract is a prediction, not an observation.**
- `partition_dict` is passed directly to model constructors without any structural check. The expected structure is `{"test": (start, end)}` where `start` and `end` are integer month IDs, but this is not enforced.

---
Expand All @@ -63,7 +67,7 @@ All model-specific parameters are required and must be explicitly declared in th

Each factory method maps config keys to constructor parameters. The mapping is explicit and readable:

- `config["targets"]` → `targets=`
- `config["regression_targets"]` → `targets=` (derived **once** in `__init__` as `self.targets`, not re-read at each of the seven factory sites, so a future rename of the platform's target vocabulary is one edit rather than seven)
- `config["window_months"]` → `window_months=` (for `AverageModel`, `ConflictologyModel`, and `MixtureBaseline`)
- `config["lambda_mix"]` → `lambda_mix=` (for `MixtureBaseline`)
- `config["n_samples"]` → `n_samples=` (for `ConflictologyModel` and `MixtureBaseline`)
Expand Down Expand Up @@ -170,7 +174,7 @@ from views_pipeline_core.data.prediction_frame import PredictionFrame

| Boundary | What is missing |
|---|---|
| Config → Catalog | No validation that `targets` is present; no value-range validation for numeric params |
| Config → Catalog | No value-range validation for numeric params. (Target/level presence: **closed** — `regression_targets` and `level` are in `CORE_GENOME` since #85.) |
| Catalog → Model | No constructor-level range checks; no type coercion |
| Model → Pipeline | No validation of distributional output structure or shape |
| Data → Model | No MultiIndex structure validation; no column presence check; no time-range sanity check |
Expand Down
2 changes: 1 addition & 1 deletion docs/ADRs/012_model_addition_protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ This ADR serves as the authoritative checklist for adding a new model.
**File:** `views_baseline/model/catalog.py`

1. Add an import for the new class at the top of the file.
2. Add an entry to `MODEL_GENOMES` listing required config keys (beyond `targets`). For a **stochastic** model this MUST include `seed` — it is a required, audited genome key with no magic default (ADR-021). For a **parametric-family** model (ADR-022) it MUST also include `family` and `transform`; the constructor is responsible for failing loud on an unsupported family or an illegal `family×transform` combination.
2. Add an entry to `MODEL_GENOMES` listing required config keys (beyond the `CORE_GENOME` keys, which now include `regression_targets` and `level`). For a **stochastic** model this MUST include `seed` — it is a required, audited genome key with no magic default (ADR-021). For a **parametric-family** model (ADR-022) it MUST also include `family` and `transform`; the constructor is responsible for failing loud on an unsupported family or an illegal `family×transform` combination.
3. Add a factory method `_get_<model_name>()` that reads config and constructs the model.
4. Add the model name → factory method mapping in `self.models` inside `__init__`.

Expand Down
26 changes: 20 additions & 6 deletions docs/CICs/BaselineForecastingModelManager.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ All models now return `dict[str, PredictionFrame]` from `predict()`. The manager
| Source | What is consumed | Notes |
|---|---|---|
| `model_path` (constructor) | `ModelPathManager` instance | Provides `.artifacts` path. `.data_raw` is no longer accessed directly (data path comes from base class `_cached_data_path`). |
| `self.config` | `dict` | Set via property inherited from base class. Must contain `"run_type"`, `"level"`, `"algorithm"`, `"targets"`. |
| `self.config` | `dict` | Set via property inherited from base class. Must contain `"run_type"`, `"algorithm"`, and every `CORE_GENOME` key — `"steps"`, `"time_steps"`, `"prediction_format"`, `"regression_targets"`, `"level"` — all audited by `audit_manifest` before the catalog is built. (`targets` was retired upstream in pipeline-core #380; see #85. `run_type` is read one line after the audit but is NOT audited — it is injected at runtime, not declared in a config file; see C-48.) |
| `_data_loader.partition_dict` | `dict` | Must contain `"test"` key. |
| `_config_manager` | `ConfigurationManager` | Used by base class and accessed directly in `_evaluate_model_artifact` and `_forecast_model_artifact` via `add_config()` to persist the artifact timestamp. |

Expand Down Expand Up @@ -156,7 +156,11 @@ manager.config = {
"run_type": "calibration",
"level": "pg_id",
"algorithm": "LocfModel",
"targets": ["y1", "y2"],
"regression_targets": ["y1", "y2"],
# CORE_GENOME keys — audit_manifest raises before the catalog is reached without them
"steps": [*range(1, 37)],
"time_steps": 36,
"prediction_format": "prediction_frame",
}
# Training
model = manager._train_model_artifact() # fits, pickles, returns model
Expand Down Expand Up @@ -196,15 +200,25 @@ preds = mgr._evaluate_model_artifact(eval_type="temporal")

```python
# Using an algorithm name not registered in the catalog
manager.config = {"run_type": "eval", "level": "pg_id", "algorithm": "SVR", "targets": ["y1"]}
manager.config = {
"run_type": "eval", "level": "pg_id", "algorithm": "SVR",
"regression_targets": ["y1"], "steps": [1], "time_steps": 36,
"prediction_format": "prediction_frame",
}
manager._evaluate_model_artifact(eval_type="temporal")
# ValueError: "Model 'SVR' is not in the catalog. Available: ..."
# MissingHyperparameterError: "Unknown algorithm 'SVR'. Available: [...]"
# (the GATE rejects it first — the catalog's own ValueError is unreachable in a
# pipeline run, though still raised on the direct-API path)

# Missing required config key
manager.config = {"run_type": "eval", "level": "pg_id", "algorithm": "AverageModel", "targets": ["y1"]}
manager.config = {
"run_type": "eval", "level": "pg_id", "algorithm": "AverageModel",
"regression_targets": ["y1"], "steps": [1], "time_steps": 36,
"prediction_format": "prediction_frame",
}
# Missing "window_months"
manager._evaluate_model_artifact(eval_type="temporal")
# ValueError: "Model 'AverageModel' requires config keys ['window_months'] but they are missing"
# MissingHyperparameterError: "Algorithm 'AverageModel' requires missing parameters: ['window_months']"

# Indexing _forecast_model_artifact by position (it returns dict[str, PredictionFrame], keyed by target)
forecast = manager._forecast_model_artifact()
Expand Down
Loading
Loading