Skip to content

feat(ux): human-readable titles — UX-1 of the work-topology plan - #157

Open
kevintseng wants to merge 2 commits into
mainfrom
feat/ux1-human-readable-titles
Open

feat(ux): human-readable titles — UX-1 of the work-topology plan#157
kevintseng wants to merge 2 commits into
mainfrom
feat/ux1-human-readable-titles

Conversation

@kevintseng

Copy link
Copy Markdown
Contributor

UX-1: human-readable titles (work-topology plan, PR 1 of 6)

Entities gain a nullable title column — the string a human (or agent) reads first. name keeps its identity as the stable machine dedup/append key and is no longer used as display anywhere.

Write side

  • ALTER-only migration in both schema owners (src/db.ts + the hook mirror in scripts/hooks/_shared.js). check-schema-drift.mjs cannot see ALTER blocks, so each side carries a comment locking it to the other.
  • All three write hooks generate heuristic titles via shared truncateTitle (120 chars): commit subject verbatim (post-commit), date+project+verb (session-summary, pre-compact). The weekly-summary compressor — the fourth entity-creation path — titles its digest rows too.
  • Every machine-generated title is stamped metadata.title_source='heuristic'. An unmarked title is treated as human-provided and permanent; the mark is what lets a future LLM titling pass (dreamer backfill) know what it may replace.
  • remember() / MCP / HTTP / CLI accept an optional title through the one shared Zod schema. Blank-after-sanitize collapses to undefined (= leave an existing title untouched — the namespace rule). CLI quick capture titles the memory with its own positional text.

Search side

  • entities_fts stays two-column: the title is folded into the observations feed by one private helper that both insertFtsRow and removeFromFts call, so a contentless delete can never build its match text differently from the insert that created the row.
  • The archived LIKE supplement matches e.title too — a memory findable by its title while active stays findable once archived.
  • One-time, fill-only, marker-keyed backfill (title_backfill_v1, modeled on the signal-score pass) titles pre-existing rows and reindexes what it titles — active rows only: archived rows have no FTS entry, and a contentless delete for text never indexed is itself the corruption.

Display side

  • displayTitle() in entity-display.ts is the single fallback chain: title → pickBestObservation → typeLabel+datenever name.
  • MemoryRow headline prefers the title; the raw-name meta span is removed (name survives as the headline's tooltip). LessonsTab cards, ProjectRoadmap rails/phase anchors/mindmap labels all route through the same helper. Browse/Lessons filter haystacks include the title so filtering by the visible headline matches.

Verification (all run at HEAD, this branch)

測試: node scripts/run-tests-isolated.mjs → exit=0
      Test Files 129 passed (129) / Tests 2024 passed (2024)   (main 基線: 1997/128)
typecheck: exit=0
verify:release: exit=0 (lint, version/mirror/doc-claims, audit, consumer advisories)

Mutation break-tests (all KILLED — two only after strengthening a vacuous probe)

  1. Backfill reindex deleted → survived the first probe: kg.search()'s status='active' JOIN hid the stale FTS tokens the raw index still held (delete failed with "database disk image is malformed" on stderr while the test stayed green). Probe rewritten against raw entities_fts MATCH → KILLED.
  2. Hook-side delete without previousTitle → KILLED by both the captureEntity symmetry test and the new mirror-parity title-fold case.
  3. Archived LIKE title arm removed → survived the backfill-derived fixture (backfilled titles ⊆ observations, so the o.content arm masks the title arm). New isolating test with a manual title whose token appears nowhere else → KILLED.

Audit baseline

UX-1's insertions shifted 18 line-keyed C5 entries — re-keyed with each statement verified at its new line, classifications unchanged. cli.ts needed collision-safe ordering (:171→:177 lands on the key :177→:183 vacates — the wrong-sibling hazard the 2026-08-13 note in those entries warns about). Two new C1 hits triaged: mirror-parity got a genuine count pin; db.test.ts's .toBe(0) is PRAGMA notnull flag equality, baselined with that reason.

Out of scope (per the reviewed plan)

LLM-generated titles (dreamer backfill, next), status/enum work (display-derived only), work-layer type whitelist (UX-4), M0 measurements.

Adds a nullable `title` column to entities: the display string a human
reads, distinct from `name` (which stays the stable machine dedup key).

Write side:
- ALTER-only migration in BOTH schema owners (src/db.ts and the hook
  mirror in scripts/hooks/_shared.js — check-schema-drift cannot see
  ALTER blocks, so the two are hand-locked with comments on each side)
- captureEntity + all three write hooks generate heuristic titles
  (commit subject verbatim; date+project+verb for session/compaction
  saves), marked metadata.title_source='heuristic' so a later LLM
  titling pass knows which titles it may replace — an unmarked title is
  treated as human-provided and permanent
- remember() / MCP / HTTP / CLI accept an optional title (shared Zod
  schema; blank collapses to undefined = leave-untouched, the namespace
  rule); CLI quick capture uses its positional text as the title
- weekly-summary compression (the fourth entity-creation path) titles
  its digest rows too

Search side:
- title is folded into the contentless-FTS feed by ONE function used by
  both insert and delete, so the delete can never build its match text
  differently from the insert (the stale-row-forever class)
- the archived LIKE supplement matches e.title as well — a memory
  findable by its title while active stays findable once archived
- one-time, fill-only, marker-keyed backfill titles pre-existing rows
  and reindexes what it titles (active rows only; archived rows have no
  FTS entry and a mismatched contentless delete corrupts the index)

Display side:
- MemoryRow headline prefers title (fallback: best observation, then
  type+date — never the machine name); the raw-name meta span is gone,
  the name survives as the headline's tooltip
- LessonsTab cards and ProjectRoadmap rails/mindmap/phase anchors use
  the same displayTitle helper; Browse/Lessons filter haystacks include
  the title so filtering by the visible headline matches

Tests: 2024 passed / 129 files (was 1997/128), typecheck clean. Three
mutation break-tests all KILLED — two of them only after the first probe
proved vacuous (kg.search()'s status filter hid stale FTS tokens the raw
index still held; the observation arm masked the title arm for backfilled
rows), so the tests now probe the raw index and use a manual-title
fixture respectively.
…C1 hits

UX-1's insertions shifted 18 line-keyed C5 baseline entries; each is
re-keyed with its statement verified at the new line, classifications
unchanged. cli.ts needed collision-safe ordering: the old :171 entry
(relationsCreated) moves onto :177, whose previous occupant
(relationErrors) moves to :183 — the exact wrong-sibling hazard the
2026-08-13 correction note in these entries describes.

C1: mirror-parity's new emptiness assertion gains a genuine count pin
(the token was demonstrably indexed before the re-title deletes it);
db.test.ts's hit is PRAGMA notnull flag equality, not set-emptiness —
baselined SAFE with that reason.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant