I once wrote the most rigorous “no” of my life, and it was wrong by lunchtime.
The ask was small. An assistant bot I run renders an animated avatar in a web front end, and I wanted the avatar to occasionally make a fist and release it. Trivial-sounding. So I went looking, and I did the work properly — traced through the shipped bundle rather than guessing, and wrote down what I found:
| route | verdict |
|---|---|
| replace the idle animation clip | closed — the clip URL is a module constant, no setting, no store |
| register a per-frame hook | closed — the hook exists, but nothing outside its own package may register one |
| use the spring-bone physics | closed — passive only, and the wrists move 1.5–4.4° |
| set a resting pose | done, and static |
Four routes, four verdicts, each one traced to a line of code. I wrote blocked upstream in the notes, which is a deliberately stronger claim than “not tried”, and I meant it as an act of honesty about the difference.
Two more pieces of evidence arrived later and hardened the table. The idle animation clip turned out to contain 28 channels and not a single finger bone, so even a replaceable clip would not have helped.
And the capability shipped that afternoon.
What the list actually proved
Read the four rows again and notice what they have in common. Replace the clip. Hook the frame loop. Use spring physics. Change the rest pose. Every single one of them is a way to animate a bone.
So the table did not establish “a fist is impossible”. It established “bone animation is unreachable from here” — a much narrower and much less interesting claim, wearing the first claim’s clothes.
The fist shipped as a morph target. Bake the curled-finger vertex positions into the mesh, register them under a name, and drive that name through the expression API the host already calls every time the avatar changes mood. No bone moves. Nothing upstream had to change. Nothing was monkey-patched that an update could silently revert. It took an afternoon.
Why the enumeration felt complete
Because it was complete. Over the wrong set.
The investigation started from the question how do we animate her fingers, and every step after that was a faithful, careful search of that space. This is the part I want working engineers to sit with: rigour applied inside a fixed frame is indistinguishable, from the inside, from rigour applied to the problem. The evidence is real. The verdicts are sound. The document is honest. The conclusion is wrong. There is no sloppiness anywhere in the chain to catch.
And being well-evidenced made it harder to revisit, not easier. A table with four traced rows looks like a closed question. A note that said “haven’t looked yet” would have been re-opened in a week.
The signature to learn is this: a list of routes that differ in detail but agree in mechanism. Four ways to do one thing is one data point, not four.
Three questions before you write “blocked”
Do these routes differ in mechanism, or only in detail? Line the routes up and try to describe them in one sentence. If you can — “these are all ways to animate a bone”, “these are all ways to get a webhook past the firewall”, “these are all ways to make the ORM emit that query” — you have enumerated one mechanism, and you have not yet enumerated the problem.
State the capability without naming a mechanism. This is the highest-leverage move, and it costs one sentence. “Her hand changes shape on demand” admits morph targets, shader deformation, swapping the mesh, a pre-rendered sprite overlay. “Her finger bones rotate” admits exactly one family and quietly excludes every alternative before the search begins. The mechanism-free phrasing is the one you should be searching from. Almost every “blocked” I have written since has failed this test.
What does the host already drive successfully? Any live, working path into the same subsystem is a candidate carrier for your feature. This is the cheapest of the three and it is what solved my case: the expression API was already being called, already reaching the avatar, already documented — in my own notes, two pages away, under a different heading, for an unrelated feature. I had written the answer down myself and filed it where I wasn’t looking.
The corollary about status words
There is a real temptation, reading this, to conclude that you should soften your language — write “not currently reachable” instead of “blocked”, hedge everything, never close anything. That is the wrong lesson and it makes teams worse. Strong status claims are load-bearing; a backlog where nothing is ever closed is a backlog nobody reads.
The right lesson is the opposite, and it is uncomfortable: a strong claim earns more scrutiny, not less. The evidence that makes “blocked upstream” honest is exactly the evidence that will stop anyone re-opening it. So the scrutiny has to happen before the word is written, at the moment you feel most justified in writing it — because that is the last moment anyone will look.
Its mirror image is worth naming too, because I have shipped that one as well: an open mechanism mistaken for a reachable capability. “The API exposes it” is not “we can do it” any more than “the bones are locked” is “it is impossible”. Both errors come from treating the mechanism and the capability as the same thing.
The rule: before you write “blocked”, restate the capability without naming a mechanism — and check whether your routes differ in mechanism or only in detail.
sources: concepts/blocked-can-be-the-wrong-question