Contracts
ci-yaml-shape.md
Contract: CI YAML shape (golden-YAML pins)
These are the exact resolved shapes the golden-YAML pytest assertions must enforce, using the canonical pattern from tests/architectural/test_dual_mode_contract.py (yaml.safe_load(path.read_text()); note PyYAML parses bare on: as boolean True, so read doc[True] for the trigger block).
C-YAML-1 — ci-router.yml top-level concurrency (Lever 1a)
# round-trip: skip: illustrative CI-router concurrency shape, not a Pydantic contract example
concurrency:
group: ci-router-${{ github.event_name == 'push' && github.sha || github.ref }}
cancel-in-progress: ${{ github.event_name != 'push' }}
Assertions (NEW pin, tests/architectural/test_dual_mode_contract.py) — EXACT equality, not substring (a substring pin like "github.sha" in group passes for a broken always-cancel expression such as ${{ github.event_name != 'push' || true }}, silently re-enabling the main cancel-cascade — the exact hole FR-001 closes; Renata HIGH / Debbie LOW):
workflow["concurrency"]["group"] == "ci-router-${{ github.event_name == 'push' && github.sha || github.ref }}".workflow["concurrency"]["cancel-in-progress"] == "${{ github.event_name != 'push' }}".- The
on:block still haspush.branches == ['main'], thepull_requesttrigger, andworkflow_dispatch(unchanged).
C-YAML-2 — ci-fleet-verdict.yml trigger types (Lever 2a.1)
# round-trip: skip: illustrative workflow_run trigger shape, not a Pydantic contract example
on:
workflow_run:
workflows: [ ...8 unchanged... ]
types: [completed]
Assertions (UPDATE tests/ci/test_fleet_verdict.py around :125):
set(trigger["types"]) == {"completed"}← was{"requested","in_progress","completed"}.set(trigger["workflows"])unchanged (still the full registered set).
C-YAML-3 — ci-fleet-verdict.yml top-level concurrency (Lever 2a.2)
> Fix round 1 (pre-merge squad MAJOR @0568119, 2026-09-15). The original per-head_sha key conflated verdict subjects on the CI Aggregate trigger path: a workflow_run child always executes on the default branch, so an aggregate completion's head_sha is the current main tip, not the PR head it verified — every PR's aggregate completion landed in the current tip's group and cancel-cascaded other subjects' runs (both the PR-stranding and the lost-main-evaluation channels NFR-002 forbids; cancel-in-progress: false does not close the pending channel either, because a newly queued run replaces a pending run in a group by default). The key is therefore subject-safe per path: the direct producers keep head_sha (their head IS the subject); the aggregate path keys on the aggregate run's own id, which its display_title binds to exactly one source CI Modules run, hence one PR head — a per-subject singleton group. Recorded in research.md D9.
# round-trip: skip: illustrative fleet-verdict top-level concurrency shape, not a Pydantic contract example
concurrency:
group: ci-fleet-verdict-${{ github.event.workflow_run.path == '.github/workflows/ci-aggregate.yml' && format('aggregate-{0}', github.event.workflow_run.id) || github.event.workflow_run.head_sha }}
cancel-in-progress: true
Assertions (pin in tests/ci/test_fleet_verdict.py) — EXACT equality (a substring head_sha in group passes for a broken expression that drops the workflow_run. path and collapses ALL tips into one shared cancel:true group = the cross-tip-cancel NFR-002 forbids; Renata/Debbie):
- top-level
workflow["concurrency"] == {"group": "ci-fleet-verdict-${{ github.event.workflow_run.path == '.github/workflows/ci-aggregate.yml' && format('aggregate-{0}', github.event.workflow_run.id) || github.event.workflow_run.head_sha }}", "cancel-in-progress": True}.
C-YAML-4 — report-main job concurrency — UNCHANGED (negative pin)
> Reversed post-plan squad (Alphonso MAJOR, operator-ratified 2026-09-15). Moving report-main to per-SHA coalesce opens a create-create race on the single from:ci incident issue. report-main is therefore left unchanged; the fan-out cap is delivered entirely by C-YAML-2 (types trim) + C-YAML-3 (top-level coalesce), which cut report-main inflow to ≈1/tip so the existing single-group queue drains trivially.
# round-trip: skip: illustrative report-main (unchanged) concurrency shape, not a Pydantic contract example
jobs:
report-main:
concurrency:
group: ci-fleet-verdict-main
cancel-in-progress: false
Assertion (existing tests/ci/test_fleet_main.py:166 stays green as-is — do NOT change it):
job["concurrency"] == {"group": "ci-fleet-verdict-main", "cancel-in-progress": False}.- This preserves cross-tip incident-issue serialization (edge case: two concurrent red tips cannot both create an incident).
C-YAML-5 — report (PR) job concurrency (unchanged — negative pin)
# round-trip: skip: illustrative report(PR) (unchanged) concurrency shape, not a Pydantic contract example
jobs:
report:
concurrency:
group: ci-fleet-verdict-pr-${{ matrix.pr }}
cancel-in-progress: false
Assertion (existing test_fleet_verdict.py:126-127 stays green): matrix.pr in group; cancel-in-progress is False. Must not change (FR-002 / invariant CK-2).
C-YAML-6 — report (PR) job comment refresh (Alphonso/Debbie INFO)
The report (PR) job's cancel-in-progress: false + its comment ("Global concurrency could lose another PR", ci-fleet-verdict.yml:40-41) become semantically stale once the top-level cancel:true block owns same-subject coalescing (different PR heads never share the top-level group — per-head_sha on the direct path, per-aggregate-run-id on the aggregate path, per fix round 1; the double-snapshot head-drift guard covers re-pushes). Implement task: update the comment at :40-44 so a future reader knows the top-level key now carries that guarantee. No behavior change; not a golden-YAML pin.
C-WIRE-1 — script-invocation wiring pins (mirror test_router_gate_step_wiring...)
New/kept assertions that the workflow steps still invoke the verdict scripts (so the fan-out cap cannot silently orphan them):
identifystepruntext containsscripts/ci/fleet_verdict.py(or-m scripts.ci.fleet_verdict) andattempts/where a jobs-API call appears.reportstep invokesfleet_verdict.py --pr.report-mainstep invokespython3 -m scripts.ci.fleet_main.
verdict-invariants.md
Contract: Verdict invariants (dedup survivor — behavior pins)
Stage 1 changes no classify/report behavior; these are the invariants the new dedup unit test must pin, because the fan-out cap (coalesce-with-survivor) now depends on them.
VI-1 — Survivor re-reads live evidence (double-snapshot)
fleet_verdict.report() snapshots current evidence at entry (:317) and again immediately before publishing (:332-334); if the head SHA or evidence fingerprint drifted between the two reads it raises "changed before publication; later event will reconcile" and posts nothing.
- Test: given a survivor whose second snapshot differs from the first,
report()raises (does not post a stale verdict). - Rationale: under
cancel-in-progress: true, only the last completion survives; it must recompute from live state. Pinned so a future edit cannot turn coalescing into a stale-post. - Non-tautology guard (Renata, implement-review): the drift between the two
snapshot()calls MUST be driven through the in-memoryAPI()stub's mutating state soreport()actually callssnapshottwice and compares — NOT by patchingsnapshotitself (which would assert the mock, not the behavior). Reviewer verifies the test exercises the real double-read.
VI-2 — Newer verdict is never suppressed; only exact-duplicate running is
The running-suppression (:325-331, guard at :329) returns without posting only when the latest ledger comment is a bot comment carrying the same fingerprint, OR (state=="running" AND the latest body starts with [ci] running @<sha>). A newer terminal verdict (green/red) for the same head is not suppressed.
- Test:
test_duplicate_latest_evidence_is_suppressed_but_newer_verdict_is_not(existing,:209-219) stays green; ADD an explicit case: arunningpost followed by agreen/redpost for the same head does publish. - Rationale: NFR-002 — the last-writer terminal verdict a landed tip needs is never dropped by dedup.
VI-3 — Never-green preserved
classify never turns absent/cancelled/skipped/incomplete evidence into green (:87).
- Test:
test_truncated_files_and_deferred_pr_never_green(existing) stays green. Stage 1 adds no path that could green-wash.
VI-4 — Main path shares the same guarantee
fleet_main.report() re-reads main head + attempts at :124 (raises on drift) and dedups a single open incident issue.
- Test:
test_fleet_main.pymain-intake tests stay green; the per-SHAreport-maincoalesce does not change the incident-issue idempotence.
VI-5 — Aggregate fail-closed guard untouched (NFR-001)
Stage 1 touches no source-eligibility or reconciliation logic in ci-aggregate.yml / scripts/ci/reconcile_shards.py.
- Test:
tests/ci/test_reconcile_shards.pyand the aggregate guard tests stay green; the Stage-1 diff contains noci-aggregate.yml/reconcile_shards.py/aggregate_source.pychanges.
VI-6 — Coalescing depends on types:[completed] staying present (Renata LOW)
The survivor's "later event will reconcile" behavior (VI-1) relies on upstream completed workflow_run events remaining the trigger. The Stage-1 trim to types:[completed] preserves the reconciling event (completions are exactly what remain), so the assumption holds. But a future narrowing of types: (or removing a completing upstream workflow) could silently turn coalescing into a suppression path — the same class as the Stage-3 compat edge case.
- Pin: not unit-testable in pytest (C-002); documented here + in the top-level-concurrency golden-YAML pin's neighborhood so a future
types:edit is reviewed against this dependency. A wedged terminal survivor is caught on the merged tip by SC-006 (terminal-verdict presence).