Planning bug: a story scoped its population from where a corpus "STOPS" — but the coverage was a SET, not a prefix, so 44 entries below the boundary were invisible to every check
Filed from motir run MOTIR-3296 (2026-08-21), which stopped at guard #4 before creating a worktree. The re-plan is plan cmt2wqx0f0005i3n8baz5uia9.
The defect
MOTIR-3296 scoped a corpus backfill as notes.html #159–#353, on the premise — stated in its first paragraph — that src/seed/lessons.base.ts "stops at notes.html #158".
"Stops at #158" is true and means nothing like what the scope needed it to mean. The seed's coverage is a SET, not a prefix: its sourceRefs account for #1–#111 ∪ {#156, #157, #158}. The maximum is 158; the set has a 44-entry hole at #112–#155 that has never been seeded and never excluded.
Measured on origin/main, motir-ai @ f682d19:
grep -c "sourceRef:" src/seed/lessons.base.ts → 70 (the card said 72)
distinct #n across all sourceRefs → 112, max 158
missing from 1..158 (minus EXCLUDED_ENTRIES {1,19}) → 112..155
notes.html entries (motir-meta origin/main) → 355, contiguous (the card said 353)
Three numbers in the premise, and the arithmetic on the one that mattered was answering a different question. A max() over a set is a real measurement; treating it as a coverage boundary is an inference, and the inference was never written down as one — so no later reader had anything to check.
Why nothing caught it
- The card's own gate could not see it. MOTIR-3296's acceptance criterion is "every entry in the range has an explicit disposition; the disposition count equals the source count." That is a totality claim over the range the card names, so it is satisfiable at 100% while the range itself is wrong. A totality gate cannot audit its own denominator.
- The seed's test has the same blind spot, and for the same reason.
tests/lessonsBaseSeed.test.ts:83assertsseeded ∪ EXCLUDED == 1..111exhaustively, thenfor (const n of [156, 157, 158]).#112–#155is exempt by construction, and so is every entry written from now on. The corpus drifted for 25 days under a green suite. The card and the test made the same mistake independently — both wrote "the newest N" where the invariant needed "all of them". - The consequence was about to become permanent, not merely deferred. MOTIR-3320 deletes the hand-run seed once the migration carries the rows. Shipped as scoped, the 44 entries lose their last bridge, and the store's row count — the only number anyone checks — reads exactly as it should.
Second finding from the same pass: a live card already owned this
MOTIR-1948 — todo, ready, under epic MOTIR-2200, filed 2026-07-31 — covers the same backfill and the totality-guard fix, and had been kept current by five of Yue's comments routing individual entries into it. It also had the range right, naming #112–#119, #121–#155, #159–… explicitly.
plan-rules/core.md gate 8's TAKER limb already asks "does any EXISTING card already claim this deliverable?" on every card AUTHORED, not only on a re-plan. It was not run. One search_work_items for lessons.base.ts — a path in MOTIR-3296's own context refs — returns MOTIR-1948 in the first page.
Note the compounding: the duplicate card was also the correction. Finding it would have fixed the population in the same read.
The rule this suggests
Gate 2's QUANTITY limb requires that a number in a criterion be produced by a measurement run at the criterion's own scope. This case passes that reading and still fails, because the measurement WAS run — max(sourceRef) — and was arithmetically true of the wrong question. The gap is a type error rather than a missing measurement:
A COVERAGE claim may not be stated as a BOUNDARY. "Stops at N", "up to date through N", "covers everything before N" assert an interval; coverage of a corpus is a SET, and the two agree only if the set is contiguous — which is a separate fact requiring a separate check. Write coverage as the set (or as
total − covered), and derive the population fromsource \ covered, never fromsource > max(covered).
The same shape generalizes past corpora: a migration "already applied through version N", a locale catalog "translated up to key N", a test suite "covering everything since release N". In each, the gap-in-the-middle is invisible to a check written against the head.
Acceptance criteria
- The coverage-is-a-set-not-a-prefix rule is added to
plan-rules/, in the pack that owns gate 2's QUANTITY limb, as a limb of it rather than a new gate — with the discharge stated as derive the population by set difference, and never from a maximum. - The rule states the generalization (migrations, catalogs, suites), not only the corpus case, and names the tell: a scope sentence containing "stops at", "through", "up to" or "since" followed by a single identifier.
- Whether the mirror in motir-ai's
SHARED_PLANNING_RULESneeds the same limb is decided and stated — mirrored, or explicitly not, with the reason (the two-homes rule). - The
notes.htmllesson for this incident exists and is cited. - No product code changes; the plan-side correction is already carried by plan
cmt2wqx0f0005i3n8baz5uia9.
Context refs
- MOTIR-3296 — the card this was found on; the full diagnosis is in its 2026-08-21 comment.
- MOTIR-1948 — the duplicate the pass did not search for.
motir-aisrc/seed/lessons.base.ts,tests/lessonsBaseSeed.test.ts:83— the set, and the test that exempts its hole.motir-metaprompts/plan-rules/core.md— gate 2's QUANTITY limb, and gate 8's TAKER limb.