## What #3019 actually changed, pinned — and a correction to my own
claim
I described #3019 as closing a hole where an operator could re-route a
running task on a renamed board. **That was wrong.**
`TaskStore.updateTask` runs the same guard with its own resolved lanes
(`resolveNodeOverrideLanes`) and throws, so the change was refused
either way.
This test is how I found out: I wrote it to cover #3019's wiring and it
passed against a tree with that wiring removed. A test that passes with
the change reverted is not a test, so I went looking for what was really
refusing — and it was the store.
## But the two paths *are* distinguishable, which my correction then got
wrong in the other direction
In correcting myself on #3019 I said the paths were externally
indistinguishable and no test could separate them. Also wrong. Measured
both ways:
| | `details.error` |
| --- | --- |
| pre-check fires (wired) | `"task-in-progress"` — machine-readable
reason code |
| pre-check misses (unwired) | `"Cannot change node override for KB-001
while it is in progress…"` — the store's thrown prose |
So on a **legacy** board a caller could branch on `task-in-progress`; on
a **renamed** board it silently got a sentence instead. That is a real
API inconsistency, visible only to whoever was parsing it — the kind of
thing nobody notices until it breaks.
That is what these cases pin, and it is the honest description of
#3019's value: an error-contract fix, not a security fix.
## Revert proof
With #3019's wiring removed:
```
Expected: "task-in-progress"
Received: "Cannot change node override for KB-001 while it is in progress. …"
Tests 1 failed | 1 passed (2)
```
Verified by actually reverting, not by reading the source — which is the
discipline that caught both of my wrong claims above.
The paired case ("still allows the override once the card leaves that
wip lane") passes both ways by design; it guards against over-refusal,
so I am not counting it as coverage of the contract.
## Also closes the gap I named in #3019
That PR shipped with `check-lane-wiring` as its only regression proof,
and I said a behavioural test was owed. The two are complementary and
fail for different reasons: **the ratchet** fails if the argument stops
being passed; **this** fails if it is passed and the contract still
degrades.
## Verification (measured)
- **2 passed / 0 failed**
- `tsc --noEmit` clean; `eslint` clean (one pre-existing warning, no
errors)
- `check-fnxc-future-dates`, `lifecycle-column-census --strict`,
`check-lane-wiring` — green
Tests only; no product file touched. No changeset.
## Note on the harness, for whoever writes the next one of these
Seeding a card into a renamed lane has two traps, both inherited from
`merge-blocker-renamed-review-lane.test.ts` and both recorded in this
file's header: the real API is `createWorkflowDefinition` +
`selectTaskWorkflow` (the plausible `saveWorkflowDefinition?.()` does
not exist and the optional call swallows it silently), and moving a card
takes `moveTask`, not `updateTask({ column })`. Both are guarded here by
asserting the card really is in `building` before the subject runs.