Contracts

battery-partition.md

Contract: architectural battery partition

Requirements: FR-001, FR-003, FR-004, FR-005, FR-013, C-010 · Design: research.md D-01…D-06, R1 §1 and §4

Single sources

  • Registry (.github/ci-module-registry.yml → special_tiers.architectural): base selection (paths, marker expression, whole-file deselects owned by other always-on lanes), worker count (4), fast-gate roster (path, budget seconds, reason), shard count (2), timings key. Nothing else declares these values; workflows carry literal arguments that pin tests assert equal to the registry. The nightly backstop's location is deliberately not a registry fact (D-26): it is pinned only by WP04's tests (tests/ci/test_nightly_architectural_backstop.py, tests/ci/test_nightly_exit_code_honesty.py), which keeps the nightly work package off the registry lane.
  • Timings (.github/ci-shard-timings.json, battery per-file durations): input to the shared selector only.
  • Selector (scripts/ci/shard_select.py): the only LPT implementation, used in test granularity by module rows and in file granularity by the battery.

Partition semantics

Let B be the base selection: the node-ids collected from tests/architectural with the base marker expression and deselects. Let F, S1, S2 be the node-ids executed under --battery-part fast, 1/2, 2/2.

1. Completeness: F ∪ S1 ∪ S2 = B. 2. Disjointness: F, S1, S2 are pairwise disjoint. 3. File granularity: every test-module file of B belongs to exactly one part; the plugin never ignores directories, conftest.py or _*.py helpers, so an enumeration gap appears as an overlap (caught), never as a silent loss. 4. Balance: S1/S2 are assigned by the selector from per-file timings; a file without timing gets the median weight. A timing-set mismatch emits a ::warning:: and a step-summary line and never silently falls back to uniform weights for the battery. 5. Nightly: the backstop executes B with no partition flag, so its correctness does not depend on the partition code.

Proof obligations

  • A static test evaluates the three literal --battery-part commands through _gate_coverage (partition field) and asserts (1)–(3).
  • Positive control: a synthetic unassigned file (or a roster entry removed from the fast part without being added to a shard) makes the proof fail.
  • The fast roster's Σ measured seconds ≤ registry max_total_measured_seconds and each entry ≤ its budget, checked statically against committed timings.
  • Workflow-shape test: battery legs, fast job, backstop and Packs corpus pass the literal -n 4; a guard fails on -n auto for these jobs.

green-match.md

Contract: skip-if-green on ready-for-review (scripts/ci/green_match.py)

Requirements: FR-011, SC-005 · Design: research.md D-15, D-16, R3 §1 · Decision Moments: 01M3TZHMR2CVAY2FNHSGNJJYSR, 01M3V02AV3002PNZYY8HFJ6DQF, 01M3V1F7JYH2YNZCM4TAHB3Q3P

Tested key

(workflow file, PR number, head SHA, base SHA) where base SHA is the first parent of the merge commit the run actually tested (bound via the commits API; the second parent must equal the PR head). The Actions API's pull_requests[].base.sha is NOT used: it reports the PR's current base, not the tested one.

Every executing pull_request run uploads a zero-content artifact ci-tested-key-pr<N>-base-<sha>. A run that skipped uploads ci-green-match-run-<id>-attempt-<n> instead and is never itself a match candidate.

Decision (decide, pure)

ConditionResult
event ≠ pull_request (push, dispatch, workflow_call, schedule)Run
merge ref cannot be bound to a tested keyRun
action ≠ ready_for_reviewRun (records tested key)
run attempt ≠ 1 (re-run)Run — re-running a skip run forces execution
a completed, successful, same-workflow pull_request run for the head SHA carries a non-expired ci-tested-key-pr<N>-base-<base> artifactSkip, naming the matched run (::notice:: + step summary)
otherwise (no candidate, prior failed/cancelled/in progress, moved base)Run
any lookup error (HTTP, network, JSON, missing key)Run, with ::warning::
A4: a skip marker from an EARLIER attempt of the same run (re-run all jobs)ignored — markers bind to their attempt (created_at >= run_started_at); the re-run executes and Aggregate does not re-point

Outputs: skip, reason, marker, matched-run-id, matched-run-attempt, matched-run-url.

Transport: gh api (testable through the fake-gh harness); bind_tested_base(parents, head) lives here and is reused by aggregate_source.py.

Workflow integration

  • Router changes, Packs changes and CI Modules generate-matrix each run the helper first (permission actions: read); on skip they skip their path-filter step so all path-gated jobs skip and the required gate (router gate, CI Modules gate) reports success. No job if: or needs: changes.
  • CI Aggregate collect gains a first effective-source step: if the source run carries a green-match marker, it re-points every source-run reference to the matched CI Modules run after re-verifying head SHA, workflow, event, success and tested identity from the matched run's immutable merge ref; on any mismatch collect fails ("re-run CI Modules to execute") — never a silent empty-selection green. Step names and env var names are preserved; run-name stays bound to the skip run so Fleet Verdict finds it.
  • Sonar's informational per-change upload may repeat on a skip run (accepted).

Verification

  • Parametrised unit tests over recorded API fixtures cover every row above (15 negative cases) and the Aggregate effective-source path.
  • Live verification of the Aggregate half is a post-merge follow-up (it runs from main's workflow file via workflow_run).
  • Amendment to ADR 2026-09-23-1 records that a required gate may pass on matched prior-run evidence under exactly this key.

router-two-authority-amendment.md

Contract amendment: router two-authority — ci_config group and the partitioned battery

Amends: kitty-specs/ci-pipeline-reinstatement-01M1X35E/contracts/router-two-authority.md (that file is byte-frozen by tests/architectural/test_archive_root_byte_identical.py; this mission records the amendment here instead of editing it). Mission: ci-runtime-stabilisation-01M3TZH6 (issue #5510) · Decision Moments: 01M3TZHTVXWTFYPW8TD73F3FZJ, 01M3V02DWR56AAPTF06J46YXH0, 01M3V1FAQV07WJF3RYAFN9J7GC

Unchanged invariants

1. The two CI path routing authorities remain the router filter block and the job if: gates in .github/workflows/ci-router.yml, kept in lockstep. 2. scripts/ci/gate_selection.py remains a derived parser of those authorities; it is never given a parallel encoding of a group. 3. A src/** path matching no named group still sets unmatched=true and fans out to run-all (#3463 class). 4. The path-scoped battery stays a non-required check (ADR 2026-09-23-1).

Amendments

A1. New non-src group ci_config. Paths: .github/workflows/, .github/actions/, scripts/ci/, pytest.ini, pyproject.toml, Makefile, .github/ci-module-registry.yml, .github/ci-shard-timings.json. It gates only** the architectural battery. It reverses, for the battery alone, the "CI-config changes gate no router job" rulings of #4386 and #5302; the existing ci group, its "gates no router job" pin and its registry scrub mirror are untouched. _ci_integrity_oracle.HEAVY_BATTERY_NON_SRC_GROUPS becomes {"architectural", "ci_config"}.

A2. Battery shape. The battery is an always-on architectural-fast job (registry-held roster) plus architectural-heavy as a single job key with a 2-leg include: matrix (--battery-part 1/2, 2/2). The heavy job's if: remains the code-scoped expression (src-backed groups ∪ HEAVY_BATTERY_NON_SRC_GROUPS) ∧ ¬prose_only. architectural-fast joins MUST_RUN_ALWAYS_ON_GATES and router-gate needs.

A3. Removed per-group router shards. tests (cli), tests (status), tests (consolidation) and tests (corpus) are deleted; their module rows (and the Packs corpus lane) are the sole homes. Router-gate needs shrinks accordingly.

A4. Ready-for-review short-circuit. On a pull_request ready_for_review event, the changes selection job may skip its path-filter step when scripts/ci/green_match.py finds a successful run for the same tested key (see green-match.md); every path-gated job then skips and the router gate reports success. Push, dispatch, re-run attempts and other pull_request actions are never short-circuited.

Verification

  • tests/architectural/test_gate_selection_authority.py and test_ci_quality_path_filters.py pass with no gate_selection.py edit.
  • Gate selection for a CI-config-only changed-path set selects architectural-heavy; for a prose-only set it does not.
  • test_ci_module_wiring.py::test_ci_group_gates_no_router_job_and_stays_out_of_the_catch_all stays green unchanged.