Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 195 additions & 6 deletions .github/workflows/quality-advisory.yml

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions docs/CI.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,45 @@ for diff-coverage and usually *not* true for complexity, so the two land in diff
| Diff-coverage | **Inline on the Files changed tab**, one `::notice` per contiguous uncovered range of lines the PR changed, plus a step summary. Every line it flags is a line the PR touched, so this is the one signal that is reliably inline. |
| Complexity (`C901`) | A **merge-base-vs-HEAD delta** — only functions this PR introduced over the threshold or made more complex. Findings anchor on the `def` line, which a body-only edit does not touch, so **most complexity annotations appear in the Checks tab and the step summary rather than inline**. The summary table is this signal's primary surface. Pre-existing findings are never reported; the full list stays in the job log. |
| Duplication (`jscpd`) | Step summary only. jscpd emits one location per clone pair chosen by scan order, so annotating it would anchor on the untouched twin about half the time. |
| **Gate liveness** | A pass/fail table proving each gate above actually *measured* something. See below — this is the only job in that workflow that can go red. |
| Mutation (`mutmut`) | A **killed / survived / not-covered** table in the step summary, with the surviving mutants listed — those are injected bugs the tests did not catch. Runs on PRs too: measured at **461 mutants in 3 seconds** (87 killed, 19 survived) over the bounded scope, because mutmut 3 only runs the tests that cover each mutant. Repaired 2026-07-27 — `mutmut<3` resolved to 2.5.1, which crashes on Python 3.14 before generating a single mutant and, thanks to `\|\| true`, had been reporting success in 37s while measuring nothing. |

### Gate liveness — the check that watches the checks

Three defects across two of `quality-advisory.yml`'s gates spent months green. Two were gates
**measuring nothing** — diff-coverage (a shallow fetch destroyed its merge base, and the resulting
empty report looked clean) and mutation (the tool crashed before producing a single mutant, and
`|| true` made that green in 37 seconds). The third was the close cousin: a gate that measured
correctly and **published a wrong number** — a `grep` for a line the tool never prints, so a healthy
461-mutant run reported "Killed 0".

The rubric's anti-metric rule guards against trusting a *number* too much. Nothing guarded against
trusting a *green check that never ran*. The `liveness` job is that control.

Each measurement job emits a small **receipt** recording what it examined; the `liveness` job reads
them all and demands either proof of execution or an explicit, reasoned "nothing to measure".

- **Liveness is not "the gate found something."** A clean repo legitimately has zero clones. Receipts
count units **examined** — files scanned, mutants processed, changed lines analysed — which is
non-zero whenever the tool ran, whatever it concluded. A check that fires on good news gets muted.
- **"Nothing to measure" passes — if it says why.** `no lines with coverage information in this diff`
is a real, correct outcome. A silent empty report is not. The two look identical on screen; the
reason is the difference.
- **Numbers must reconcile, against an independent source.** Two checks, because the obvious one is
weaker than it looks. `killed + survived + no-tests + other` must equal the mutants processed — but
since `killed` is *derived* as total-minus-listed, that sum reduces algebraically to
"every listed mutant carries a recognised status" and never validates `killed` at all. So `killed`
is additionally reconciled against **mutmut's own counter**, parsed from its progress line: two
independent derivations that must agree. That second check is what would catch a recurrence of the
`killed=0` bug; the sum alone would not. `tests/test_gate_liveness.py` asserts both, including an
explicit test documenting the sum's blindness rather than hiding it.
- **It is the one job there allowed to go red**, deliberately: it has no `continue-on-error` and no
`|| true`. A red mark still blocks nothing — it is not, and must never become, a required context.

`tests/test_gate_liveness.py` replays all three historical incidents and asserts each is caught, and
asserts the good-news cases pass. A liveness gate that cannot catch the failures it was built for
would be exactly the thing it exists to prevent.

### The `CI gate` roll-up

`CI gate` `needs:` the individual legs, runs with `if: always()`, and fails **only** on a `failure` or
Expand Down
36 changes: 34 additions & 2 deletions docs/Code_Quality_Standards.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ The full standard follows: the evidence review, the AI failure-mode map, the com
| **Applies to** | Any project developed under the [SDS](Secure_Development_Standards.md). **MessageFoundry (MEFOR)** is the reference implementation (Appendix A); future projects add Appendix B, C, … |
| **Maintained by** | Project maintainers (open-source). Each deploying/adopting organization assigns its own local owner. |
| **Status** | Draft for review |
| **Version** | 0.10 |
| **Version** | 0.11 |
| **Date** | July 27, 2026 |
| **License** | Publishable under the project's open-source license; intended to be shared with adopters and reused across projects. |
| **Review cadence** | At least annually, and on any material change to the metric evidence base or the AI toolchain. |
Expand Down Expand Up @@ -161,6 +161,37 @@ Each signal is a **risk → control → measure**, tagged by **gate type** (dete
>
> **Evidence & citations for the matrix.** Every signal and claim above maps to its supporting study in [**Appendix B.3**](#b.3-evidence-behind-each-rubric-element) (per-element evidence table), with full bibliographic citations in [**Appendix B.4**](#b.4-references), the derivation method in [**Appendix B.2**](#b.2-how-the-matrix-was-derived), and the claims that *failed* verification in [**Appendix B.5**](#b.5-what-was-refuted-the-verification-worked).

### 4.0 The liveness rule (hard) — a gate that cannot fail is not a control

**Every advisory gate must prove it measured something, or say why it could not.** A gate that reports
a conclusion without recording that it performed a measurement is indistinguishable from one that
worked, and it will stay that way indefinitely, because nobody investigates a green check.

This rule was written from failure, not theory. Three defects across **two** of this document's own
Tier 2 gates were found (2026-07-27) to have been green throughout: two gates measuring nothing (one
of them for two full versions of this rubric, during which it was scored ✅ **Built** here — v0.10),
and one gate that measured correctly but published a wrong derived number. Section 4.1 protects
against trusting a *number* too much; nothing protected against trusting a *green check that never
ran*. That is a distinct failure mode and it needs its own control:

1. **Proof of execution, not of findings.** The receipt counts units *examined* — files scanned,
mutants processed, changed lines analysed — never units *found*. A clean codebase legitimately
reports zero clones, and a liveness check that fires on good news gets muted, leaving the project
worse off than before it existed.
2. **"Nothing to measure" is acceptable only when stated.** An explicit, reasoned declaration passes;
a silent empty result fails. The two are visually identical, which is precisely why the reason is
load-bearing.
3. **Reported numbers must reconcile against an INDEPENDENT source.** A gate can execute perfectly
and still emit a wrong derived figure. Two rules, because the obvious one is weaker than it looks:
parts must sum to the whole, each counted independently and never as a remainder (a remainder
makes the sum true by construction); *and* any derived headline figure must be cross-checked
against a second, independently produced measurement of the same quantity. A sum that includes a
derived term can be algebraically blind to that term — ours was, and the blindness is now asserted
by a test rather than assumed away.

Applies to any gate, in any project adopting this rubric — a deferred or advisory gate that silently
stops measuring is worse than an absent one, because the scorecard still counts it.

### 4.1 The anti-metric rule (hard)

**Do NOT certify quality — or fail a build — on any single one of:** line-coverage %, LOC, raw or cognitive cyclomatic complexity, or SonarQube severity counts. Each is a weak or gameable predictor (§2). They may be *surfaced as advisory triage signals*; they must never be *the* quality gate. This mirrors the AI companion's "gates are deterministic checks, never ask the model to be secure" — here: *a scoreboard is never the verdict.*
Expand Down Expand Up @@ -270,7 +301,7 @@ The five gates this document adds (rubric rows 7–11) are *quality-measurement*

Ordered by anti-slop leverage, not effort (build placement per §5). **✅ = shipped** (advisory; \#1028 or \#1040):

1. **Mutation testing** — highest leverage; directly counters shallow-test slop, extra weight under the solo-maintainer review deviation (A.4). ✅ **shipped** (#1040 — `mutmut` over a bounded, well-tested module, mirror-nightly + `workflow_dispatch`; widen the scope later).
1. **Mutation testing** — highest leverage; directly counters shallow-test slop, extra weight under the solo-maintainer review deviation (A.4). ✅ **shipped** (#1040 — `mutmut` over a bounded, well-tested module; runs on PRs + nightly cron + `workflow_dispatch`; widen the scope later). *(v0.11: was "mirror-nightly" — there is no mirror post-cutover, and the job no longer carries a repo-slug gate.)*
2. **Clone-detection on diffs** — ✅ **shipped** (`jscpd`, store-parity whitelisted) — catches the copy-instead-of-abstract signature the parallel-worktree workflow is most exposed to.
3. **Diff-coverage visibility** — measured on changed lines, guidance only (never a whole-repo % gate — §4.1). ✅ **shipped** (#1040 — `pytest-cov` + `diff-cover`, PR-only).
4. **Advisory** `C901` **complexity** — ✅ **shipped** (advisory triage).
Expand Down Expand Up @@ -366,6 +397,7 @@ The evidence caveats in **§7** are part of this appendix's basis: the metric-in

| Version | Date | Change |
|----|----|----|
| 0.11 | July 27, 2026 | **Added the liveness rule (new section 4.0) and built the control.** v0.10 recorded that signal 7 had been scored ✅ Built for two versions while its tool crashed before producing a mutant. That is a failure mode this rubric had no defence against: section 4.1 forbids over-trusting a *number*, but nothing forbade over-trusting a *green check that never ran* — and three defects across two of the five Tier 2 gates turned out to have that shape (two measuring nothing, one publishing a wrong derived number). Section 4.0 now requires every advisory gate to prove it measured something (units **examined**, never units found — a clean repo reports zero and must still pass) or to declare explicitly, with a reason, that it had nothing to measure; and any derived headline figure must be cross-checked against an independently produced measurement of the same quantity. Implemented as the `liveness` job in `quality-advisory.yml` — the only job there permitted to go red — with `tests/test_gate_liveness.py` replaying the historical incidents to prove the check catches them, and the good-news cases to prove it does not fire on them. **The control was itself adversarially reviewed before merge, and the review found it carrying the same weakness it was built to catch, in three places** — a dead coverage gate could pass by claiming "not applicable", an empty mutmut results file reported a flawless score, and the reconciliation sum was algebraically blind to the very count it claimed to protect. All three are fixed and regression-tested; rule 3 above was rewritten because of the third. No scoring change (A− stands); the gates were repaired in v0.10, this is the control that keeps them honest. |
| 0.10 | July 27, 2026 | **Restored to the repo, and corrected three claims that did not survive measurement.** This file had been absent from the repository's entire git history despite being cited by `quality-advisory.yml` and `pyproject.toml`; it is restored here from the maintained copy. Corrections, each measured rather than reasoned: **(a) Signal 7 was scored ✅ Built in 0.8 and 0.9 while producing nothing.** `mutmut<3` resolved to 2.5.1, which crashes on Python 3.14 in its pony-ORM cache (`cannot pickle 'itertools.count'`) *before generating a single mutant*; `\|\| true` made the job report success in 37s, so the gate looked green for two versions. Repaired on `mutmut==3.6.0` (+ `pytest-timeout`, and `source_paths` must be the package, not the one file, or the mutant copy cannot import `conftest`). Now genuinely measured: **461 mutants, 87 killed, 19 survived, 3 seconds** — so the "Expensive / never per-PR" cost model in §5 was also wrong, and mutation now runs on PRs. **(b) Signal 11's "85 functions over C901>10" is now 122 across 43 files**, and the raw list was found unusable as a diff signal (every finding anchors on one `def` line), so a merge-base delta was added that reports only PR-caused changes. **(c) Signal 8 now emits inline PR annotations** rather than console-only output. The A− verdict stands, but note that (a) is exactly the failure mode this rubric exists to catch — an advisory gate that reports success while measuring nothing — and it was caught by re-verification, not by the gate itself. |
| 0.9 | July 14, 2026 | **Restatused signal 10 (lint breadth) to ✅ Built — all 11 signals now Built.** The `extend-select = [B,C4,SIM,UP,I]` sweep shipped (#1047): B008 handled via `extend-immutable-calls` + a route-layer per-file ignore, 515 auto-fixed, 235 grandfathered with `# noqa`, enforced by the required `ruff check` leg. Flipped the exec verdict, §5 gate table + callout, §6 map, and Appendix A.1 / A.2 (row 10 + Tier-2 roll-up) / A.3 (gaps list + "remaining gate" prose → rollout *record*). No scoring change (A− stands). |
| 0.8 | July 14, 2026 | **Restatused signals 7 (mutation) + 8 (diff-coverage) to ✅ Built.** Both shipped as advisory jobs in `quality-advisory.yml` (#1040) — mutation over a bounded module (mirror-nightly + `workflow_dispatch`), diff-coverage on the diff's changed lines (PR-only). Flipped every place that called them deferred: the exec verdict, §5 gate table + callout, §6 map, and Appendix A.1 / A.2 (rows 7–8 + the Tier-2 roll-up) / A.3 (gaps list + DEP-1 note) / A.4. **Only signal 10 (lint breadth) remains designed-but-deferred → 10 of 11 signals now Built.** No scoring change (A− stands). |
Expand Down
20 changes: 12 additions & 8 deletions docs/quality-gates/HANDOFF-mutation-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,14 @@ diff-cover coverage.xml --compare-branch=origin/main --fail-under=0
git fetch --no-tags --depth=1 origin main || true
diff-cover coverage.xml --compare-branch=origin/main --fail-under=0 || true
```
**Cost lever:** if a full-suite run per PR is too much on the private repo, either add a path filter (only run
when `messagefoundry/**` or `tests/**` change) or gate the whole job to the mirror with the repo-slug
`if: github.repository == 'MEFORORG/MessageFoundry'` (free minutes) — but then it won't report on private
PRs. Start advisory-on-PR; move to mirror if cost bites.
**Cost lever:** if a full-suite run per PR is too much, add a path filter (only run when
`messagefoundry/**` or `tests/**` change).

> **STALE — pre-cutover (corrected 2026-07-27).** This paragraph used to offer "gate the whole job to
> the mirror with the repo-slug `if: github.repository == 'MEFORORG/MessageFoundry'` (free minutes)".
> **There is no mirror**; the cutover moved development directly onto the public repo, and that
> repo-slug gate was *removed* from the mutation job. Do not re-add it — it would make the job a no-op
> on every PR while looking deliberate.

---

Expand All @@ -137,8 +141,8 @@ that *adversarially* proves your tests assert something (rubric A.4: matters mos
2. **Where it runs** (mutation is too slow for every private PR):
- **(A) PR, diff-scoped** — mutate only files changed vs `origin/main` (small diffs → few mutants →
tolerable). Closest to the rubric's "mutation on changed code"; higher per-PR cost. **Recommended.**
- **(B) mirror-nightly, rotating** — a nightly cron on the mirror (free minutes) mutates a rotating
slice. Cheap, but not per-PR.
- **(B) nightly, rotating** — a nightly cron mutates a rotating slice. Cheap, but not per-PR.
*(Was "mirror-nightly (free minutes)" — there is no mirror post-cutover.)*

### 3b. Local verify (against the installed mutmut version)
```powershell
Expand Down Expand Up @@ -194,14 +198,14 @@ mutmut results
mutmut results || true
```

### 3c′. CI job — option (B) mirror-nightly (add a `schedule:` trigger + repo-slug gate)
### 3c′. CI job — option (B) nightly (add a `schedule:` trigger)
Add the cron to the workflow's `on:` block, then the job:
```yaml
# on:
# pull_request:
# workflow_dispatch:
# schedule:
# - cron: "23 4 * * *" # nightly; the job's repo-slug if keeps it mirror-only (free minutes)
# - cron: "23 4 * * *" # nightly sweep against main

mutation-nightly:
name: mutation (nightly, advisory)
Expand Down
Loading
Loading