The title was `subject — trigger` because the stored title WAS the
embedded one, and the join is what makes these kinds rank on the
situation they apply to (#2485). Every surface that shows a title then
showed the trigger too -- menus, lists and search rows ran to kilobytes.
- embeddings.document_title(title, note_type, data, body) joins the
trigger from `data` (body fallback) at embed time. Idempotent: an
un-migrated composed title comes out the same, never doubled. The
embed path, the startup backfill and the dedup gate's semantic signal
all use it, so the embedded text -- and every vector -- is unchanged.
- Writers store the subject: snippet create/update (service, REST, MCP)
and lesson_document. Both compose_title helpers are removed.
- Readers: dedup takes `data`; the menus strip the embedded title from a
passage; list rows project `when_to_use`, which SnippetListView reads.
- 0108 rewrites existing rows on an exact `' — ' || <own trigger>`
suffix with raw SQL, leaving updated_at alone so the backfill does not
re-embed the corpus for identical vectors. Downgrade recomposes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>