Lesson gains kinds, types and phases — mirroring the work item and the rule-pack selector; the topical categories are dead data
The existing categories String[] is dead data. Proven on origin/main:
selectForInjectionbuilds{ aiProjectId, queryText, mistakeTypes: [phase, 'planning_craft'], limit }— it never passescategories, so no planning selection has ever filtered on them.captureMistakewritescategories: []— every runtime-captured lesson carries none.- The only values that exist are the hand-written topical tags on the seed rows (
ordering,design-system,task-type), which nothing queries.
A topical tag also answers the wrong question. It says what a lesson is about; retrieval needs to know which card it applies to.
So give the lesson the same three axes the rule packs already route on. MANIFEST.md's selector is pack(operation, phase, kind, type) — the two corpora should share one routing model rather than invent a second:
| column | 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) |
Set-valued, empty means applies-to-all. The corpus's own routing is set-valued — type ∈ {code, chore, deploy} loads type-migration.md — so a migration lesson carries all three types. A scalar could not express it, and the fallback would be "applies to everything", which is the over-injection this epic removes.
⚠️ phase is an overloaded word here. PlannerPhase already exists in lessonService and means onboarding_planning | regular_planning — which pass is running, not which part of the card is being written. Do not reuse that type or that name; mistakeTypes keeps its current job as the consumer pre-filter.
Ship the columns and tag the existing rows in a forward-only, idempotent data migration — release_command runs prisma migrate deploy, so it reaches production on release.
Acceptance criteria
Lessoncarrieskinds,typesandphases, all set-valued, all defaulting to empty; empty on an axis means unconstrained on it.- Each vocabulary matches the work item's, and
phasesis exactlyskeleton/deepen; a test pins all three so a typo fails the build rather than silently never matching. - The new axes are independent — a lesson may constrain one and leave the others empty.
- A forward-only, idempotent data migration adds values to the existing rows; it no-ops on a fresh database and on a re-run.
- The dead
categoriescolumn is left inert and its disposition recorded — dropped in a later card, or kept with a stated reason. It is not silently repurposed. prisma migrate deployapplies cleanly on the test database.
Context refs
motir-aisrc/services/lessonService.ts—selectForInjection(nocategories),captureMistake(categories: []), and thePlannerPhasename to avoid.motir-aisrc/repositories/lessonRepository.ts— the existing&&overlap clause, the model for the new axes' filtering.motir-aiprisma/schema.prisma— theLessonmodel.motir-metaprompts/plan-rules/MANIFEST.md—pack(operation, phase, kind, type), the selector these mirror.motir-aifly.tomlrelease_command·.github/workflows/ci.yml:107.