Generated from
cat-harness/skills/sdlc/sdlc-core/generalise-the-fix.md— do not edit here.
Generalise the fix, then attack the generalisation
A fix that repairs one instance and leaves its siblings is half a fix. A fix widened past the problem it solves is the next defect. This skill is the pass between those two, and it is two moves in order — generalise, then adversary — because the adversary has no subject until a generalisation exists.
Run it when the fix works and before the pull request. Not while debugging: this asks what the defect was, which you cannot answer until you know.
Move 1 — what class is this?
Three questions, in order. The first is the one that is usually skipped.
1.1 What was the defect, as opposed to the symptom?
The symptom is where you noticed it. The defect is the rule that was wrong. They are rarely the same, and the fix belongs at the defect.
vq8g, 2026-09-23. Symptom: two beans reported as having no options. Defect: the counter recognised one markdown form — top-level list items — while MADR, the authority, is format-agnostic. The corpus was fine; the detector had invented a requirement. Fixing the two beans would have left the rule free to misjudge the next one.
The tell that you are fixing a symptom: the fix names a specific instance.
j6t3 and xgd8 in a rule, document-ingestion.html in a selector, one
filename in a condition.
1.2 What else has this shape?
Look, do not guess. The corpus answers this and your memory does not.
vq8gagain. A twelve-bean sweep over the store found ten using list items, two using bold enumerations — and one,dhvf, using subheadings and being over-counted ten for four options. The sweep found a second error direction nobody had reported, and it changed the design: three forms, most structured wins, never summed.
A fix designed from one example handles one example. A fix designed from the sweep handles the class, and the sweep is usually one command.
1.2a A sweep must report its own coverage
The sweep in 1.2 is a measurement, so it answers to the same rule every other measurement here does: say what you covered, or a silent under-match reads as a clean corpus.
Measured 2026-09-23, sweeping the health checks for more findings whose action could not clear them. The first pass regex-matched
action:followed by string literals and reported 6 actions, all sound. The file has 15. The nine it missed included a ternary, and the pass would have concluded the class was closed after seeing 40 % of it — while looking for exactly that kind of blind spot.The fix is one line of output:
parsed 15 of 15. The count of subjects found against the count that exist, printed beside the verdict.
So a corpus sweep prints the denominator. grep -c for the crude marker, parse
for the real one, and show both — if they disagree, the parse is the thing to
fix before reading anything into the result. This is
rendered-verification’s “assert the conditions”
rule with the browser taken out of it: a check that cannot tell an empty walk
from a clean one is not a check.
1.3 Is the fix at the right layer?
Walk outward from the symptom until you reach the thing that is actually wrong, and stop there. Both directions are failures: too deep and the siblings stay broken, too high and see Move 2.
tcq2. The search field was 44 % of its panel. Our rules saidwidth: 100 %andflex: 1 1 autoand they were all correct — walking the box chain found the theme’s own.search-input-wrapatmax-width: 536px, two layers above where the change felt like it belonged.
Move 2 — the adversary
Now attack what you just wrote. One question, and answer it in writing:
What does this fix now apply to that the defect did not?
If the honest answer is “nothing”, say so and move on — the pass is cheap when it passes. If it is anything else, that is the list to justify or narrow.
The two axes, and they go wrong independently
Most over-generalisation is one of these. vfr8 was both at once.
| axis | the question | widened past the problem looks like |
|---|---|---|
| SCOPE | which things does it apply to? | the fix names a container when the problem was one child |
| STATE | when does it apply? | the fix is unconditional when the conflict only arises in one state |
vfr8, the worked case. A rule was written so ONE panel could paint over a full-bleed figure. It was applied to the whole sidebar (scope) in every state (state), as:root.fa-has-fullwidth .side-bar { z-index: auto }. At specificity (0,3,0) it beat the open-nav rule’s (0,2,0) always — so on every page with a wide figure the navigation opened behind the page content, which the open-nav rule’s own comment had predicted in as many words.Neither axis needed widening. The panel is
position: fixed; only the panel was in conflict; and only while a figure was expanded.
Four tells, all cheap to check
- The fix names a container, the bug named a child.
.side-barwhen the problem was.fa-qr-panel. - The fix is unconditional and the conflict is not. Ask when the defect can occur. If the fix is broader than that window, narrow it or say why not.
- You summed where you should have chosen. Recognising three forms is
right; adding their counts gave
dhvf14 options where it has 4. When alternatives are alternatives, take the most specific present — do not add. - The label is doing work the relation should do.
thuxkeys onparentrather thantype: epic, becausetypeis a label a bean sets about itself whileparentis a fact another bean asserts about it. Keying on the label would also have forced a ruling on whatfeaturemeans.
Keep the discrimination the check exists for
A generalisation that excuses everything has deleted the check. Name the case that must STILL fire, and test it.
thuxexcuses a claim whose child moved recently. The case that must still fire: a parent all of whose children are also quiet.bzyuis the worked one — 8 open children, none moving — and it has a test of its own. Without it the change would have excused every parent.
The strongest outcome is when the adversary reverses you
Expect to find a trade, and measure it rather than reasoning about it. Twice today the measurement said the opposite of the reasoning.
vfr8. I expectedz-index: 100to fix the nav at the cost of the panel the old rule protected, so I synthesised that panel and measured. Underautoit was not visible at its own centre — covered despite itsz-index: 1200. Under100it was. The old value failed the case it was written for, so the new one was better on both counts and there was no trade to weigh.
An adversarial pass that only ever confirms you is not being run.
Move 3 — falsify the guard
A guard that would not have caught the bug is worth nothing, and you cannot tell by reading it.
Reintroduce the defect, watch the guard fail, restore, watch it pass. Two commands.
vfr8:sedthe value back toauto→ both assertions fail; restore → 25 pass.o5qj: the finding fired before, is silent after, and the adjudicated group is still counted so “never created” and “all resolved” cannot read the same.
If the guard cannot be falsified — because the defect needs a state you cannot produce — say so where the guard lives, and say what does cover it instead.
Where this is NOT a bug fix
The moves are the same; only the vocabulary changes.
- A schema or model change. Scope is which records must now satisfy this;
state is from when. A field made required is a fix applied unconditionally
to every record ever written — the
vfr8shape in a schema. - A detector or gate. Its class is the set of inputs it judges. Sweep the corpus (1.2) before changing the rule, or you will tune it to the two examples in front of you.
- A claim in a paper. The generalisation is the theorem’s hypothesis; the adversary asks which objects now satisfy it that the proof does not cover. The falsification is the counterexample you look for on purpose. Widening a hypothesis past what the argument supports is over-generalisation with a different name.
What this deliberately does not do
It does not ask you to generalise everything. “Nothing else has this shape” is a complete and common answer to 1.2, and one worked example is not a class. The cost of the pass is a few minutes; the cost of inventing a class that is not there is a rule nobody can satisfy.
It does not replace rendered-verification or a
test. It asks whether the fix is aimed correctly. Whether it works is a
different question and comes first.
It does not decide a trade the owner should decide. When narrowing the
generalisation costs something real — tcq2 found the corner magnifier under
the staging banner, where raising it defeats the banner and lowering it costs
the position — report the measurement and the two options rather than picking.
Provenance
Written 2026-09-23 from five fixes in one session, at the owner’s ask: “try to generalize pattern of a fix. adversarty skill, did you genrealize too much… are you picking up unindeneded things.”
vfr8 (both axes widened, adversary reversed the conclusion), vq8g (detector
too narrow, and the sweep found a second error direction), o5qj and thux
(findings no permitted action could clear), tcq2 (the constraint two layers
above where the fix felt like it belonged).