docs(learnings): a seventh inert shape — reusing an already-resolved local (my #3114 turned main red) (#3130)

Records the failure shape behind #3126, from the person who caused it.

## What is new about it

The three inert conversions this program had catalogued — #3051, #3062,
#3068 — all called `resolveTaskWorkflowIrSync` **at the call site**,
where the sync resolver is visible in the diff. That is what the
existing entries describe, and it is why they read as avoidable.

Mine was not that. #3114 converted a `triage.ts` arm to
`disposeLanes.wip`, reusing a value `resolvePlannerLanes` had produced a
few lines above. It merged, and `main` went red: `triage.ts: 7 -> 8`.

My reasoning at the time, verbatim from the PR body:

> `resolvePlannerLanes` already called immediately above — no new
resolution/await.

That sentence checks the **cost** question and skips the **correctness**
one. I confirmed I was not adding an `await` to a synchronous listener —
the usual blocker, and a real one — and never asked what kind of
resolver had produced the local I was reusing.

**Reusing an already-resolved value reads as strictly safer than
resolving.** No new work, no new await, no new failure mode. That
intuition is correct about cost and silent about correctness, and the
sync-ness sits one hop away inside the helper, where a call-site
reviewer does not see it.

So the check is not *"am I calling a sync resolver here?"* but **"what
produced every lane value I am about to compare against,
transitively?"** A local is not evidence; the resolver behind it is.
#3122 widened the gate to follow wrappers for precisely this reason —
and I walked through the door it was widened to cover, during the same
phase I was adding it.

## Two corollaries recorded with it

1. **A gate that catches the defect but does not block is a report.**
`check-inert-sync-lanes` fired correctly and the PR merged anyway,
because it is not in the blocking set. #3127 fixes that, and I would
prioritise it over any individual conversion — this is the second time
this phase a correct non-blocking signal was ignored.
2. **`triage.ts`'s remaining 7 are not backlog.** The revert takes it to
7, and those seven are the same shape: they need the emitter-side /
async-threading work tracked in #3082, not another conversion pass.
`--claims` (#3124) now marks sync-resolver files as inert-risk and keeps
them out of the start-here list for exactly this reason.

## Scope

Docs only — one section appended to the existing learnings file, placed
with the other numbered shapes and before "The rule that produced every
fix above". No code, no gate, no changeset (internal docs).

The revert itself is #3126, which I confirmed on a clean detached
`origin/main` checkout rather than on a branch; I did not open a
competing fix.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-31 05:38:20 -07:00
committed by GitHub
parent ad5172afd5
commit 984b3ed0c1

View File

@@ -396,6 +396,38 @@ This matters more than it sounds. A probe that wrongly reports "caught" retires
that wrongly reports "missed" sends you rewriting an instrument that was already correct. The first
nearly shipped a double-counting change to the SQL gate, on evidence that was entirely fictional.
### A seventh shape: reusing an ALREADY-RESOLVED local, without checking what resolved it
The three inert conversions this program had catalogued (#3051, #3062, #3068) all called
`resolveTaskWorkflowIrSync` **at the call site**, where the sync resolver is visible in the diff. The
fourth did not, and it is mine: #3114 converted a `triage.ts` arm to `disposeLanes.wip`, reusing a
value `resolvePlannerLanes` had already produced a few lines above. It landed, and `main` went red on
`check-inert-sync-lanes` (`triage.ts: 7 -> 8`).
My stated reasoning, verbatim: *"`resolvePlannerLanes` already called immediately above — no new
resolution/await."* That sentence checks the cost question and skips the correctness one. I confirmed
I was adding no `await` to a synchronous listener — the usual blocker — and never asked what kind of
resolver had produced the local I was reusing.
**Reusing an existing resolved value reads as strictly safer than resolving.** No new work, no new
await, no new failure mode; the resolution is a fait accompli. That intuition is correct about cost
and silent about correctness, and the sync-ness sits one hop away inside the helper, where a
call-site reviewer will not see it.
So the check is not "am I calling a sync resolver here?" but **"what produced every lane value I am
about to compare against, transitively?"** A local is not evidence of anything; the resolver behind it
is. #3122 widened the gate to follow wrappers for exactly this reason — and I walked through the door
it was widened to cover, in the same phase I was adding it.
Two corollaries worth keeping:
- **A gate that catches the defect but does not block is a report.** This fired correctly and the PR
merged anyway, because `check:inert-sync-lanes` was not in the blocking set (being fixed in #3127).
That is the second time in one phase that a correct non-blocking signal was ignored.
- **The remaining count is not backlog.** Reverting the arm takes `triage.ts` to 7, and those seven
are the same shape — they need the emitter-side / async-threading work, not another conversion pass.
`--claims` now marks sync-resolver files as inert-risk and keeps them out of the start-here list.
## The rule that produced every fix above
**A green guard is evidence only once you have watched it go red.**