The runbook lesson corpus reaches the store — every un-accounted notes.html entry distilled, categorized, and delivered by data migration
⚠️ MEASUREMENT CORRECTION (applied by the run of this story, 2026-08-21). Three figures below were wrong WHEN WRITTEN, and are superseded by this block rather than edited out of the prose, so the diff shows an amendment and not a silent drop.
this card says actual how it was checked lessons.base.tsholds 70 rows69 counted at f682d19and at this branch's base;git log f682d19..HEAD -- src/seed/lessons.base.tsis empty, so the file is byte-identical across the window and the count was wrong when written, not driftthose rows account for 112 distinct entries 114 — {1..111} ∪ {156,157,158}112 seeded + 2 excluded notes.htmlhead is 355, un-accounted 241356 and 242 re-measured at motir-metaorigin/main2d3c93e; the corpus is contiguous1..356No range and no disposition changes.
#112–#155is 44 entries exactly as stated, and the two backfill ranges are unaffected. Evidence and the per-range disposition counts are in motir-ai#274; the same finding is added to MOTIR-3363, which already owns this measurement family.
notes.html holds 355 entries and is written to most days. src/seed/lessons.base.ts holds 70 rows, was last written 2026-07-26, and accounts for #1–#111 plus #156, #157, #158 — 112 distinct entries, two of which (#1, #19) are EXCLUDED_ENTRIES carrying a reason. The runtime captureMistake path only ingests the shipped planner's own mistakes, so the one bridge for runbook-captured lessons is a seed script somebody runs by hand.
The gap, re-measured
On origin/main — motir-ai @ f682d19, motir-meta origin/main — on 2026-08-21:
notes.html entries | 355, numbered contiguously 1..355, no gaps, no duplicates |
lessons.base.ts rows | 70 |
| entries those rows account for | #1–#111 ∪ {#156, #157, #158} — 112 |
| un-accounted | #112–#155 (44) and #159–#355 (197) — 241 entries |
⚠️ This story was first authored on the premise that the seed "stops at #158" — which reads as everything below it is covered. It is not. #112–#155 has been in neither the seeded set nor the excluded set since the day those entries were written. A backfill scoped from #159 upward would satisfy this story's own "every source entry has a disposition" gate while 44 entries received none — and then MOTIR-3320 would delete the hand-run seed, removing the last bridge they had. A gap that was merely unfilled would become one that is closed off, with the store's row count reading exactly as it should.
And the reason the drift was silent is a hole in the guard, so closing it is part of this story. tests/lessonsBaseSeed.test.ts:83 asserts totality exhaustively for n <= 111 only, then checks a hardcoded for (const n of [156, 157, 158]). Writing a new notes.html entry therefore creates no obligation anywhere: the corpus can grow indefinitely and CI stays green. That is what happened between 2026-07-26 and today, and backfilling without closing it only resets a clock that will run down again. The fix rides on MOTIR-3306, which already owns that file.
Delivery is a DATA MIGRATION, not a hand-run seed. fly.toml's release_command is pnpm prisma migrate deploy, so a migration reaches production on release with no operator — and, more importantly, so does every lesson added after this story. A generator emits the migration from lessons.base.ts, which stays the authored source.
The half that is easy to miss: ranking is cosine distance against embedding, and setEmbedding is called only from the create and edit paths. A row inserted by SQL is present, healthy and permanently unrankable. The story therefore owes an embedding sweep as well as the migration.
This is a distillation, not an import — the corpus-distillation gate applies in full: generalize past the incident, rewrite in the product's voice, dedup against the 70 rows already there, and give every one of the 241 source entries an explicit disposition. Dropping one silently is the failure that gate exists to prevent, and it is the failure this story's own first draft was about to commit at scale.
⚠️ SUPERSEDES MOTIR-1948, which this re-plan retires. That card — filed 2026-07-31 under MOTIR-2200, still
todoandready— owned exactly this backfill and the totality-guard fix, and had been kept current by five of Yue's comments routing individual entries into it. It predates the migration delivery path and the routing axes, and its own follow-up plan was amanualprod seed run; this story is strictly the better shape. Its comments do not die with it — the per-entry routing decisions recorded there (#180,#196,#224among them, each with a suggested generic register and a dedup note) are inputs to the distillation cards and are cited on each of them.
⚠️ NOT BLOCKED on the retirement loop, and this is a considered exclusion rather than an oversight. Every row this story adds is global scope, and MOTIR-3323 establishes that a global lesson's
lastOccurredAtcan never be bumped — so these rows fall out of every query 90 days after they are seeded, with no path back. That is a real dependency in fact. It is deliberately not wired as an edge because the backfill does not need the fix to BUILD, blocking on the whole of MOTIR-3322 would also serialize this behind work it has no relationship to, and 3323 is ahighest-priority three-pointer with ninety days of runway. If 3323 slips past that window, this story's output is gone and the exclusion was wrong — so the two are linked, and whoever schedules this one should check that one first.
Acceptance criteria
- Every
notes.htmlentry in#112–#155and in#159–the corpus head has an explicit disposition recorded in a PR: a new row, a merge into an existing row, or a drop with a stated reason. The disposition count equals the source count, and each distillation card states the head it measured and the ref it measured on. - Each new row carries its kind/type/phase axes,
title+howToApplywritten generically, and itssourceRef. - No row is a verbatim paste of a
notes.htmlentry. - The seed's totality assertion no longer restates a hardcoded list of entry numbers: adding an entry to
notes.htmlwithout accounting for it fails a test, and the failure message says what to do. - The rows reach a deployed database by
prisma migrate deployon a normal release — no card in this story ismanual. - The migration is forward-only and idempotent on
sourceRef; a re-run inserts nothing and removes nothing. - A migration-inserted row is proved rankable: not returned by
listForInjectionbefore the embedding sweep, returned after it.
Context refs
motir-aisrc/seed/lessons.base.ts— the 70 existing rows,EXCLUDED_ENTRIES, and their shape.motir-aitests/lessonsBaseSeed.test.ts:83— the totality assertion, exhaustive ton <= 111and hardcoded thereafter; the hole that made the drift silent.motir-aifly.tomlrelease_command,.github/workflows/ci.yml:107— the automaticprisma migrate deploy.motir-aisrc/repositories/lessonRepository.ts—setEmbeddingand the cosine ranking that makes the sweep necessary.motir-metanotes.html— the source corpus, at#355on 2026-08-21.- MOTIR-1948 — superseded by this story; its five comments carry Yue's per-entry routing decisions and remain authoritative.
- MOTIR-3323 — the global-expiry defect these rows are exposed to.