Planning bug: a test card's fixture table encoded axis semantics that contradict the three-axes-are-independent rule its own parent story states in prose
Found while running MOTIR-3294 (motir run on the parent, branch parent/MOTIR-3294-lesson-axes in motir-ai). The defect is in the PLAN, not in shipped code; the correction is already applied to MOTIR-3301 on the record.
What the plan said, in two places, and they disagree
The prose, three times:
- MOTIR-3294 (the story): "All three are set-valued, empty means unconstrained" and "empty on an axis means unconstrained on it, and the three are independent."
- MOTIR-3299: "empty on an axis means unconstrained on it"; "The new axes are independent — a lesson may constrain one and leave the others empty."
- MOTIR-3300: "The three axes are independent: a query naming only
typesdoes not constrainkindsorphases."
The fixture table, in MOTIR-3301, seeds five rows and lists an expected set per query. Three of those five expected sets are only reachable under the OPPOSITE semantics — that a lesson is universal only when it is unconstrained on ALL THREE axes, and otherwise must overlap on whichever axis is queried:
| assertion | card expects | correct under the prose | the difference |
|---|---|---|---|
2 · types: [chore] | C, D | C, D, E | E carries no types, so it is unconstrained on that axis |
3 · phases: [skeleton] | E, D | C, D, E | C carries no phases |
5 · kinds: [bug] | E, D | A, B, C, D, E | A, B and C carry no kinds |
Assertions 1 and 4 are identical under both readings, which is why the table looks coherent read top to bottom.
Why the prose is the correct half
Not a preference — the fixture's reading inverts the epic:
- MOTIR-3299's backfill tags 59 of 69 base lessons, most on a SUBSET of the axes. Under the fixture's reading
notes.html #37(taggedtypes: [test]) becomes invisible to everykindsorphasesquery, and the more precisely a lesson is tagged the less of the corpus a planner can reach. - Every lesson
captureMistakehas ever written carries all three axes empty. Those keep working under either reading, but under the fixture's reading they are the ONLY rows a filtered query can ever reach.
So tagging the corpus would REDUCE retrieval, which is the over-injection this epic exists to remove, rebuilt inside the mechanism meant to fix it.
Disposition applied
listForInjectionimplements the prose: per axis,AND (cardinality(col) = 0 OR col::text[] && ARRAY[…]::text[]), and the clause is omitted entirely when the QUERY leaves that axis empty. Empty means unconstrained on BOTH sides.- MOTIR-3301's three expected sets are amended on the record, with the evidence, and the amendment is repeated as a comment at the head of
tests/lessonAxisFiltering.test.tsso a later reader meets it where the numbers are. - Six mutants (three per failure mode, one per axis) are killed by the suite.
Acceptance criteria
- A planning rule exists requiring that when a card states an expectation as a TABLE OF EXPECTED RESULTS, the table is checked row by row against the semantics its parent states in prose — because a table that is right on two rows out of five reads as verified.
- The rule names the tell: an expected set that omits the members which are UNCONSTRAINED on the queried axis. A fixture author enumerating "what this query is FOR" writes down the rows that match and forgets the rows that do not object.
- The
notes.htmlentry recording this is linked from the rule.
Context refs
motir-metaprompts/plan-rules/phase-deepen.md— where an authoring bar for a card's own acceptance table belongs.motir-aitests/lessonAxisFiltering.test.ts— the amended table and the reasoning, as shipped.- MOTIR-3301 — the card amended.