Supported OS / Python Compatibility Matrix
Source of truth. This document explains which operating systems and Python versions the
repository supports. config/python-support.json is its machine-readable counterpart and the two
are checked together. The policy was added by
issue #333.
The rule of thumb is “supported = CI-enforced.” A combination is supported only if the project’s CI actively exercises it on a runner. Combinations that are declared but not CI-tested are called best-effort and are kept deliberately distinct so no one reads best-effort as equivalent to supported.
Runtime promise
pyproject.toml declares:
requires-python = ">=3.11,<3.14"
Python 3.11 is the root-project floor and 3.13 is the ceiling. The upper bound is intentional: it makes
the promise closed rather than open-ended, so a future 3.14 cannot silently ship as “supported”
without the CI matrix being extended first. A consequence of the <3.14 bound is that uv sync
on a 3.14 interpreter will reject the project unless you pass --no-install-project (the pattern CI
already uses), so local development on a newer interpreter stays possible while the supported
runtime remains bounded.
Root and standalone boundaries
config/python-support.json is the machine-readable boundary. The shared root project and its
uv.lock support Python 3.11 through 3.13. Packaged skills remain source-compatible with Python
3.9 when their own requirements.txt permits it. Python 3.9 clean-room jobs must not sync the root
lock or install the root project; they validate only standalone artifacts. Ruff therefore retains a
py39 syntax target across skills/ and shared standalone scripts.
Supported (CI-enforced)
Actively tested in CI:
| OS | Python | Where |
|---|---|---|
Linux (ubuntu-latest) |
3.11 | test / coverage / workflow-replay jobs, plus lint / metadata / security / supply-chain |
Linux (ubuntu-latest) |
3.9 | standalone market-calendar-compat clean room |
Linux (ubuntu-latest) |
3.13 | compat-smoke (PR) |
Windows (windows-latest) |
3.13 | compat-smoke (PR) |
Windows (windows-latest) |
3.11 | compat-nightly (daily) |
macOS (macos-latest) |
3.13 | compat-smoke (PR) |
macOS (macos-latest) |
3.11 | compat-nightly (daily) |
The macOS runner uses the macos-latest label. actions/setup-python provisions the requested
Python version independently of the base runner image, so the 3.11 leg is available on that label.
Best-effort (NOT CI-enforced)
Any combination whose Python version satisfies >=3.11,<3.14 on
ubuntu-latest / windows-latest / macos-latest, except the CI-enforced rows above.
Defined as the set-complement of the supported rows, so the two tiers never overlap. Nothing in this tier is claimed to “just work” — it is simply not ruled out at the package level.
Policy
Every job in every workflow under .github/workflows/ must satisfy the following (validated
statically by scripts/check_compat_matrix.py):
- Its OS is in
{ubuntu-latest, windows-latest, macos-latest}. - Root-project jobs use Python in
[3.11, 3.14). Explicit standalone clean-room jobs may use the separately declared 3.9 floor.
The drift guard globs the whole workflow directory rather than a hard-coded allowlist, so a
brand-new workflow that runs a job on an unsupported OS / out-of-range Python is caught and not
silently skipped. ci.yml:dependency-review (a third-party-action job) and
fmp-contract-canary.yml:preflight (a shell-only gate job) have no setup-python step, so they
are OS-checked only; single-runner jobs without a matrix axis are only OS-checked because no
Python version can be pinned. Compatibility-defining jobs (compat-smoke, compat-nightly) are also
checked against the documented axis mapping below.
Job → (os, python) mapping
The compatibility-defining jobs are declared here and must match the actual workflow YAML:
compat_matrix:
python: ">=3.11,<3.14"
supported_os: [ubuntu-latest, windows-latest, macos-latest]
jobs:
compat-smoke:
os: [ubuntu-latest, windows-latest, macos-latest]
python: ["3.13"]
compat-nightly:
os: [windows-latest, macos-latest]
python: ["3.11"]
scripts/check_compat_matrix.py parses this block and compares it to the axis sets it infers from
ci.yml / compat-nightly.yml. Workflow environments are not (and cannot be) exercised by the
static checker.
Enforcement scope
The issue title asks to define and enforce the matrix. Enforcement here is static and has two parts:
- A drift guard (
scripts/check_compat_matrix.py, run in themetadataCI job and as a pre-commit hook) that fails a change if the documented matrix diverges from the workflow YAML, or if any workflow job leaves the supported OS set / Python range. - Declaring only supported combinations in the workflow itself.
GitHub’s list-form runs-on syntax is a conjunction of self-hosted runner
labels, not an OS matrix. The checker classifies a list such as
[self-hosted, linux, x64] as self-hosted-linux; it never treats linux as
ubuntu-latest. Self-hosted categories are default-deny and must be listed in
config/python-support.json before a job may use them. Unknown, ambiguous,
dynamic, or non-self-hosted label lists fail closed. These runner categories
remain separate from the documented GitHub-hosted OS set above.
A runtime rejection of “this unsupported combination reached a runner” is out of scope for a static checker: a static tool cannot observe the live runner. This is a deliberate, documented reduction so the gap between “enforce” and the current implementation is explicit.
Why this matrix
Two past failure classes motivated bounding the promise and testing cross-platform:
- #64 — a Windows default-encoding Markdown failure that would not have surfaced if the suite had run on a Windows runner.
- #311 — a standalone dependency gap discovered by running the risk/state/navigator skills
outside the main project venv. The
compat-smoke/compat-nightlyjobs therefore run an extra isolated packaged-dependency import step: each core skill’s declaredrequirements.txtis installed into a throwaway environment (uv run --isolated --no-project --with <reqs>) and the entry modules listed inconfig/compat-import-smoke.jsonare imported independently with a bounded timeout, per OS and per Python version. Stdlib-only skills run the same isolated import without any--witharguments; they are not skipped. The explicit inventory avoids executing a newly added helper merely because it matchesscripts/*.py, while still covering each documented CLI/resource entry point. This catches a missing dependency declaration that the shared dev environment would otherwise mask via transitively installed packages. Function-local and dynamic imports remain the responsibility of the repository-widescripts/check_skill_deps.pyAST gate; the runtime smoke is intentionally scoped to configured import-safe entry modules.
The cross-platform jobs (compat-smoke, compat-nightly) exercise the compatibility-sensitive
suites — position-sizer, futures-position-sizer, trader-memory-core,
drawdown-circuit-breaker, trading-skills-navigator — plus the UTF-8 / path / line-ending /
temp-dir / subprocess-quoting regressions in scripts/tests/test_compat_regressions.py, so these
classes no longer ship silently.
Verification record
Local verification on a macOS (aarch64) host, using uv sync --locked:
- Python 3.11: resolves the locked environment successfully after the Python 3.10 fork retirement.
- Python 3.13: resolves the locked environment successfully (
scipy==1.17.1,statsmodels==0.14.6). - Result: the
>=3.11,<3.14range is viable for--extra dev --extra cion this machine.
Windows and macOS runner outcomes are validated by the PR’s GitHub Actions; they cannot be reproduced locally from a macOS host.