Four review rounds across `TaskCard.tsx` and `TaskDetailModal.tsx` each
found a **real defect**. None was in the conversion itself — every one
came from the same property change. The fleet has ~600 guards left to
convert against the same helpers, so this is written down rather than
left in four commit messages.
## The property that changes
```ts
task.column === "in-progress" // stable for the lifetime of the render tree
isWipColumn // derived from fetched trait flags — CHANGES after first paint
```
Column trait flags arrive from a board-workflows fetch. Until it lands
they are `undefined` and every role helper falls back to the legacy id.
So a converted role is `false`, then `true`, within one mounted
component.
## The four forms
| | form | symptom |
|---|---|---|
| 1 | **stale memo** — deps still keyed only on `task.column` | timers,
labels, completion dates frozen at first-paint values (4 instances in
TaskCard) |
| 2 | **frozen `useState` initializer** | the section does not start
collapsed — it *appears later, already collapsed*, on a card nobody
touched |
| 3 | **eager action on a guess** — effect mutates state before flags
resolve | a tab opens and instantly bounces; the correction never lands
because the action destroyed the state it would have corrected |
| 4 | **stale identity** — flags resolved, but for the *previous* entity
| roles resolve from another task's workflow: confidently wrong rather
than merely stale |
**Form 4 defeats the obvious fix for form 3.** A `metadata === null`
guard asks whether data *loaded*, not whether it describes the entity
currently open — and it only appears in components that stay **mounted
across entity changes**, which is why TaskCard never showed it and the
modal did.
## Why a doc rather than four commit messages
All four ship **green**: types pass, existing tests pass, and the
**default board behaves identically** — because on the default lineage
the legacy fallback and the resolved role agree. They diverge only on a
**renamed board**, which is precisely the case the conversion exists to
support.
So the failure mode is: census count reaches zero, everything is green,
and the feature is broken exactly where the programme was meant to fix
it. A reviewer catching these one at a time is the expensive path, and
it has now cost four rounds on two files.
Also relevant: **this repo has no `react-hooks/exhaustive-deps` rule**,
so form 1 has no automated backstop at all.
## Contents
A checklist a converter can run against a component file, and the
concrete fix shape for each form — including tagging fetched metadata
with the id it describes, and applying that guard to the **role
bindings** rather than only the effects (reordering effects fixes the
call sites you noticed and leaves the bindings stale for everything
else).
Follows the convention already established by the engine-side scoping
note in `architecture-patterns/fleet-self-healing-cluster-scoping.md`,
which records the equivalent hazard for sync workflow reads.
## Verification
`pnpm test:gate` green (10 / 158 / 487 / 71). `pnpm
check:lifecycle-columns` exits 0. `pnpm lint` clean.
Docs-only; no changeset.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>