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-partcommands 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_secondsand 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 autofor 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)
| Condition | Result |
|---|---|
event ≠ pull_request (push, dispatch, workflow_call, schedule) | Run |
| merge ref cannot be bound to a tested key | Run |
action ≠ ready_for_review | Run (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> artifact | Skip, 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, Packschangesand CI Modulesgenerate-matrixeach run the helper first (permissionactions: 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 jobif:orneeds:changes. - CI Aggregate
collectgains a firsteffective-sourcestep: 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 mismatchcollectfails ("re-run CI Modules to execute") — never a silent empty-selection green. Step names and env var names are preserved;run-namestays 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-sourcepath. - Live verification of the Aggregate half is a post-merge follow-up (it runs from
main's workflow file viaworkflow_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.pyandtest_ci_quality_path_filters.pypass with nogate_selection.pyedit.- 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_allstays green unchanged.