Metadata-Version: 2.5
Name: ratchet-gate
Version: 0.5.1
Summary: Ratchet quality gate: enforce monotonic improvement of code-quality metrics against a committed baseline.
Project-URL: Homepage, https://git.marim.dev/mateuscmarim/quality-gate
Project-URL: Source, https://git.marim.dev/mateuscmarim/quality-gate
Author: Mateus Marim
License: MIT License
        
        Copyright (c) 2026 Mateus Marim
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: baseline,ci,code-quality,coverage,linting,ratchet
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Software Development :: Testing
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: pydantic>=2.9
Provides-Extra: dev
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: pyyaml>=6; extra == 'dev'
Requires-Dist: types-pyyaml; extra == 'dev'
Provides-Extra: kotlin
Requires-Dist: pyyaml>=6; extra == 'kotlin'
Description-Content-Type: text/markdown

# quality-gate

A **ratchet** quality gate. It reads what your linters, type checkers, test
runners and scanners already wrote, compares every metric to a committed
baseline, and fails the build if any of them got worse.

Not a threshold gate. There is no number you have to reach before it is useful —
the baseline starts wherever your code is today, and the only rule is that it
may not move backwards. Improvements are adopted automatically after merge, so
the bar rises on its own and never falls.

```
## Ratchet Quality Gate

| Package | Metric | Baseline | Current | Δ | Policy | Status |
|---|---|---:|---:|---:|:---:|:---:|
| `api` | coverage_lines_pct | 89.09 | 88.22 | -0.87 | ↑ ±0.1 | 🔴 FAIL |
| `api` | complexity_violations | 108 | 105 | -3 | ↓ | 🟢 improved |
| `api` | mypy_errors | 0 | 0 | +0 | ↓ | ⚪ ok |
| `(global)` | secret_findings | 0 | 0 | +0 | =0 | ⚪ ok |

**FAIL** — 1 regression(s):
- `api` / **coverage_lines_pct** 89.09 → 88.22
```

`Policy` is the rule the row was scored by, not a second opinion about it: `↑`
and `↓` say which way is good, `=0` marks a metric that may only ever be zero,
and a `±` appears only where the metric declares a tolerance — so a row that
slipped inside its margin can be told apart from one the gate failed to notice.

## Why this repo exists

Five repos were running five copies of the same 400–1500 line script. They had
diverged: different exclusion lists, different report paths, one repo carrying a
one-line bug fix that never reached the other four because there was no
mechanism for it to travel by. Comparing them, almost every difference was
configuration wearing the costume of code.

So the program lives here once, and everything that legitimately differs per
repo lives in that repo's `quality-gate.toml`.

## Install

```bash
uvx ratchet-gate@0.3.0 check
```

No install step in CI, no vendored copy to keep in sync, and the pin says
exactly which version scored a given run.

The distribution is `ratchet-gate`; the command is available as both
`ratchet-gate` and `quality-gate`, so an adopting workflow can keep whichever
name it already says.

From a machine on the LAN you can install straight from the source of truth
instead, which is what `scripts/verify-against-repo.sh` does when checking an
unreleased commit:

```bash
uvx --from git+https://git.marim.dev/mateuscmarim/quality-gate@v0.3.0 quality-gate check
```

`git.marim.dev` has no public DNS record, so that form works from self-hosted
runners and nowhere else. PyPI is the form to put in a workflow. See
[RELEASING.md](RELEASING.md).

## Use

```bash
quality-gate check                              # score this run, exit 1 on regression
quality-gate check --baseline-ref origin/main   # score against the stricter baseline
quality-gate check --measured api,worker        # only these packages ran
quality-gate promote --monotonic                # adopt improvements only (post-merge)
quality-gate stage                              # multi-module Gradle repos only, see below
quality-gate collect-duplication                # run jscpd, write jscpd.json for the parser above
```

Point it at a directory of tool output — `.quality/<package>/coverage.json`,
`ruff.json`, `mypy.txt`, and so on — and a `quality-baseline.json`.

## Configure

`quality-gate.toml` at the repo root. Every key is optional; a repo whose layout
matches the defaults needs no file at all.

```toml
[gate]
artifacts_dir = ".quality"
baseline_path = "quality-baseline.json"
non_ratcheted_dirs = ["tests", "scripts", "notebooks"]
default_baseline_ref = "origin/main"
```

`non_ratcheted_dirs` is the one most repos change. Findings in those directories
stay in the uploaded artifacts — they just do not gate. One-off analysis
scripts, notebook data collectors and test helpers are read top-to-bottom and
run by hand; a 200-line `main()` is the right shape for what they are.

### Multi-module Gradle repos

Kover, detekt and Android Lint report per Gradle *module*, and a ratchet package
is usually several modules. `stage` folds them into the one file per group the
parsers read, so tell it which modules belong to which group:

```toml
[gate.kotlin]
detekt_config = "config/detekt.yml"          # read to tell complexity from formatting
kotlinc_log = "build/kotlinc-warnings.log"   # one repo-wide --console=plain capture

[gate.kotlin.groups]
core = ["core/network", "core/database"]
app = ["app"]
```

Then `uvx --from 'ratchet-gate[kotlin]==0.3.0' ratchet-gate stage` before
`check`. The extra carries PyYAML, which nothing but the detekt config reader
needs.

The map lives in your repo rather than in this package because your workflow's
change-detection step and your own test suite read it too — a TOML table serves
all three.

Two things `stage` refuses to do, both of which used to be silent passes: it
will not stage a zero for a module that produced no report, and `check` will not
score a staged snapshot older than the reports it came from — including one left
behind by a `./gradlew clean`, or by a different version of the gate.

## Two things it will not do

**It will not let a branch set its own bar.** Every number the gate enforces
comes out of a tracked file on the branch being scored, so the same commit that
adds 47 lint errors can raise `lint_errors` to 47 and pass. That needs no
malice: `promote` without `--monotonic` sets baseline = current for *every*
metric, so a branch that runs it locally to seed one new row rewrites all the
others. Pass `--baseline-ref origin/main` in CI and each metric is scored
against the stricter of the two baselines.

**It will not go green when it could not measure something.** A missing
`gitleaks.json` is not a clean secret scan; a crashed `bandit` is not zero
findings; a jscpd report that never got written is not zero duplication. Every
one of those is an error that exits 1. All three used to be written the other
way round, and each one silently disabled the check it was reporting on.

## Supported

| Language | Tools |
|---|---|
| Python | coverage.py, ruff (lint + complexity), mypy, bandit |
| JavaScript / TypeScript | vitest, biome, tsc, npm audit |
| Kotlin / Android | Kover, detekt, Android Lint, kotlinc (via `stage`) |
| Repo-wide | jscpd, gitleaks, pip-audit, npm audit |

## Developing

```bash
uv venv && uv pip install -e '.[dev]'
.venv/bin/python -m pytest        # includes the equivalence suite
.venv/bin/python -m mypy          # --strict, src and tests
```

The suite runs the implementations this package replaces side by side with this
one over identical inputs and asserts their stdout, stderr, exit code and
written files match exactly. That is what lets a repo switch over without its
gate changing verdict on the day it does. Every intended departure is listed in
[DIVERGENCES.md](DIVERGENCES.md); anything else is a bug.

To check against a repo's real reports rather than synthetic ones:

```bash
scripts/verify-against-repo.sh ../../trainwithme/nasa nasa-server
```

Design notes live in the Quality Gate project on mddocs.
