MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-3363

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

Done
Description

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:83 asserts seeded ∪ EXCLUDED == 1..111 exhaustively, then for (const n of [156, 157, 158]). #112–#155 is 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-1948todo, 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 from source \ covered, never from source > 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_RULES needs the same limb is decided and stated — mirrored, or explicitly not, with the reason (the two-homes rule).
  • The notes.html lesson 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-ai src/seed/lessons.base.ts, tests/lessonsBaseSeed.test.ts:83 — the set, and the test that exempts its hole.
  • motir-meta prompts/plan-rules/core.md — gate 2's QUANTITY limb, and gate 8's TAKER limb.