principle-assert-invariants-not-last-bug · git:20260907.14d3ce9 · 2026-09-07 · sha256 6235b3d418733019
principle-assert-invariants-not-last-bug git:20260907.14d3ce9A
Immutable. This exact content never changes and is served at /api/v1/blob/6235b3d418733019.
---
name: principle-assert-invariants-not-last-bug
description: "Apply after a bug ships past review or a passing grade/check: assert the general class of failure instead of patching only the specific instance that was caught. Use on /reflect, whenever a fix's own commit message reads like 'fix X for ticker/case Y', or whenever a fix makes a row disappear instead of shipping it with a status."
disable-model-invocation: true
---
# Assert invariants, not the last bug name
Generalized from a real repo's incident log: the same shape of bug (an
identity/provenance/uniqueness invariant) shipped four separate times
because each fix patched the specific case a judge or reviewer had just
pointed at, not the general rule — so the next differently-shaped instance
of the same class sailed through untouched.
The same log later showed the mirror-image blind spot: every check asked
"did we invent a value?" and none asked "did we drop a row?" — so a batch
of defects that were all *missing* rows passed every grade.
## Why this exists
Patching "the AUR case" doesn't stop a structurally identical bug from
shipping as "the SERV case" next week. Each time a fix targets only the
instance that was caught, the *rule* that would have caught every other
instance never gets written down or enforced — so the class stays real, and
grading/review stays blind to every case except the one already fixed.
A rule also has a shape, and a one-sided rule is a half-written invariant.
"Never invent" without "never silently omit" tells the code the safest
thing to do with a hard row is to skip it — which is exactly how a sell
with no matching cost lot, a table that parsed to zero rows, or a period
grid that stopped at the first source all shipped clean.
## The method
1. **Name the invariant in one line.** Not "AUR shouldn't duplicate" —
"every (period, entity) pair has exactly one row."
2. **Name its omission twin in the next line.** For every "must not
invent / must not fabricate / must not duplicate" rule, write the paired
"must not be missing" rule: "every period the source discloses and every
event the ledger records appears in the output, as a value or as a
status-marked row." A no-invent rule with no no-drop twin is not done.
3. **Encode both in code** — an assert, a coalesce, a refuse-to-ship check,
a completeness count — for entities/cases you haven't seen yet, not just
the one that broke.
4. **Encode both in the relevant skill/doc**, in assert language ("X must
always Y"), not as "remember the AUR incident."
5. **Re-validate** (re-run the grading/review/test suite) before claiming
fixed — a fix isn't proven until the same class of check runs clean
again, not just the one case that originally failed.
**Steps 3 and 4 land together, in the same change.** A principle documented
without a matching code change is invisible drift: the doc says the rule
exists, nothing enforces it, and only a later validation run (if one ever
exercises that exact path) reveals the gap. Never add an invariant to a
skill/doc as a "the code should eventually do this" placeholder — either fix
the code in the same change, or don't document the rule yet.
## Never invent, never silently omit
An unparseable, unpriced, uncovered, or unmatched row still ships. It
carries an explicit status column (`unknown`, `not_disclosed`, `unpriced`,
`unmatched`, `truncated`, …) and a one-line reason — not a fabricated value,
and not an absent row.
- "Empty parse ⇒ drop the row" is a banned rule shape. The allowed shape is
"empty parse ⇒ emit the row with `status=unparsed` and the reason."
- A silent `continue`, early `return`, or `break` on a missing lookup is the
omission form of an escape hatch. It must be named in the skill, and it
must emit a status row rather than nothing.
- A loop that stops at the first source, the first filing, or the first
page must prove it reached the last one; "the grid ended" is not evidence
that the source ended.
- Grading must count omissions as well as fabrications. Every
source-disclosed period and every ledger event (every sell, every filing,
every entity) must appear in the output, or appear as a status-marked row
with a reason. A grader that only asks "is each emitted value real?"
cannot see a row that was never emitted; it also has to ask "is every
expected row present?"
- The status column is data, not a comment. It is queryable, it is graded,
and downstream consumers must handle it explicitly — a status row that a
later step quietly filters out has just moved the drop, not fixed it.
## Anti-patterns
- "We checked the one case that broke" (should be every case of that shape).
- "It looks fine now" (checked the surface, not the invariant).
- "Every emitted value traces to a source, so the output is correct" (a
fabrication check with no completeness check — the missing rows are
invisible to it).
- "The parser returned zero rows, so there was nothing to emit" (zero rows
from a source known to disclose data is an `unparsed` status row per
expected period, not an empty table).
- "No matching prior record, so skip it" (a `continue` that deletes an
event from the ledger; ship it as `unmatched` with the reason).
- "The rubric/reviewer allowed this shortcut" (a shortcut that passes review
by accident is still the escape hatch that will break the next case).
- "Every X does Y" as a blanket rule when the real rule has an exception
class — restating the common case as universal is itself the bug (a
15-instance form-in-instance example: "every exit forces the value to
zero" was true for exits with a real filed value behind them, and false
for identity-only exits that never had one — the blanket version invented
a fabricated number for the second case).
- Adding a code path "for continuity" without naming it in the skill and
either banning it or tightly typing it — an untracked escape hatch that
happens to look rubric-compliant is how the same class recurs.
## No silent escape hatches
If a code path exists "just for continuity" or "just to keep things
looking consistent," it must be named explicitly in the relevant skill
**and** either banned outright or tightly typed. An untracked shortcut that
happens to pass today's checks is exactly the shape that lets the same class
of bug recur under a new name.
The same applies to code paths that exist "just to skip the hard rows."
Skipping is a decision about the output; it has to be visible in the
output as a status row, never only in the control flow.