Contracts

gate-disposition-contract.md

Contract — pinning-rule inventory and dispositions

Mission: sonar-per-pr-coverage-reuse-01M2FR32 · Owner: WP03 (derives + executes) · Requirement: FR-010, C-005

Why this is a contract and not a checklist

A rule that goes red when the topology changes is found by CI. A rule that goes greener by deletion is found by nobody. The mission's own research inventory was ~4× understated and contained four false positives — so the inventory must be derived mechanically, never inherited from prose.

Derivation (WP03's first task, before any edit)

Enumerate every automated rule referencing the job name, the host workflow, or the retiring step's invocation, across the whole test tree. Classify each by assertion form, because form predicts what relocation does to it:

Assertion formBehaviour on relocation
exact-string over a conditionbreaks — the pinned literal is unsatisfiable under the new trigger
set-equality over job namesbreaks — reds on removal
path-anchored lookupbreaks — its operand disappears
derived relation over a parsed modelusually survives; verify the model covers the new host
docstring / synthetic fixtureno dependency — false positive, record as such

Disposition vocabulary (exactly one per rule, with a reason)

host. Default for anti-drift guarantees.

justification describing a deleted step). Re-express it; assert each conjunct individually, so dropping one is caught, rather than pinning a whole condition string as a brittle blob.

reason**. This resolves the v1 contradiction that forbade the disposition which is often correct.

  • relocate — the property still matters and still has a subject. Move the assertion to the new
  • rewrite — the property matters but its expression no longer can (an unsatisfiable literal, or a
  • retire-as-moot — the subject genuinely ceased to exist. Permitted, and **recorded with the

Prohibited: deleting a rule with no disposition; leaving a justification that describes something no longer true; weakening an assertion to make it pass.

Known members (seed only — the derivation is authoritative)

RuleFormExpected disposition
PR-only condition on the reporting jobexact-stringrewrite — per-conjunct, in the new trigger's vocabulary
Cross-surface action pin-paritypath-anchored, two operandsrelocate — re-point the second operand; never drop
Non-blocking allowlist entrydict entry + non-empty-rationale checkrewrite or retire-as-moot with reason (its subject leaves the scanned file)
Exact job-set equality on the retiring hostset-equalityrewrite — remove the retiring job from the expected set
Governance job-inventory documentsprose, gate-enforced on workflow changesrewrite — C-010 lockstep

Aggravator: an open pull request already touches one of these files. Check before editing.

Acceptance

  • Every derived rule carries a disposition and a reason (SC-009).
  • The inventory is reproducible by re-running the derivation, not by reading this file.
  • No rule is deleted without an explicit retire-as-moot record.

reporting-job-contract.md

Contract — the per-change reporting job

Mission: sonar-per-pr-coverage-reuse-01M2FR32 · Owner: WP02 · Host: .github/workflows/ci-aggregate.yml

This mission adds no HTTP API. Its externally-visible contracts are the conditions under which a report may be produced and the provenance of every input. Each clause below maps to a spec requirement and is independently assertable — the mission's gates assert clauses, never the whole condition string, so dropping one clause is caught.

C1 — Execution condition (all conjuncts required)

#ConjunctRequirementWhy not the obvious thing
1triggering run's event is a pull requestFR-014, C-001github.event_name == 'pull_request' is always false under workflow_run. A verbatim port passes its gate over a job that never runs.
2triggering run's head repository equals this repositoryFR-011, NFR-004, C-003Secret withholding no longer provides this. source.json carries no origin field.
3the assembly job succeededFR-004, FR-007—
4the assembly reported a complete setFR-007, NFR-006The artefact uploads even on failure, so presence ≠ completeness.
5the quality-service credential is presentFR-008Absent ⇒ skip with an advisory notice, never fail.

always() is prohibited on this job. The sibling change-coverage gate uses always() plus an internal re-check; copying that shape without the re-check publishes a partial figure that reads as a coverage regression.

C2 — Input provenance

InputSourceProhibited source
coverage reportsthe reconciled per-change setany other run's reports; the stale fallback
change identitysource.json pr_number (validated against the immutable merge ref)workflow_run.pull_requests[] — a mutable projection the producing script explicitly refuses
analysed revisionsource.json tested_shathe trusted checkout's own revision; github.sha
head/base branch names, origin repositoryone authenticated read keyed on the validated pr_numberthe event payload
every analysis settingthe trusted checkout, passed explicitlyany file read from the change under review

C3 — Analysis-configuration trust (FR-012, NFR-005)

The scanner must not consume a configuration file supplied by the change under review.

Mechanism (corrected 2026-09-14 — see the refutation below): keep the analysis base directory at the trusted checkout root and replace ONLY the two analysed trees in place.

fetch the change's tested revision
verify the fetched revision equals the recorded tested revision, and FAIL LOUD on mismatch  (NFR-008)
remove, then restore, exactly the two analysed trees from that revision

Removing before restoring is required so deletions in the change are honoured rather than leaving stale files behind. Trust becomes an allowlist of two replaced paths: the analysis configuration, the build configuration, and the workflow definitions all stay at trusted content.

⚠️ The subdirectory design is REFUTED — do not reintroduce it

An earlier version of this contract specified placing the change's sources in a subdirectory and pointing the analysis base directory at it. That is wrong, in the exact way it was trying to be right: the scanner locates its configuration file relative to the base directory, so pointing the base directory at the change's subtree makes it load the change's own configuration — the precise FR-012/NFR-005 breach the subdirectory was chosen to prevent.

It also traded a file-enumeration problem for a key-enumeration problem: explicit arguments override only the keys they name, while a contributor-authored configuration file can set keys the trusted side never passes.

Recorded here rather than deleted, because the reasoning is the useful part: a mechanism can defeat its own purpose through a detail of where a tool looks for its configuration.

Open risk retired: whether a base-directory shift breaks coverage path resolution was ranked this mission's largest unknown. It was resolved negative — coverage reports carry an empty <source> and repo-root-relative filenames, so both roots move together. The mechanism above sidesteps the question entirely by not shifting the base directory at all.

C4 — Non-blocking guarantee (NFR-003, SC-006)

Three surfaces can block; the guarantee must hold at all three.

SurfaceMechanism
job conclusionjob-level continue-on-error
workflow-run conclusion (read by the aggregate verdict script)the terminal verdict job explicitly excludes this job
branch protectionoutside the repository; currently permissive. Declared, not claimed — no test can pin it.

The terminal verdict job is what makes "excluded" declarable, and therefore assertable.

C5 — Regression battery (NFR-007, SC-007)

A permanent in-tree fault-injection battery. Each mutation must be demonstrated red:

#MutationWhy it is in the battery
1reintroduce the retiring step in its canonical formthe literal regression
2reintroduce it inlined, with no shared-target referencedefeats a literal-string rule
3reintroduce it behind indirection (a shell wrapper or script)defeats both
4reintroduce it in a new workflow fileclosed candidate lists miss net-new files
5remove the non-blocking declarationNFR-007's second half; asserted by nothing today
6remove the same-origin conjunctFR-011 / SC-008

Non-vacuity floor: the rule's workflow set must be asserted non-empty and to include the new host. The existing substrate is documented as blind to the retiring step's invocation form, and a sibling assertion currently passes while that step is present in the file — so a rule built naively on it is vacuous by construction.

C6 — Documentation-only changes

The retiring step runs one always-on check unconditionally on every change; its replacement home is code-scoped. Either preserve that check's unconditional execution or record the reduction deliberately. Silent loss is not acceptable.