docs(workflow-learnings): a sixth shape — the resolved value arrives after a memo has answered (#2998)
## The shape Three defects this session, all the same, none visible to any instrument here: A lane value resolved **asynchronously** (the board fetches workflow traits after first paint) is read inside a `useMemo`/`useCallback` whose dependency list omits it. The first computation runs with the flags `undefined`, the role helpers correctly fall back to legacy ids, and on a **renamed** board that answer is wrong. When the flags arrive nothing in the dep list changed, so the memo never recomputes. | defect | severity | |---|---| | blocker fan-out trait index (#2993) | permanent — empty index for the mount | | card live elapsed-time indicator (#2996) | permanent — never subscribes | | near-duplicate chip (#2997) | bounded — self-heals on the next task refresh | A legacy board hides all three: there the fallback already answers correctly on the first paint, so the stale list costs nothing. **Every instance is renamed-board-only**, which is why they accumulated — and this repo has no `react-hooks/exhaustive-deps` rule, so the class is invisible to lint. ## Two properties decide severity, both readable off the dep list 1. **Does any dependency refresh quickly?** `allTasks`, a live clock, a task identity — any of them rebuilds the closure on the next update, making the wrong answer a bounded window. The chip keys on `allTasks` and recovers; the indicator keys on `task.column`, which never changes, so it never does. 2. **Is the value covered transitively?** A dependency that itself lists the flags gets a new identity when they arrive, and that propagates. ## A gate was built and rejected — the part worth writing down The scanner reports **19 sites; two were real.** Property 2 is why: transitive coverage is invisible to any purely syntactic check and would need a real dependency graph. `TaskCard`'s context-menu memo omits all three role flags and is **nonetheless correct** — it depends on `taskActionMenuModel.actions`, and that model lists `taskColumnFlags`, so the whole chain recomputes. I checked that before filing it, which is the only reason this PR isn't a bug report about missing Archive/Revert menu entries. Freezing 19 would have baselined mostly noise and trained everyone to skip the report — the exact failure this document already records for `sortTasksForDisplayColumn`, where an annotation saying "ignore these" hid a real defect for days. **A good investigative tool is not automatically a good ratchet**, and the next person deserves to know the turn was considered rather than missed. The triage that does work is cheap: run the scan, then ask the two questions above. Nine of nineteen survive question 1; hand-checking those is an afternoon, not a project. Docs only — no code, no baselines. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -273,6 +273,45 @@ What actually finds these:
|
||||
|
||||
A false positive is loud and gets fixed. A false negative prints a baseline and reads as coverage.
|
||||
|
||||
### A sixth shape: the resolved value arrives, and a memo has already answered
|
||||
|
||||
Three defects in one session, all the same shape and none visible to any instrument here.
|
||||
|
||||
A lane value resolved asynchronously — the board fetches workflow traits after first paint — is read
|
||||
inside a `useMemo`/`useCallback` whose dependency list omits it. The first computation runs with the
|
||||
flags `undefined`, the role helpers correctly fall back to the legacy ids, and on a RENAMED board
|
||||
that answer is wrong. When the flags arrive nothing in the dependency list has changed, so the memo
|
||||
never recomputes and the pre-load answer stands.
|
||||
|
||||
A legacy board hides it completely: there the fallback already answers correctly on the first paint,
|
||||
so the stale list costs nothing. **Every instance is renamed-board-only**, which is why they
|
||||
accumulated. This repo also has no `react-hooks/exhaustive-deps` rule, so the class is invisible to
|
||||
lint, and a disable directive for that rule fails CI.
|
||||
|
||||
Found: the blocker fan-out map (empty trait index, permanent), the card's live elapsed-time indicator
|
||||
(never subscribed, permanent), and the near-duplicate chip (stale closure, bounded).
|
||||
|
||||
**Two properties decide severity, and both are checkable by reading the dependency list:**
|
||||
|
||||
1. **Does any dependency refresh quickly?** A live clock, `allTasks`, a task identity — any of these
|
||||
rebuilds the closure on the next update, so the wrong answer is a bounded window rather than
|
||||
permanent. The near-duplicate chip keys on `allTasks` and self-heals on the next task refresh; the
|
||||
time indicator keys on `task.column`, which never changes, so it never recovers.
|
||||
2. **Is the value covered TRANSITIVELY?** A dependency that itself lists the flags gets a new
|
||||
identity when they arrive, which propagates. `TaskCard`'s context-menu memo omits all three role
|
||||
flags and is nonetheless correct, because it depends on `taskActionMenuModel.actions` and that
|
||||
model lists `taskColumnFlags`. Reading the dependency list alone reports this as a defect.
|
||||
|
||||
**A gate for this was built and rejected.** A scanner for "memoized hook reads a lane value absent
|
||||
from its deps" reports 19 sites; two were real. Property 2 is the reason — transitive coverage is
|
||||
invisible to any purely syntactic check, and would need a real dependency graph to resolve. Freezing
|
||||
19 would have baselined mostly noise and trained everyone to skip the report, which is the failure
|
||||
this document already records for `sortTasksForDisplayColumn`. The scanner is a good investigative
|
||||
tool and a bad ratchet; the distinction is worth keeping.
|
||||
|
||||
The triage that does work is cheap: run the scan, then for each hit ask the two questions above. Nine
|
||||
of the nineteen survive question 1; hand-checking those is an afternoon, not a project.
|
||||
|
||||
### The probe harness lies more often than the gate does
|
||||
|
||||
Probing four gates with unimagined shapes in one session produced **two rounds of silently invalid
|
||||
|
||||
Reference in New Issue
Block a user