Contracts
artefact-naming.md
Contract — Coverage/xunit artefact naming (E2)
Load-bearing so glob aggregators + Sonar resolve every shard's output. Preserve verbatim (C-005).
Producer (each test shard)
— dotted --cov=<module> form (never path form); [tool.coverage.run] relative_files = true.
- Coverage:
out/reports/coverage/coverage-<tier>-<module>.xmlviapytest --cov=<module> --cov-report=xml:... - xunit:
out/reports/xunit-reports/xunit-result-<shard>-<run_id>.xml. - Upload:
actions/upload-artifactname: <job>-reports,path: out/reports/,if: always().
Invariants
1. Coverage basename coverage-.xml unique per shard (dedup is by basename — a collision silently drops a shard's data). 2. Artifact name ends -reports (consumers glob pattern: '-reports'). 3. Cobertura XML with ≤1 <source> (dotted --cov guarantees this; guarded by the coverage-root-collision test).
Consumers
diff-cover(PR gate): download-reports, globcoverage-.xml,--fail-under=90on changed critical-path lines.sonar.yml(nightly): download*-reports(+ previous-run fallback + cross-workflow by head-SHA), dedup by basename, comma-join tosonar.python.coverage.reportPaths. No single mergedcoverage.xml— Sonar merges server-side.
Stale-artefact fallback (spec edge case)
- A partial re-trigger leaving stale coverage → the aggregator/Sonar falls back to the most-recent successful run's
*-reportsfor shards that did not re-run. (Needs a WP home — flagged for tasks.)
governance-map-schema.md
Contract — Governance verdict-map schema (E1)
Authority: docs/convergence/interim-ci-producer.md (map) + tests/release/test_release_ci_ownership.py (enforcer). This contract adds the introduced disposition.
Dispositions (closed vocabulary → extended)
| disposition | meaning | set membership asserted by enforcer |
|---|---|---|
restore | pre-fork workflow reinstated | exact set |
defer | pre-fork workflow not yet restored | exact set |
never-restore | pre-fork workflow retired forever (dead subject) | exact set |
introduced (new) | net-new workflow, no pre-fork ancestor (router, module-*, aggregate, sonar, nightly, packs) | exact set |
Invariants (enforcer MUST assert)
1. Every .github/workflows/*.yml maps to exactly one disposition. 2. introduced + restore workflows use stock runners ("blacksmith" not in text) and carry no SK_CI_TOKEN on public jobs. 3. Any de-defer (defer→restore) or net-new (∅→introduced) edits the map in the same change (C-001). 4. The enforcer runs on every workflow-changing PR (not only tag push). 5. Adding introduced must not weaken the existing three-set assertions (self-mutation test: deleting a row reds).
p1-census-oracle.md
Contract — P1 dead-code census oracle (E3) + CI-integrity oracle (E5)
P1 census oracle
- Input: live
src/**+ retirement gates + importer graph → per-surface{status: dead|live, evidence}. - Authorizes: (a) a base-red (#3284) drop iff its subject is census-
dead; (b) coverage-denominator exclusion of census-deadsurfaces. - Non-vacuity (DIR-043): MUST fail if it evaluates an empty set. A self-mutation negative test plants a dead-code shard / retired-import and asserts the census flags it and the exclusion gate fires.
- Bidirectional guardrail (post-plan squad — the loophole is two-sided):
- False-negative (rubber-stamp): a base-red (#3284) may be dropped as "dead" ONLY with independent evidence (retirement-gate ref / ADR / zero-importer-AND-not-dynamically-reached), never self-certification; each of the 23 drops is individually evidenced and reviewer-verifiable.
- False-positive (mis-mark live as dead): a "known-live is never dead" guard protects surfaces reachable only dynamically (entry points, plugin/registry dispatch,
getattr/import-string, CLI wiring) whose static importer-count is 0 — these are NOT droppable; a positive-side negative test plants a live-but-importer-0 surface and asserts the census refuses to mark itdead. - Boundary: census-
deadbehavioral tests are excluded from CI; the enforcement allowlists (test_no_dead_symbols/test_no_dead_modules/test_no_retired_subsystems) that quote dead surfaces are always-on and out of scope of exclusion. A committed membership (E4) prevents relabeling either way.
CI-integrity (collection-completeness) oracle
- Input: the real on-disk reinstated workflow YAML (not a literal snippet).
- Assertion: no test is selected by zero gates; the enumerated must-run gates are all wired; the two-authority routing model is internally consistent.
- Non-vacuity: carries a floor + a planted-orphan negative test (a test wired to no group reds the oracle). Fixes the zero-producer/inert-slot bare-name false-pass (#2967).
- Runtime vs static: the oracle proves wiring; each gate's runtime execution is evidenced by its own job result (SC-004), never inferred from the static oracle.
Gate-selection authority (#2476)
- A single importable function is the one authority for "which tests/gates a change selects", reused by both CI routing and local pre-PR parity — not a second parser.
router-two-authority.md
Contract — Router two-authority model (E5)
The path→job routing has exactly two hand-authored sources; everything else is derived + asserted.
The two authorities
1. dorny filter block (changes job): group → globs[] (path→group). 2. job if: gates: if: needs.changes.outputs.<group> (group→job).
Derived (asserted against the two, never hand-maintained)
- The catch-all unmatched OR-list; the aggregator
needs:set; the completeness oracle.
Invariants
1. A src/ change matched by no group forces run-all (loud unmatched alarm), never a silent skip. 2. docs/corpus are non-src → excluded from the unmatched loop (data, not code). 3. Gate-selection authority (FR-016/#2476): ONE importable function parses these two authorities (does not re-encode them) and answers "which shards/gates does this diff select" — reused by CI routing and local pre-PR parity (no second parser). Built with T1 (router); consumed by L3. 4. Non-vacuous completeness oracle: reads the real on-disk workflow YAML; fails if any test is zero-gated or an enumerated must-run gate is unwired; carries a planted-orphan negative test (#2967). 5. Reintroducing filters updates test_ci_quality_path_filters.py in lockstep* (it currently asserts the husk has no* filter). 6. on.paths (Gate-0) and dorny filters: (Gate-1) stay in lockstep OR the workflow runs on every PR with Gate-0 dropped (#3008 hazard); corpus keeps its exit-5 floor.
Module shard registry (E7, matrix realization)
workflows (≤ GitHub's 20-unique-reusable-workflows-per-caller limit), not ~40 separate module-*.yml. The registry (module → roots, --cov target, tier, shard_count) is the single data source; adding a module is a registry row, not a new workflow file.
- Per-module shards are a matrix over a committed module registry inside a bounded set of reusable
Diff-scoped matrix (mission ci-modules-diff-scoping)
registry row still gets a matrix leaf every run (ci-modules.yml's generate-matrix job iterates modules[] generically), but each leaf carries a selected flag computed via scripts/ci/gate_selection.py's select_modules — the SAME single gate-selection authority ci-router.yml's path routing consumes (invariant 3 above). A module the diff did not select still instantiates its test job leaf; that leaf is gated if: matrix.selected == 'true', reporting a neutral SKIPPED check — filtering it out of include: instead would report NO check at all and hang a required-check gate forever.
fail-closed unmatched-src case both select every module (run-all), matching ci-router.yml's own run-all semantics exactly. A push whose pre-push SHA is the null SHA (first push / force push — a degenerate base git diff cannot resolve) also forces full selection.
github.event_name == 'push' as a run-all condition (pinned by test_ci_router_transcription_guards.py::test_changes_outputs_never_force_run_all_on_push); ci-modules.yml diff-scopes push the same way it scopes a PR.
(ci-nightly.yml) carries its own generate-full-matrix + full-module-matrix jobs that realize the SAME committed registry unconditionally selecting every module/shard, calling module-tests.yml (the single-job reusable shard) directly with mode: full — this is the cross-module post-merge safety net that replaces the old push-always-full-scope behavior (operator decision). It does not uses: ./.github/workflows/ci-modules.yml (a nested multi-job reusable-workflow call): the architectural gate-coverage model's local-uses: splicer (tests/architectural/_gate_coverage.py::_splice_local_uses) requires a spliced reusable workflow to define exactly one job — module-tests.yml qualifies, ci-modules.yml (a 3-job matrix orchestrator) does not.
set.** ci-modules.yml's generate-matrix job publishes the resolved selected-module set as the selected-modules artifact. ci-aggregate.yml's collect job downloads it and reconciles per module: a SELECTED (changed) module missing from the triggering run's shards fails closed exactly as before — never served from the stale-artefact fallback, even when the fallback has it, so a genuine shard failure on changed code can never be masked by old green. An UNSELECTED (unchanged, provably byte-identical to the fallback source) module missing from the triggering run is backfilled from the most recent successful "CI Modules" run (the PR branch's own prior run, else the default branch's) — safe because diff-cover only scores the actual diff, which lives entirely in selected/fresh modules, so backfilling unchanged coverage cannot mask a regression while keeping whole-tree/Sonar coverage coherent. When no selection info is available at all (a legacy/pre-feature run, or a download failure), every missing shard remains fallback-eligible, matching the pre-Approach-C behavior. Manual workflow_dispatch replay downloads neither the fallback run nor the selection artifact, so it always requires complete evidence from its exact source attempt (no backfill at all).
- The module matrix is registry-generated THEN authority-scoped, never a second re-encoded map: every
mode == 'full'(workflow_dispatch, or aworkflow_callinvocation withmode: full) and the FR-004- Push is diff-based, exactly like a PR.
ci-router.yml'schanges.outputs.*no longer folds in ci-nightly.ymlis the full-run home. Since push is no longer always-full-scope, the nightly lane- **
ci-aggregate.ymlstays whole-tree via selection-aware backfill (Approach C), never a shrunk expected