A lesson is keyed like a work item — kinds, types and phases, the same selector the rule packs route on
The foundation every other story routes through: until a lesson can be keyed to the card being planned, nothing can leave the rules corpus — moving a paragraph out would only make it unreachable.
The existing category axis is dead data, proven on origin/main: selectForInjection never passes categories, captureMistake writes categories: [], and the only values in existence are hand-written topical tags on the seed rows that nothing queries. A topical tag also answers the wrong question — what a lesson is about, never which card it applies to.
So key the lesson the way the rule packs already route. MANIFEST.md's selector is pack(operation, phase, kind, type); the two corpora should share one routing model rather than invent a second:
| axis | vocabulary | meaning |
|---|---|---|
kinds | epic · story · task · bug · subtask | which card kind the mistake happens on |
types | the fourteen-member work type | which work type |
phases | skeleton · deepen | widening — planning the children — vs deepening — writing the description |
The phase axis is the one with no predecessor and it discriminates hardest. Mistakes made while laying a level's children are a different family from mistakes made while writing a card's body: the first are about shape, edges and coverage; the second about criteria, sizing and claims. Today a planner writing a description receives all the widening lessons.
All three are set-valued, empty means unconstrained — the packs' own routing is set-valued (type ∈ {code, chore, deploy}), so a migration lesson carries three types and a scalar would force it to "applies to everything".
⚠️ phase is overloaded. PlannerPhase already means onboarding_planning | regular_planning — which pass is running, not which part of the card is being written. mistakeTypes keeps that job as the consumer pre-filter; the three axes sit underneath it.
Acceptance criteria
Lessoncarrieskinds,typesandphases, set-valued, defaulting to empty; empty on an axis means unconstrained on it, and the three are independent.- Each vocabulary matches the work item's;
phasesis exactlyskeleton/deepen. A test pins all three so a typo fails the build rather than never matching. listForInjectionapplies one overlap clause per axis, each omitted when its axis is empty;selectForInjectioncarries all three and passes them down.mistakeTypesbehaviour is unchanged, asserted.- A test proves the axes narrow independently, that a lesson tagged for three types matches a query for one of them, that a
skeletonquery returns widening lessons and not deepening ones, and that an unconstrained lesson reaches every query. - Existing rows are tagged by a forward-only, idempotent data migration; the dead
categoriescolumn's disposition is recorded rather than silently repurposed.
Context refs
motir-aisrc/services/lessonService.ts—selectForInjection(no categories),captureMistake(categories: []),PlannerPhase.motir-aisrc/repositories/lessonRepository.ts— the&&overlap clause with omit-when-empty, the pattern the three axes follow.motir-aiprisma/schema.prisma— theLessonmodel.motir-metaprompts/plan-rules/MANIFEST.md—pack(operation, phase, kind, type).motir-aifly.tomlrelease_command·.github/workflows/ci.yml:107.