Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 35 additions & 30 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ name: Linting
# - ruff format + ruff check are HARD gates (block the PR).
# - mypy is a HARD gate: the package type-checks cleanly under strict mypy.
# (The earlier ~28-error burn-down is complete.)
# - The SDK filter/shape conformance check needs the canonical manifest from the
# private makegov/tango repo, which requires a TANGO_API_REPO_ACCESS_TOKEN
# secret the public CI does not have. The conformance job SKIPS cleanly when
# the token is absent (rather than failing red) and becomes a real gate the
# moment the secret is configured.
# - The SDK filter/shape conformance check is a HARD gate: it runs against the
# vendored contract at contracts/filter_shape_contract.json on every run (no
# secrets needed, works on forks). A second, token-gated step compares the
# vendored contract against the tango repo's HEAD and emits a staleness
# notice (never a failure — tango HEAD may carry unreleased changes).
# Refresh the vendored contract with scripts/refresh_contract.py.
on:
workflow_dispatch:
push:
Expand Down Expand Up @@ -45,13 +46,29 @@ jobs:
run: uv run mypy tango/

conformance:
# Requires the canonical filter_shape manifest from the private makegov/tango
# repo. When TANGO_API_REPO_ACCESS_TOKEN is not configured, every real step
# is skipped and the job passes (rather than failing on an empty token).
# Configure the secret to turn this into a hard gate automatically.
# Hard gate against the vendored contract (contracts/filter_shape_contract.json).
# Runs unconditionally — no secrets required, so forks and tokenless runs
# get the full check instead of a silent skip.
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6

- name: Install uv
uses: astral-sh/setup-uv@v8.1.0
with:
version: "latest"

- name: Set up Python
run: uv python install 3.12

- name: Install dependencies
run: uv sync --all-extras

- name: Check SDK filter/shape conformance (vendored contract)
run: uv run python scripts/check_filter_shape_conformance.py

# --- Staleness notice (best-effort, never fails the job) ---------------
- name: Determine token availability
id: gate
env:
Expand All @@ -61,34 +78,22 @@ jobs:
echo "ready=true" >> "$GITHUB_OUTPUT"
else
echo "ready=false" >> "$GITHUB_OUTPUT"
echo "::notice::Skipping SDK conformance check — TANGO_API_REPO_ACCESS_TOKEN not configured."
echo "::notice::Contract staleness check skipped — TANGO_API_REPO_ACCESS_TOKEN not configured."
fi

- uses: actions/checkout@v6
if: steps.gate.outputs.ready == 'true'

- name: Checkout tango API repo (manifest source)
- name: Checkout tango API repo (contract source)
if: steps.gate.outputs.ready == 'true'
uses: actions/checkout@v6
with:
repository: makegov/tango
path: tango-api
token: ${{ secrets.TANGO_API_REPO_ACCESS_TOKEN }}

- name: Install uv
- name: Compare vendored contract against tango HEAD
if: steps.gate.outputs.ready == 'true'
uses: astral-sh/setup-uv@v8.1.0
with:
version: "latest"

- name: Set up Python
if: steps.gate.outputs.ready == 'true'
run: uv python install 3.12

- name: Install dependencies
if: steps.gate.outputs.ready == 'true'
run: uv sync --all-extras

- name: Check SDK filter/shape conformance
if: steps.gate.outputs.ready == 'true'
run: uv run python scripts/check_filter_shape_conformance.py --manifest tango-api/contracts/filter_shape_contract.json
run: |
if ! diff -q contracts/filter_shape_contract.json tango-api/contracts/filter_shape_contract.json >/dev/null; then
echo "::warning::Vendored contract differs from makegov/tango HEAD. Refresh with: uv run python scripts/refresh_contract.py"
else
echo "Vendored contract matches makegov/tango HEAD."
fi
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **Contract-first conformance system.** The canonical API filter/shape
contract is now vendored at `contracts/filter_shape_contract.json` (refresh
with the new `scripts/refresh_contract.py`), so the conformance check runs
out of the box — locally, in CI, and on forks — with no tango checkout or
access token. `scripts/check_filter_shape_conformance.py` gained three new
checks on top of filter coverage: **staleness** (an SDK filter argument the
API no longer accepts is an error — it would silently no-op), **types**
(each argument's annotation is validated against the contract's
`schema_version: 2` per-filter type metadata), and a **known-gaps baseline**
(`contracts/conformance_baseline.json`) that downgrades accepted missing
params from errors to warnings so backlog is tracked instead of silent. A
new `--suggest` flag prints ready-to-paste typed parameter scaffolds for
any missing filters. The first run against the current API surface found 7
real coverage gaps, now baselined: `key` on contracts/IDVs/OTAs/OTIDVs,
`cage` on entities, `id` on forecasts, and `opportunity_id` on
opportunities.
- `TangoValidationError` now exposes the API's structured validation details
directly: `.issues` (the list of `{"path": ..., "reason": ...}` entries the
server returns for shape errors) and `.available_fields` (the endpoint's
valid field set, when included). Both were previously reachable only by
digging through `.response_data`. ([#45](https://github.com/makegov/tango-python/issues/45))

### Changed
- The CI conformance job (`lint.yml`) now runs unconditionally against the
vendored contract instead of silently skipping when
`TANGO_API_REPO_ACCESS_TOKEN` is absent; the token is only used for a
best-effort staleness notice comparing the vendored contract to tango HEAD.
`scripts/pr_review.py` likewise defaults its conformance step to the
vendored contract (override with `TANGO_CONTRACT_MANIFEST`).
- 400 error messages now name the rejected field(s) and reason when the API
returns structured `issues` — e.g.
`Invalid request parameters: Invalid shape: tradeoff_process (unknown_field)`
Expand Down
12 changes: 12 additions & 0 deletions contracts/conformance_baseline.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"_comment": "Accepted SDK coverage gaps vs the API contract. Params listed here downgrade from error to warning in scripts/check_filter_shape_conformance.py. Each entry is tracked backlog: remove it in the same PR that adds the param to the SDK. Run the checker with --suggest for ready-to-paste typed parameter scaffolds.",
"missing_filters": {
"contracts": ["key"],
"entities": ["cage"],
"forecasts": ["id"],
"idvs": ["key"],
"opportunities": ["opportunity_id"],
"otas": ["key"],
"otidvs": ["key"]
}
}
Loading
Loading