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

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:
2026-09-23 16:48:28 -04:00
co-authored by Claude Opus 5.5
parent bb632c4196
commit 66e21a6c60
24 changed files with 358 additions and 189 deletions
+3
View File
@@ -91,6 +91,9 @@ export interface SnippetListItem {
* hit can be flagged as being in a DIFFERENT language than the file being
* written, which is a shape to adapt rather than code to paste. */
language?: string;
/** When to reach for it, projected from the `data` mirror. The title is the
* name alone since milestone 427, so this is where the situation lives. */
when_to_use?: string;
/** Always present from the backend, zero-filled for records with no events. */
usage?: SnippetUsage;
/** Present on the detail record; the list feed carries it when a check has
+17 -10
View File
@@ -189,11 +189,18 @@ const onLocationInput = onSearchInput;
onMounted(loadSnippets);
/** Titles are stored as "name when to reach for it"; split for display. */
function splitTitle(title: string): { name: string; when: string } {
const idx = title.indexOf(" — ");
if (idx === -1) return { name: title, when: "" };
return { name: title.slice(0, idx), when: title.slice(idx + 3) };
/** A row's name and when-to-use. The title is the name alone since milestone
* 427 and the situation arrives as `when_to_use`; a title composed before then
* ("name — when to reach for it") is split, so either shape reads the same. */
function nameAndWhen(s: { title: string; when_to_use?: string }): { name: string; when: string } {
const when = s.when_to_use || "";
if (when && s.title.endsWith(`${when}`)) {
return { name: s.title.slice(0, -(when.length + 3)), when };
}
if (when) return { name: s.title, when };
const idx = s.title.indexOf(" — ");
if (idx === -1) return { name: s.title, when: "" };
return { name: s.title.slice(0, idx), when: s.title.slice(idx + 3) };
}
function languageOf(tags: string[]): string {
@@ -313,7 +320,7 @@ function driftTitle(s: SnippetListItem): string {
<div v-for="(g, i) in duplicateGroups" :key="i" class="dup-group">
<div class="dup-members">
<span v-for="s in g.snippets" :key="s.id" class="dup-member">
{{ splitTitle(s.title).name }}
{{ nameAndWhen(s).name }}
</span>
</div>
<span class="dup-score">{{ Math.round(g.top_score * 100) }}% alike</span>
@@ -413,11 +420,11 @@ function driftTitle(s: SnippetListItem): string {
:class="{ on: selectedIds.has(s.id) }"
aria-hidden="true"
></span>
<span class="snippet-name">{{ splitTitle(s.title).name }}</span>
<span class="snippet-name">{{ nameAndWhen(s).name }}</span>
<span v-if="languageOf(s.tags)" class="lang-pill">{{ languageOf(s.tags) }}</span>
</div>
<p v-if="splitTitle(s.title).when" class="snippet-when">
{{ splitTitle(s.title).when }}
<p v-if="nameAndWhen(s).when" class="snippet-when">
{{ nameAndWhen(s).when }}
</p>
<div class="card-footer">
<span class="meta-date">Updated {{ new Date(s.updated_at).toLocaleDateString() }}</span>
@@ -458,7 +465,7 @@ function driftTitle(s: SnippetListItem): string {
:class="{ chosen: canonicalId === s.id }"
>
<input type="radio" name="canonical" :value="s.id" v-model="canonicalId" />
<span class="merge-choice-name">{{ splitTitle(s.title).name }}</span>
<span class="merge-choice-name">{{ nameAndWhen(s).name }}</span>
<span class="merge-choice-tag">{{ canonicalId === s.id ? "keep" : "fold in" }}</span>
</label>
</div>