refactor(notes): a snippet's and lesson's stored title is its name; the trigger joins it only in the embedded document (milestone 427)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / integration (push) Successful in 52s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m35s
CI & Build / Build & push image (push) Successful in 32s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / integration (push) Successful in 52s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m35s
CI & Build / Build & push image (push) Successful in 32s
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>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
"""trigger_leaves_the_stored_title — a snippet's and lesson's title is its name (milestone 427)
|
||||
|
||||
Revision ID: 0108
|
||||
Revises: 0107
|
||||
Create Date: 2026-09-23
|
||||
|
||||
Snippets and lessons stored their title as `subject — trigger`, because the
|
||||
join is what makes these kinds rank on the situation they apply to (#2485) and
|
||||
the stored title WAS the embedded one. Every surface that shows a title then
|
||||
showed the trigger too: menu lines, lists and search rows ran to 1,500–3,000
|
||||
characters, and an injected repeat spent all of it again.
|
||||
|
||||
The trigger's home is `data` (decision #4157), and since this milestone the
|
||||
join happens at embed time (`embeddings.document_title`). This revision
|
||||
rewrites the rows already stored.
|
||||
|
||||
EXACT-SUFFIX, READ FROM THE ROW'S OWN MIRROR. A title is only rewritten when
|
||||
it ends with `' — ' || <that row's trigger>`, so a subject that legitimately
|
||||
contains an em dash is never cut, and a row whose title and mirror disagree —
|
||||
hand-edited through the generic note door — is left alone. Such a row still
|
||||
embeds correctly (`document_title` is idempotent) and merely shows its old
|
||||
title; that is the right way round for a data migration to fail.
|
||||
|
||||
NO RE-EMBED, AND `updated_at` IS NOT TOUCHED. The embedded text is identical
|
||||
before and after, so the vectors are already right. The startup backfill
|
||||
re-embeds any row whose `updated_at` is newer than its vectors, and a raw
|
||||
UPDATE that leaves `updated_at` alone is what keeps this from queueing the
|
||||
whole snippet corpus for work that would produce the same numbers. There is
|
||||
no database trigger on `updated_at`; it is set by the ORM only.
|
||||
"""
|
||||
from alembic import op
|
||||
|
||||
revision = "0108"
|
||||
down_revision = "0107"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
# kind → the `data` key its trigger is mirrored under (embeddings._TRIGGER_DATA_KEYS).
|
||||
_KINDS = (("snippet", "when_to_use"), ("lesson", "when_to_apply"))
|
||||
_SEP = " — "
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
for note_type, key in _KINDS:
|
||||
op.execute(f"""
|
||||
UPDATE notes
|
||||
SET title = btrim(left(title, length(title) - length('{_SEP}' || (data->>'{key}'))))
|
||||
WHERE note_type = '{note_type}'
|
||||
AND coalesce(btrim(data->>'{key}'), '') <> ''
|
||||
AND right(title, length('{_SEP}' || (data->>'{key}'))) = '{_SEP}' || (data->>'{key}')
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Recompose, on the same condition inverted: only where the trigger is not
|
||||
# already on the end, so a downgrade run twice cannot double it.
|
||||
for note_type, key in _KINDS:
|
||||
op.execute(f"""
|
||||
UPDATE notes
|
||||
SET title = title || '{_SEP}' || (data->>'{key}')
|
||||
WHERE note_type = '{note_type}'
|
||||
AND coalesce(btrim(data->>'{key}'), '') <> ''
|
||||
AND right(title, length('{_SEP}' || (data->>'{key}'))) <> '{_SEP}' || (data->>'{key}')
|
||||
""")
|
||||
Reference in New Issue
Block a user