Small teams cannot afford a dedicated QA team. They cannot afford not to have consistent quality either. The Ratchet Principle is how those two facts coexist.
Most code-quality philosophies assume there will be a moment, later, when you go back and clean things up. There won't be. There is no later. Later is a fiction we tell ourselves to feel less bad about the present commit.
The honest model is this: the codebase you have right now is the codebase you will have forever, plus or minus the changes you make today. So the question is not "will I clean this up later?" but "is today's commit moving the system forward or backward?"
The Ratchet Principle says: every commit moves the system forward. If a commit cannot defend itself as forward motion — if it adds dead code, breaks a test, ships a regression, leaves a TODO that you will not actually do — it does not land.
The wrong mental model is the pendulum: quality goes up when we have time, down when we don't, and we hope the average is OK. Under the pendulum, every "quality sprint" is undone by the next "ship sprint." The codebase oscillates around an average that drifts down over time because human attention drifts down over time.
The right mental model is the ratchet: quality goes up, or it stays the same. It does not go down. The mechanism that enforces this is not willpower — it is CI. The teeth of the ratchet are tests, types, lint rules, coverage gates, performance budgets. Each tooth, once installed, holds.
.github/workflows/quality.ymlA starting point you can paste into a real repo today. Adjust the language-specific commands to match your stack:
name: quality
on:
push:
branches: [main]
pull_request:
jobs:
ratchet:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
# 1. Lint — no inline disables, no warnings.
- run: npm run lint -- --max-warnings=0
# 2. Type check — full strict mode.
- run: npm run typecheck
# 3. Tests — all must pass. No skipped tests.
- run: npm test -- --ci --reporters=default
# 4. Coverage gate — must not regress.
- run: npm run coverage
- name: Enforce coverage floor
run: |
COVERAGE=$(jq '.total.lines.pct' coverage/coverage-summary.json)
FLOOR=80
echo "Line coverage: $COVERAGE% (floor: $FLOOR%)"
awk "BEGIN { exit !($COVERAGE >= $FLOOR) }"
# 5. No commented-out code blocks (heuristic).
- name: Reject commented-out code
run: |
if git diff origin/main...HEAD -- '*.ts' '*.tsx' '*.js' \
| grep -E '^\+\s*//.*[{};]' ; then
echo "::error::Commented-out code detected. Delete it; git remembers."
exit 1
fi
# 6. No TODO/FIXME without an issue link.
- name: Reject undocumented TODOs
run: |
if git diff origin/main...HEAD \
| grep -E '^\+.*\b(TODO|FIXME)\b' \
| grep -vE '#[0-9]+' ; then
echo "::error::TODO/FIXME without an issue link."
exit 1
fi
The point of the workflow above is not the specific commands — it is that each rule is a ratchet tooth. Once installed, it holds. Future commits cannot regress past it without an explicit decision (changing the workflow itself), which is exactly the kind of decision that should be visible in a PR.
What makes this work is taking quality enforcement out of your head. As a solo dev or small team, your willpower is a finite, precious resource. Spending it on "should I write the test, even though I'm tired?" is wasteful. Spend it once, on building the ratchet. After that, the system enforces itself, every commit, forever.
The ratchet is a way to outsource your discipline to your build system. That is not a workaround — it is the entire point.
The full SSD methodology builds on this principle.
Read SSD