Second live defect from the same sweep as #2996 — memoized hooks reading
a lane value absent from their dependency list. **13 hits, triaged by
hand, 2 real.**
## The defect
`resolveNearDuplicateCanonicalInactive` decides whether a card's
*"duplicate of X"* chip is hidden because the canonical is finished. It
calls `getTaskColumnFlags`, whose identity changes when the board's
workflow traits arrive — while its own dependency list was `[allTasks]`
alone. So it kept the closure created during the **pre-load** render,
over an empty trait map.
With no traits the role helpers fall back to legacy ids, so a canonical
sitting in a renamed complete lane reads as still **active**, and the
chip stays up advertising a duplicate of work that has shipped.
## Severity, stated honestly
The closure is rebuilt whenever `allTasks` changes identity, which any
task-list refresh does. So this is a **bounded window**, not a permanent
wrong answer — unlike #2996, where the dependency that would have
refreshed it (`task.column`) never changes. On a quiet board the window
is the gap until the next update.
I'd rather say that plainly than let it read as equally severe because
it's in the same family.
## The hoist is required by the fix, not tidying
`getTaskColumnFlags` sat *after* this callback, with a note explaining
that the body only runs during render so the const is initialised by
then. That's true of the **body** and false of the **dependency array**,
which evaluates eagerly — so the reference could not be listed at all
until the declaration moved.
The existing note reasoned carefully about declaration order and said
nothing about staleness, which is exactly how it read as considered.
Both hoisted callbacks close over props only, so the move carries no
behaviour.
## Measured
| check | result |
|---|---|
| test written first | red for the right reason — arrival case `expected
false to be true`, negative passed |
| after the fix | 2 passed |
| **dropping the dep while keeping the hoist** | arrival case fails
again |
| `Column.test` + new suite | **87 tests green** |
| gates | census + FNXC green; lint and `tsc` clean |
That third row is the one that matters: it isolates the test as
load-bearing on the **dependency**, not on the code move that had to
accompany it.
The observable is the prop `Column` computes, not the chip markup —
`Column` is the producer here, and asserting on `TaskCard`'s rendering
would test the consumer of a value this component gets wrong.
## The negative case
Re-resolving must not degrade into "every canonical is inactive". A
canonical still in a live lane keeps its chip, or the fix silently hides
**real** duplicate warnings — worse than a stale one, because then
nothing points at the collision at all.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>