Planning bug: a card that hands the runner a SCAN defines the population BY that scan — and this one's pattern was incomplete in three ways, one of which hid a real defect
Found running MOTIR-3077 (motir-core#2143). The card is a good card — it bounds its sweep honestly, it names its three buckets, and it insists on a nil result being reported. The defect is one level up: it defines the work's POPULATION by handing the runner a scan, and a scan is a claim about coverage that nothing checked.
The card's context refs end with:
The scan:
Promise.all([inlib/**/*.tswhose block also matcheswithWorkspaceServiceContext|withWorkspaceContext|$transaction|withOrg\w*Context.
and its body says that returns 24 sites. Measured at the run's base (fbe5e2cd), that pattern is wrong in three independent ways.
1. The COUNT is a windowed grep, and the card reports it as a property of the code
Promise.all([…]) sites in lib/: 55. Whose balanced block carries one of the four tokens: 15. Matched by a 30-line windowed grep (grep -A 30): 26. The card's 24 is the windowed number at an earlier base — MOTIR-2732 merged two more that same morning.
A runner reading "returns 24 sites" and measuring 15 has to decide whether it broke something or the card is stale, and the card gives it nothing to decide with. The fix is not a better number: it is that a card citing a scan cites the COMMAND and the COMMIT it was run at, so the number is reproducible rather than asserted.
2. The PATTERN misses transaction openers that exist
lib/ exports seven with…Context helpers that open a transaction. The alternation names three of them. withSystemContext, withUserContext, withOrgServiceWriteContext and withBootstrapSlugContext are absent, and publicProjectsService.ts:775 — whose two arms are withSystemContext and withUserContext — appears in no reading of the card's scan. It happened to be low-risk. Nothing in the card made that lucky rather than lucky-looking.
3. The SCOPE is lib/, and the class is not
This is the one that cost something. The second dangerous site is app/(authed)/code-health/page.tsx, and it is doubly invisible to the card: it is not under lib/, and its fan-out is Promise.all(refs.map(…)) rather than a literal array, so widening the pattern inside lib/ would not have found it either. It was found by the card's own empirical half — the shard-3 probe — which is the half a reader is most likely to treat as a formality once the source scan has produced a tidy table.
Three fan-outs there abandoned every other repo's audit and convention read whenever the project gate refused, which is an ordinary path (a non-admin on an admin-only page). Repaired in the same PR, reported as out-of-population rather than absorbed silently.
What this is telemetry FOR
The correction is already applied — the PR classifies 16 sites rather than 24, names the four missing openers, and repairs the app/ site. Nothing is blocked. What is worth keeping is the rule, which generalises past this card:
- A card that hands the runner a scan owes the scan's COMMAND, its COMMIT, and its count as an output of that pair — never a bare number in prose. A stale count reads as a defect in the runner.
- A scan that enumerates a family (context helpers, error classes, event kinds) owes the enumeration's SOURCE, so the runner can re-derive membership instead of trusting a hand-written alternation.
grep -c 'export async function with.*Context' lib/costs one command and would have caught #2. - A class sweep is scoped by where the CLASS can occur, not by where the instance was found.
lib/was the scope because MOTIR-3066's instance was inlib/; the class is "aPromise.allwhose arms open transactions", which is a property of the language and the helpers, not of a directory. - And when a card carries both a source half and an empirical half, the empirical half is the one that decides the population — it is what sees what the pattern cannot. A card should say so, so a runner does not treat a green scan as the answer and the probe as a receipt.
Acceptance criteria
plan-rules/carries the scan-citation rule: a card that hands a runner a scan gives the command, the commit it was measured at, and the count as that pair's output.plan-rules/carries the enumeration rule: a pattern that lists members of a family cites the command that derives the family.plan-rules/carries the scope rule: a class sweep's scope is argued from where the class can occur, not from where the instance was found — and a card whose scope is narrower than its class says why, in the card.notes.htmlcarries the lesson entry, with the three measurements above as its evidence.SHARED_PLANNING_RULEScarries the same rule — the shipped planner's copy of the same three clauses, in its own register, with a guard test.
AC 5 added 2026-08-20, on the record (Yue). As authored, this card's criteria named
plan-rules/andnotes.htmlonly — the RULE tier's second home absent, which is the fifth occurrence of that shape. The run that delivered it first proposed the mirror as a SEPARATE card, on the reading thatONE SUBTASK = ONE REPO = ONE PRforced a split. It does not: that rule bindskind: subtask, and a work item — abugincluded — carriestargetRepos. The card was re-pinnedtargetRepos: [motir-meta, motir-ai]and re-sized 30 → 70 min / 2 → 3 pts; it now completes only when both repositories' pull requests have merged.
Context refs
- MOTIR-3077 — the card, and motir-core#2143 the run that measured this.
- MOTIR-3066 — where the class was first found, and where the
lib/scope came from. app/(authed)/code-health/page.tsx— the site outside the population.lib/organizations/context.ts·lib/workspaces/context.ts— the seven openers the alternation names three of.