Filed by the run of MOTIR-3719 (PR #2403), under the planner-bug home. No fix is owed in a repo — the correction is already applied to MOTIR-3719's body and to the shipped code comment. This is the telemetry record.
MOTIR-3719 was filed as "a dead arm plus a live uncaught path". Its second half:
resolveCodeContext(...)can throw aMotirAiErrorand is issued before thetry… So the one liveMotirAiErrorpath on this page is uncaught — motir-ai being unreachable renders the server-error page rather than the degraded surface the code was written to show.
That claim also carried the card's whole priority argument, in explanationMd: "the live half is the cost".
resolveCodeContext reaches no motir-ai client. On origin/main bed2bbc52:
$ grep -n "^import" lib/ai/codeContext.ts
1:import { githubInstallationRepository } from '@/lib/repositories/githubInstallationRepository';
2:import { githubRepoRepository } from '@/lib/repositories/githubRepoRepository';
3:import { withWorkspaceContext } from '@/lib/workspaces/context';
and the whole closure of those three is @/lib/db + @/generated/prisma/client. Every MotirAiError in the codebase is constructed in lib/ai/motirAiClient.ts (plus one site in projectLessonsService), which nothing on that path imports. The module's own header states it outright — "A DB read ONLY (the 891 mirror rows) — never a GitHub API round-trip on the submit path."
Not drift. git log --oneline e88d3d73..HEAD over the page, lib/ai/codeContext.ts and CodeHealthClient.tsx is EMPTY, so the three files are byte-identical between the card's base and main: the premise was false when written, not overtaken.
The claim was inferred from the module PATH, not from the module. The card's first half is exemplary — it enumerates every aiConventionService call site, walks all four paths through loadCodeHealthSurfaces, and proves the arm dead by exhaustion. The second half asserts a throw for a function under lib/ai/ whose name reads as an AI call, with no call site named and no import read.
Two properties make this hard to catch rather than careless:
lib/ai/ holds the motir-ai client, its errors and its types; codeContext is genuinely for an AI planning job. The path is not misleading by accident, and it is still not evidence about what a function throws.The false half is what made the card a defect rather than a cleanup: it set the priority argument and it framed the decision as a real fork (resolveCodeContext moves inside the try or the arm goes). With the premise falsified, option (a) is vacuous — moving a call that cannot raise a MotirAiError makes the arm no less dead — and the design asset had already settled the answer. The run spent its first block re-deriving a fork that had one arm.
The card's own first half is the method: name the call site, or read the import closure. A claim that X throws Y is checkable in one grep for Y's construction sites plus one for X's imports — the same two commands that make the dead-arm proof convincing, applied to the sentence beside it. The authoring-side rule this belongs to is plan-rules/phase-deepen.md's precondition-verification limb: a claim about what a function CAN raise is a precondition, and a module path is not a reading of one.
explanationMd's cost argument is restated, and AC 2 names the limb that discharges it.app/(authed)/code-health/page.tsx carries the finding at the resolveCodeContext call site, so the next reader meets it instead of re-deriving it (PR #2403).