Lessons reach production by DATA MIGRATION, not a hand-run seed — the generator, and the embedding sweep that makes an inserted row rankable
Replaces the hand-run seed. Nothing here is manual.
fly.toml's release_command is pnpm prisma migrate deploy and ci.yml runs it too, so a data migration reaches production on every release with no operator. That is the delivery path for lessons — now and for every lesson added later, which is the point: adding one should be a normal reviewable change, not an ops chore somebody remembers.
Two halves, and the second is what makes the first work.
1. The generator. A script reads lessons.base.ts — which stays the single authored, typed, reviewable source — and emits a forward-only data migration inserting its rows, ON CONFLICT ("sourceRef") DO NOTHING. The generated migration is committed like any other. Adding a lesson then means: write the row, run the generator, commit both.
2. The embedding sweep. listForInjection ranks by cosine distance against embedding, and setEmbedding is called only from the create and edit paths — nothing embeds a row that arrived by SQL. A migration-inserted row is therefore present, healthy and permanently unrankable, which is a silent failure: the store's row count looks right and the lesson never appears.
So add an idempotent sweep that embeds every lesson whose embedding is null, batched through the existing embedding service, and run it where the release already goes. It must be safe to run repeatedly and must no-op when nothing is missing.
Acceptance criteria
- A generator emits a forward-only migration from
lessons.base.ts, inserting rows withON CONFLICT ("sourceRef") DO NOTHING; the generated file is committed. - Re-running
prisma migrate deployagainst a seeded database inserts nothing and removes nothing, asserted. - The migration no-ops cleanly on a fresh database and does not require the gateway — no embedding is computed inside it.
- A sweep embeds every lesson with a null
embedding, batched, idempotent, and a no-op when none are missing. - A test proves the round trip: a row inserted by the migration is NOT returned by
listForInjectionbefore the sweep and IS returned after it. - No card in this story is
manual; the deployed row count is verifiable from a normal release rather than from an operator's terminal.
Context refs
motir-aifly.tomlrelease_command,.github/workflows/ci.yml:107—prisma migrate deployon release and in CI.motir-aisrc/repositories/lessonRepository.ts—setEmbedding(raw::vectorwrite) and the cosine ranking query.motir-aisrc/services/lessonService.ts:118,:638— the only two callers ofsetEmbeddingtoday.motir-aisrc/services/embeddingService.ts— the batch path, whose comment notes a backfill is a handful of round-trips rather than one per row.motir-aisrc/seed/lessons.base.ts,src/seed/seedBaseLessons.ts,scripts/seed-lessons.ts— the authored source and the seed this supersedes.