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
92 changes: 85 additions & 7 deletions .github/workflows/tests-and-quality-gates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -460,16 +460,21 @@ jobs:
/tmp/mcp-venv/bin/pytest mcp-server/vmaf-mcp/tests/ -v --tb=short

# ===== Coverage gate (principles.md §3) =====
# ≥70% overall line coverage, ≥85% on security-critical files
# (core/src/dnn/, opt.c, read_json_model.c).
# ≥70% overall line coverage, ≥90% on security-critical files
# (core/src/dnn/, opt.c, read_json_model.c). Floors ratcheted aggressively
# on 2026-05-31 by ADR-0922 (was 37% / 85%); OVERALL_MIN further raised to
# 70% by master post-ADR-0922 (#420/#412 coverage uplift).
coverage:
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
name: "Coverage Gate (Ramping to 70% / 85% Critical)"
name: "Coverage Gate (70% Overall / 90% Critical — ADR-0922)"
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Need full history so we can check out the PR's merge-base for the
# coverage-delta gate (ADR-0922). Shallow clone breaks the delta job.
fetch-depth: 0
persist-credentials: false
- name: Install deps
run: |
Expand Down Expand Up @@ -636,21 +641,92 @@ jobs:
cat build-coverage/coverage.txt
- name: Enforce coverage thresholds
run: |
# docs/principles.md §3 targets: 70% overall, 85% critical (dnn/,
# docs/principles.md §3 targets: 70% overall, 90% critical (dnn/,
# opt.c, read_json_model.c). Applies to upstream + fork-added code:
# we've already linted upstream heavily (clang-tidy cleanup rounds
# 2-4) and will keep modifying it (SIMD fixes, refactors), so the
# safety net has to cover everything we might touch.
#
# Floor history (ADR-0637):
# Floor history (ADR-0637, ADR-0922):
# 40% — original floor (meson unit suite + Python feature/CLI tests)
# 37% — 2026-05-19 merge burst (PRs #1417, #1418, #1424, #1425)
# added ~2 200 LOC of new MCP/HIP/DNN/scaffold C that the
# existing test suite does not yet reach, diluting overall
# coverage from ~39% to 37.7%. The floor tracks the measured
# floor, not the aspirational target; ratchet upward as
# targeted tests land for those paths.
scripts/ci/coverage-check.sh core/build-coverage/coverage.json 37 85
# 60% — 2026-05-31 aggressive ratchet (ADR-0922). The 37% floor
# was tracking measured, not enforcing investment. The
# ratchet pushes the overall bar back toward the
# docs/principles.md §3 aspirational target so coverage
# gaps surface immediately instead of accumulating. See
# ADR-0922 for the 30-day grace period covering PRs that
# were already open at ratchet time.
# 70% — 2026-05-31 further uplift from #420/#412 coverage additions
# (core/src/dnn/ + compat/python-vmaf/ tests); floor raised
# to match measured coverage on master post-merge.
scripts/ci/coverage-check.sh core/build-coverage/coverage.json 70 90
- name: Compute base-branch coverage for delta gate
# ADR-0922 per-PR ratchet: rebuild + re-measure coverage on the PR's
# merge-base so we can compare absolute-floor head numbers against
# what the target branch (usually origin/master) was reporting just
# before this PR. The base build is intentionally lean (CPU-only,
# no ORT install, no Python suite) to keep the additional CI cost
# bounded; the delta gate only needs the gcovr JSON summary.
if: github.event_name == 'pull_request'
env:
BASE_REF: ${{ github.event.pull_request.base.sha }}
run: |
set -euo pipefail
# Save head coverage so it survives the checkout swap below.
cp core/build-coverage/coverage.json /tmp/head-coverage.json
# Compute the changed-file list against the merge-base. The delta
# gate only scores files that appear in the diff.
git fetch --no-tags --depth=1 origin "$BASE_REF" || true
MERGE_BASE="$(git merge-base HEAD "$BASE_REF")"
echo "Merge base: $MERGE_BASE"
git diff --name-only "$MERGE_BASE"..HEAD > /tmp/changed-files.txt
echo "----- Changed files -----"
cat /tmp/changed-files.txt
# Worktree-add the base so we don't disturb the head checkout
# (artifacts, build dirs, coverage outputs all stay put).
git worktree add /tmp/base-tree "$MERGE_BASE"
(
cd /tmp/base-tree
meson setup base-cov --buildtype=debug \
-Db_coverage=true -Denable_cuda=false -Denable_sycl=false \
-Denable_float=true \
-Dc_args=-fprofile-update=atomic \
-Dcpp_args=-fprofile-update=atomic \
core
ninja -C base-cov
meson test -C base-cov --print-errorlogs --num-processes 1 || true
~/.local/bin/gcovr --root . \
--filter 'core/src/.*' \
--exclude '.*/test/.*' \
--exclude '.*/tests/.*' \
--exclude '.*/subprojects/.*' \
--gcov-ignore-parse-errors=negative_hits.warn \
--gcov-ignore-parse-errors=suspicious_hits.warn \
--json-summary /tmp/base-coverage.json \
base-cov \
2> >(grep -vE 'Ignoring (suspicious|negative) hits' >&2 || true)
)
# Cleanup the worktree so subsequent steps see a clean tree.
git worktree remove --force /tmp/base-tree
- name: Enforce coverage-delta gate (ADR-0922)
# Fails if overall coverage drops by more than 0.5pp OR any
# touched file drops by more than 0.5pp vs the merge-base. The
# ratchet is one-way: drops require a follow-up ADR superseding
# ADR-0922 to justify the new floor.
if: github.event_name == 'pull_request'
run: |
scripts/ci/coverage-delta-check.sh \
--base-json /tmp/base-coverage.json \
--head-json /tmp/head-coverage.json \
--changed-files /tmp/changed-files.txt \
--max-overall-drop 0.5 \
--max-file-drop 0.5
- name: Upload coverage artifact
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
Expand Down Expand Up @@ -743,7 +819,9 @@ jobs:
cat build-coverage-gpu/coverage.txt
- name: Enforce coverage thresholds (GPU)
run: |
scripts/ci/coverage-check.sh core/build-coverage-gpu/coverage.json 70 85
# ADR-0922 ratchet (2026-05-31): critical bar raised to 90 (was 85).
# Overall floor kept at 70 (master already above the PR's proposed 60).
scripts/ci/coverage-check.sh core/build-coverage-gpu/coverage.json 70 90
- name: Upload GPU coverage artifact
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
Expand Down
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,19 @@ linked AGENTS.md before resolving conflicts.
ports `d3647c73` `feature/speed` extractors (`speed_chroma` +
`speed_temporal`).

- **Coverage Gate ratchet + per-PR delta gate (ADR-0922)**:
[ADR-0922](docs/adr/0922-coverage-ratchet-aggressive.md). Absolute
floors live in `scripts/ci/coverage-check.sh`
(`OVERALL_MIN=70`, `CRITICAL_MIN=90`, `PER_FILE_MIN[...]`); per-PR
drop tolerance lives in `scripts/ci/coverage-delta-check.sh`
(default 0.5pp on overall and per-touched-file). Lowering any
floor or loosening the delta tolerance requires a new ADR
superseding ADR-0922. The Coverage Gate job in
`.github/workflows/tests-and-quality-gates.yml` invokes both
scripts; the delta gate needs `actions/checkout` with
`fetch-depth: 0` because it runs `git merge-base`. See
[scripts/ci/AGENTS.md](scripts/ci/AGENTS.md) §Coverage Gate
ratchet for the full coupling.
- **CI action pins — Windows MSVC dev env**
([ADR-0635](docs/adr/0635-ci-warning-omnibus-2026-05-19.md)):
`.github/workflows/libvmaf-build-matrix.yml` uses
Expand Down
14 changes: 14 additions & 0 deletions changelog.d/changed/0922-coverage-ratchet-aggressive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
- chore(ci): aggressively ratchet the Coverage Gate floors and add a
per-PR coverage-delta gate. `scripts/ci/coverage-check.sh` now enforces
70 % overall (was 37 %; recovery raised beyond the proposed 60 % to
match measured coverage after #420/#412) and 90 % critical (was 85 %); every
`PER_FILE_MIN` override tightens by +5pp (`ort_backend.c` /
`dnn_api.c` 78 → 83, `tiny_extractor_template.h` 10 → 15). The new
`scripts/ci/coverage-delta-check.sh` runs on pull-request events in the
Coverage Gate job and fails any PR that drops overall coverage by more
than 0.5pp or drops any touched file by more than 0.5pp vs the
merge-base. PRs opened before 2026-05-31 get a 30-day grace window
(through 2026-06-30); thereafter the new floors and delta gate apply
uniformly. Floor changes are one-way: loosening requires a follow-up
ADR superseding ADR-0922. See
[ADR-0922](../docs/adr/0922-coverage-ratchet-aggressive.md).
175 changes: 175 additions & 0 deletions docs/adr/0922-coverage-ratchet-aggressive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# ADR-0922: Aggressive coverage ratchet + per-PR coverage-delta gate

- **Status**: Accepted
- **Date**: 2026-05-31
- **Deciders**: lusoris
- **Tags**: ci, coverage, gate, fork-local

## Context

The fork's Coverage Gate (`scripts/ci/coverage-check.sh`, governed by
[ADR-0110](0110-coverage-gate-fprofile-update-atomic.md),
[ADR-0114](0114-coverage-gate-per-file-overrides.md),
[ADR-0117](0117-coverage-gate-warning-noise-suppression.md), and
[ADR-0637](0637-ci-test-failures-omnibus.md)) historically tracked the
**measured** floor rather than the **aspirational** target.
[`docs/principles.md` §3](../principles.md) names 70 % overall and 85 %
critical, but the live floor in CI sat at 37 % overall and 85 % critical
after the 2026-05-19 merge burst (~2 200 LOC of new MCP/HIP/DNN/scaffold
C that the existing test suite did not yet reach).

Tracking-floor is honest about the present but creates two problems:

1. **No upward pressure.** A floor that follows measured coverage downward
never asks anyone to invest in tests. Coverage stays where the cheapest
path leaves it.
2. **No per-PR ratchet.** Even with the absolute floor in place, an
individual PR can quietly drop overall coverage from 50 % to 38 % as
long as it stays above 37 %. The next PR can drop from 38 % to 37.01 %.
Coverage decays one PR at a time and the gate never fires.

The fork already enforces the absolute floor via `coverage-check.sh`. The
missing piece is a **per-PR delta gate** that rejects any PR which erodes
coverage on the files it touches (or overall) past a small tolerance,
regardless of whether the absolute floor is still met.

This ADR raises the absolute floors aggressively (37 % → 70 % overall,
85 % → 90 % critical, plus +5pp on every per-file override in
`PER_FILE_MIN`) and introduces a new per-PR coverage-delta gate
(`scripts/ci/coverage-delta-check.sh`) wired into the Coverage Gate job
on pull-request events. Note: the original PR proposed 60 % overall;
master's post-merge coverage uplift from #420 and #412 allowed the
floor to be set to 70 % on recovery.

## Decision

We will:

1. **Raise the absolute floors** in `scripts/ci/coverage-check.sh`:
- `OVERALL_MIN`: 37 → 70 (original PR proposed 60; raised to 70 on
recovery because master already measured above 70 after #420/#412)
- `CRITICAL_MIN` (default per-file critical floor): 85 → 90
2. **Tighten every `PER_FILE_MIN` override by +5pp** (the
per-file structural-ceiling exemptions established by ADR-0114):
- `core/src/dnn/ort_backend.c`: 78 → 83
- `core/src/dnn/dnn_api.c`: 78 → 83
- `core/src/dnn/tiny_extractor_template.h`: 10 → 15

No override is *lowered* relative to its prior value — the ratchet is
one-way by design. Future per-file overrides may be raised further but
not lowered without a new ADR superseding this one.
3. **Introduce `scripts/ci/coverage-delta-check.sh`**, a per-PR gate that
compares head vs. merge-base gcovr summaries and fails if:
- Overall coverage drops by more than `--max-overall-drop`
(default **0.5pp**), OR
- Any file present in both reports AND touched by the PR's diff drops
by more than `--max-file-drop` (default **0.5pp**).

New files have no base row to compare to and are covered by the
absolute floors instead. Files not touched by the PR are not scored
(the absolute gate already covers their floor).
4. **Wire the new gate into `tests-and-quality-gates.yml`** as two steps
added to the existing `coverage` job:
- A lean CPU-only-fast build at the PR's merge-base (no ORT, no
Python suite) produces `/tmp/base-coverage.json`.
- `coverage-delta-check.sh` consumes the base + head JSONs plus
`git diff --name-only "$MERGE_BASE"..HEAD`.

The two steps only run on `github.event_name == 'pull_request'` so
`push` events to `master` continue to use the absolute gate alone.
5. **Grace period for in-flight PRs.** PRs opened **before
2026-05-31** are exempt from the ratchet for **30 days** (until
**2026-06-30**). The exemption is operational rather than enforced in
code: reviewers may merge such PRs with the new gate failing if (a)
the PR predates this ADR and (b) the failure is purely due to the
new floor or the new delta gate. Past the grace window, every PR is
subject to the new gates regardless of when it was opened.
6. **Exception process.** The new floors and the delta gate may be
loosened only by a **new ADR that explicitly supersedes ADR-0922**
and is referenced inline at the changed threshold in
`coverage-check.sh` / `coverage-delta-check.sh`. Inline
`# noqa`-style escape hatches are not permitted. Per-file overrides
added to `PER_FILE_MIN` after this ADR must still cite the ADR that
justifies each lower bar (the ADR-0114 pattern remains in force).

## Alternatives considered

| Option | Pros | Cons | Why not chosen |
|---|---|---|---|
| Raise only the overall floor (37 → 70) without delta gate | Simplest change | Doesn't stop one-PR-at-a-time decay below 70.49 % | Solves half the problem; the delta gate is the structural fix |
| Raise floors incrementally (37 → 45 → 55 → 70 over months) | Less disruption to in-flight PRs | Delays the upward pressure for weeks; humans forget to ratchet | The 30-day grace window achieves the same softness without sustained planning overhead |
| Lower the delta tolerance to 0.1pp | Tighter ratchet | Too noisy — gcov small-loop hit-count variance can move per-file percentages by ~0.2pp without any source change | 0.5pp is comfortably above measured variance and still catches real regressions |
| Apply delta gate to every file (not just touched) | Catches regressions caused by unrelated test changes | Penalises PRs for shared-test-suite drift outside their diff | Touched-file scoping keeps the gate actionable; overall delta already catches whole-tree drift |
| Skip the per-file override ratchet (only raise headline numbers) | Doesn't move the ADR-0114 baseline | Lets the exemptions decay relative to the headline | +5pp keeps the exemption gap constant in absolute terms |
| Make the delta gate advisory (continue-on-error) | Soft rollout | Soft gates become permanent — that's how 37 % survived for 12 days | Required from day 1, but with the 30-day grace window for in-flight PRs |

## Consequences

**Positive:**

- The Coverage Gate now matches the aspirational target in
`docs/principles.md §3` (overall 70 % matches the documented 70 %
goal; 90 % critical exceeds the 85 % goal).
- Per-PR ratchet prevents one-PR-at-a-time coverage decay. A PR that
drops `foo.c` from 80 % → 75 % now fails its own job, not the next
PR's job months later.
- Per-file override ratchet keeps the ADR-0114 exemptions honest: the
+5pp tightening is small enough to be reachable today (current
measurements sit above the new floors per the latest CI run) but
large enough that drift is visible.
- The delta gate's failure messages name the exact files and deltas
involved and point the contributor at the supersede-ADR process,
so failures are actionable rather than mysterious.

**Negative:**

- Coverage Gate wall-clock time increases on PRs: the base-coverage
build adds roughly one full CPU-only `meson setup + ninja + meson test`
cycle on top of the existing head build. Measured locally at ~4 minutes
added on `ubuntu-latest`. The Coverage Gate is non-blocking for `push`
events (job runs unchanged), so this only affects PR CI latency.
- The 30-day grace period requires reviewer discipline to apply
correctly. There is no automated marker for "PR predates the ratchet";
reviewers cross-check the PR's open-date against 2026-05-31 manually.
After 2026-06-30 the rule self-disables and the policy is uniform.
- New files added in a PR are not scored by the delta gate (they have
no base row). The absolute critical floor still applies to files
under `core/src/dnn/`, `core/src/opt.c`, and
`core/src/read_json_model.c`. Non-critical new files are protected
only by the 70 % overall floor — a PR could ship a wholly-new
`core/src/foo.c` at 30 % coverage without tripping the delta gate.
This is the same gap the existing absolute floors have; a future ADR
could add a per-new-file absolute floor if drift shows up.

**Neutral / follow-ups:**

- `docs/principles.md §3` numbers (70 % / 85 %) are now matched or
exceeded by this ratchet. The critical floor raised to 90 %; the
overall floor now matches the documented 70 % target, with ADR-0922
cited.
- The next ratchet (70 → 80 %) should land via a follow-up ADR once two
consecutive CI weeks show overall sitting above 75 %.
- `coverage-delta-check.sh` has a smoke-test suite in this PR's
reproducer section; consider promoting it to a tracked test under
`scripts/ci/tests/` once we add the directory structure for
CI-script tests.

## References

- [ADR-0110](0110-coverage-gate-fprofile-update-atomic.md) — atomic gcov
counters; foundation of the current gate.
- [ADR-0111](0111-coverage-gate-gcovr-with-ort.md) — `lcov → gcovr`
migration that made per-file numbers honest.
- [ADR-0114](0114-coverage-gate-per-file-overrides.md) — the
`PER_FILE_MIN` map this ADR tightens.
- [ADR-0117](0117-coverage-gate-warning-noise-suppression.md) — stderr
filter for `gcovr` suspicious-hits noise; preserved unchanged.
- [ADR-0637](0637-ci-test-failures-omnibus.md) — recorded the 40 % → 37 %
drop and committed to ratcheting upward as targeted tests landed; this
ADR executes that ratchet.
- [ADR-0221](0221-changelog-adr-fragment-pattern.md) — changelog fragment
pattern used by this PR's `changelog.d/changed/` entry.
- Source: `req` (paraphrased): user directed an aggressive ratchet of the
coverage thresholds plus a per-PR coverage-delta gate, with a grace
period for in-flight PRs and an exception process gated on a follow-up
ADR.
Loading
Loading