9 min read
EP01: Product Quality vs Code Quality · All The Legible Repo Episodes →
This series is about the quality layer your CI can’t see. Each episode takes one failure that linters, scanners, and test suites never catch, shows the incident that proves it, and ends with one command you can run today. This opener names the problem — and introduces the gate that measures it.
Table of Contents
- TL;DR
- Quick Check: What Grade Is Your Repo Right Now
- The 500 Nobody Reported
- Where Product Quality Sits (and Why Linters Can’t See It)
- The Questions That Decide Whether a Stranger Stays
- How the Gate Works: Scorecard Plus Cold-Start
- What This Means for Your Repos Right Now
- ⚠ Production Gotchas
- Quick Reference
- Framework Alignment
- Key Takeaways
- What’s Next
TL;DR
- Product quality vs code quality is the gap between “the tests pass” and “a stranger can actually use this” — and no linter measures it.
- The costliest failures are silent: the person who hits friction in the first ten minutes never files an issue. They close the tab.
- Legibility is checkable: README length, copy-paste quickstart,
.env.example, actionable errors, a published artifact that still installs today. - Invigil (Apache-2.0) mechanizes ~35 of these checks into gate levels G1–G7 with a letter grade — and prints the exact fix for every failure.
- It grades itself in CI: a pull request that lowers Invigil’s own score doesn’t merge.
Quick Check: What Grade Is Your Repo Right Now
Before the story, the evidence. Two commands, two minutes, on any repo you maintain:
pip install invigil
invigil score . --offline
Sample output, annotated:
Invigil — myproject
Gate G2 · Grade C+ · 19/27 (70%) ← gate = maturity rung, grade = weighted score
FAIL [G1] README is a landing page (≤300 lines) (effort: minutes)
fix: move deep-dive sections to docs/; keep quickstart + pitch
FAIL [G1] .env.example documents every config var (effort: minutes)
fix: create .env.example listing each var with purpose + default
FAIL [G2] Errors carry a correlation ID (effort: hours)
fix: add a global exception handler returning {"error_id": ...}
Every failing line names the check, the effort class, and the exact fix. However you feel about the individual opinions, notice what just happened: nothing in your existing CI produces this view.
The 500 Nobody Reported
The day I renamed a package and pushed the new wheel, every UI page it served returned a 500. The commit message said “verified.” I had verified the import — not the experience. No test caught it, because the tests ran against my source tree, not against the artifact a stranger downloads. And no user caught it for me. The first stranger who hit that 500 did what strangers do: closed the tab and never came back.
That is the failure mode that should keep maintainers up at night. Absence of complaints is not absence of problems. Silence is the loudest negative signal a project gets.
A clean-virtualenv install from an empty directory found the bug in minutes. That habit — being your own first angry user — became a doctrine. Later, the doctrine became a CI gate called Invigil, because habits don’t run nightly and machines do.
Where Product Quality Sits (and Why Linters Can’t See It)
┌─────────────────────────────────────────┐
│ WHAT YOUR CI CHECKS TODAY │
│ ruff / eslint → code style │
│ pytest / jest → source behavior │
│ Trivy / Dependabot → CVEs, deps │
│ Scorecard → supply chain │
└────────────────┬────────────────────────┘
│ all green ✅
▼
┌─────────────────────────────────────────┐
│ WHAT THE STRANGER MEETS │
│ README (landing page or wall of text?) │
│ Quickstart (works from empty dir?) │
│ Published artifact (installs TODAY?) │
│ First error (fix included or trace?) │
│ llms.txt / AGENTS.md (agent-readable?) │
└─────────────────────────────────────────┘
nothing above checks this layer
The product quality vs code quality distinction is exactly this diagram. As a result, a repo can be immaculate in the top box and unusable in the bottom one — green CI, linted code, zero CVEs, and a quickstart that fails on the first copy-paste. In contrast to code quality, product quality has no reflexive tooling. Every good maintainer checks these things by hand, occasionally, when they remember. Nobody’s CI does it on every pull request.
I build hardened infrastructure for a living, and the same lesson repeats there: a standard that isn’t enforced mechanically is a wish. That’s why Linux hardening as code beats hardening runbooks — and it’s why legibility needs a gate, not a checklist.
The Questions That Decide Whether a Stranger Stays
Specifically, the gate asks the questions your CI never asks:
- Can someone get from “found the repo” to “it worked on my machine” in ten minutes?
- When something fails, does the error include the fix — or a traceback?
- Is the README a landing page, or 600 lines of accumulated documentation?
- Does the artifact you published still install today, after your dependencies drifted?
- Is there an
.env.example, or do users reverse-engineer your config from source? - Can an AI agent — now often the first reader — parse your
llms.txtandAGENTS.mdwithout hitting stale paths or a leaked key?
Each question maps to a mechanical check. Together, ~35 checks roll up into gate levels G1–G7 — a maturity ladder, not a binary pass/fail — plus a weighted letter grade. A repo reaches gate Gn only when every mandatory check at or below n passes.
How the Gate Works: Scorecard Plus Cold-Start
Layer 1 — the scorecard (every PR, seconds)
The static layer inspects the repo and its metadata: LICENSE, README length, quickstart shape, tracked secrets, SHA-pinned actions, enforced lockfile, coverage floor, docs index, llms.txt/AGENTS.md hygiene, and more. It runs offline in a pre-commit hook in roughly 120 ms, because a gate that adds friction is a gate that gets uninstalled.
invigil score . --format markdown # PR-comment-ready table
Layer 2 — the cold-start gate (nightly)
This is the layer that would have caught my 500. Instead of testing the source tree, it boots the published artifact — the wheel on PyPI, the image on GHCR — on a clean runner and probes its surface within a ten-minute budget:
# .invigil.yml
artifacts:
- { type: pypi, name: "myapp[all]" }
- { type: ghcr, image: ghcr.io/me/myapp:latest, port: 8000 }
probes:
- { url: "/", expect_status: 200 }
Because it installs from the real registry into a real empty environment, it catches the class of bug where CI passes but the shipped thing is broken: the missing template directory, the config default pointing at localhost, the dependency that resolved differently after an upstream release.
What This Means for Your Repos Right Now
Start in report-only mode. The progressive profile scores everything and gates nothing — you get the visibility without a wall of red blocking your next merge. Flip to enforce once the grade stabilizes, the same way you’d introduce any merge check.
The doctrine is opinionated, and that’s deliberate — but the gate bends instead of breaking. Profiles (strict | progressive | light), per-check weights, and optional flags let a team disagree with a specific opinion without forking the tool. Additionally, network-dependent checks that time out become SKIPs excluded from the grade — never a false downgrade that erodes trust in the number.
One more thing, because trust matters for a tool that grades others: Invigil grades itself in CI. A pull request that lowers its own score won’t merge. The gate passes its own gate — currently G5, grade A+.
⚠ Production Gotchas
Enforcing on day one. Turning on enforce: true before the team has seen the report produces a wall of failures and an uninstall. What breaks: adoption. How to detect it: grumbling in your PR comments. The fix: progressive first, enforce after two weeks of stable grades.
Treating the grade as the goal. The grade is a proxy for a stranger’s first ten minutes. Gaming it (a hollow .env.example, a README split that hides the quickstart) passes the check and still loses the user. The fix is cultural, not mechanical — review the fix, not just the score delta.
Skipping the cold-start layer because “CI already tests installs.” CI installs from the source tree with your lockfile present. The stranger installs from the registry into nothing. These diverge silently after any packaging change — that divergence is invisible until you test the published artifact itself.
Quick Reference
| Command | What it does |
|---|---|
invigil score . |
Full scorecard: gate, grade, exact fix per failure |
invigil score . --offline |
Fast local checks only (~120 ms class) |
invigil score . --format markdown |
PR-comment / job-summary table |
invigil evaluate . |
Alias of score — the verb agents reach for |
invigil portfolio p1 p2 --update FILE.md |
Grade many repos, update a tracked table |
| GitHub Action | uses: invigil/invigil@v1 — report-only by default |
Framework Alignment
| CISSP Domain | Relevance |
|---|---|
| Domain 8 — Software Development Security | The gate enforces secure-SDLC hygiene (no tracked secrets, least-privilege config via .env.example, SHA-pinned actions, enforced lockfile) as a merge condition rather than a wiki page. |
| Domain 7 — Security Operations | Signed releases, SBOM, and the nightly published-artifact check operationalize artifact integrity — continuous evidence instead of a pre-audit scramble. |
| Domain 1 — Security & Risk Management | Profiles and weighted gates turn a subjective quality bar into a measurable control with a defined threshold — governance expressed in code. |
Key Takeaways
- Product quality and code quality are different layers; your CI only watches one of them.
- Test the experience, not the import — the developer’s machine is a lie, and so is the source tree.
- Silence is data: the users you never hear from are the ones who hit the friction.
- A quality bar you can’t measure is an opinion; a gate you bypass is dead weight — make it fast, bendable, and report-only by default.
- Trust tools that hold themselves to their own standard: Invigil’s own PRs merge only if its self-grade holds.
- Defaults are never neutral — the same reason cloud AMI security risks demand custom images applies to your repo’s out-of-the-box experience.
What’s Next
EP02 goes deep on the layer that caught my 500: testing the published artifact, not the source tree. Clean-runner boots, real-registry installs, probe budgets — and why “it works in CI” is a statement about your lockfile, not your users. EP02: How to Test Your Published PyPI Package — Before a Stranger Does.
Invigil is Apache-2.0 and built in the open. If this episode named a failure you’ve shipped (we all have), there are more checks waiting to be written — good-first-issues with acceptance criteria at github.com/invigil/invigil. Pick one, or open a Discussion and say hello. First-time contributors get fast reviews and release-notes credit.
Get EP02 in your inbox when it publishes → subscribe