nexus/.planning/phases/24-search-history-branching/24-VERIFICATION.md

224 lines
20 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
phase: 24-search-history-branching
verified: 2026-04-01T00:00:00Z
status: gaps_found
score: 3/4 success criteria verified
gaps:
- truth: "User can export any conversation as a Markdown file or as a JSON file"
status: partial
reason: "Server supports both formats (markdown + json) and chatApi.exportConversation accepts format param, but ChatPanel only renders a single 'Export as Markdown' button (calls handleExport('markdown')). No UI control triggers JSON export."
artifacts:
- path: "ui/src/components/ChatPanel.tsx"
issue: "Only one export button at line 261: onClick={() => handleExport('markdown')}. JSON export path (handleExport('json')) is never called from any UI element."
missing:
- "Add a second export button (or dropdown) in ChatPanel header to trigger handleExport('json') — the handler, API client method, server route, and service all exist and work; only the UI trigger is missing"
human_verification:
- test: "Confirm Cmd+K -> 'Search chat messages' -> type query -> click result scrolls to the correct message"
expected: "Search overlay opens, results appear for matching terms within ~500ms, clicking a result switches to that conversation and the virtualizer scrolls to the target message"
why_human: "Cannot test keyboard shortcuts, overlay rendering, or virtualizer scroll behavior programmatically without running the app"
- test: "Confirm bookmark toggle persists: hover a message, click the bookmark icon (fills solid), refresh page, verify icon is still filled"
expected: "Bookmark state survives page reload, indicating the POST /conversations/:id/bookmarks endpoint is being called and the DB write is real"
why_human: "Requires live browser interaction and server running"
- test: "Confirm branch-on-edit: send two messages, click edit on the first, submit — verify a new branch conversation appears in the sidebar with a GitBranch icon"
expected: "Branch conversation is created, sidebar shows it indented under the original with a GitBranch indicator, branch selector bar appears above the message list"
why_human: "Requires live server, active agent streaming session, and visual inspection of the sidebar grouping"
- test: "Confirm Markdown export downloads a .md file with agent names (not UUIDs) in the headers"
expected: "Browser downloads a file named <slug>-<date>.md; assistant messages show agent name (e.g. 'Brainstormer') not a UUID"
why_human: "Requires browser file download and manual inspection of content"
---
# Phase 24: Search, History & Branching — Verification Report
**Phase Goal:** Users can find any message across all conversations in under 500ms, export conversations, bookmark key messages, and branch from any point in a conversation
**Verified:** 2026-04-01
**Status:** gaps_found — 1 gap blocking complete goal achievement
**Re-verification:** No — initial verification
---
## Goal Achievement
### Success Criteria (from ROADMAP.md)
| # | Criterion | Status | Evidence |
|---|-----------|--------|----------|
| 1 | Cmd+K opens search overlay; results returned in <500ms across 10,000+ messages | ? HUMAN NEEDED | FTS pipeline fully wired (tsvector GIN index plainto_tsquery ts_rank ChatSearchDialog CommandPalette custom event); runtime perf requires human test |
| 2 | User can bookmark any message and navigate to bookmarked messages | VERIFIED | ChatMessageBookmark renders on every message via ChatMessageActions; useToggleBookmark mutation calls POST /bookmarks; ChatBookmarkList with onNavigate wired into ChatPanel |
| 3 | Editing a message with a response creates a branch; user can switch branches | VERIFIED | handleEdit checks editedIdx < messages.length - 1 then calls chatApi.branchConversation; ChatBranchSelector rendered when branches.length > 0; ChatConversationList shows GitBranch icon + pl-4 indent |
| 4 | User can export as Markdown **or** JSON | ✗ FAILED | Server route and service support both formats; chatApi.exportConversation accepts format param; ChatPanel only exposes markdown button — JSON export has no UI trigger |
**Score:** 3/4 success criteria verified (criterion 1 needs human runtime check; criterion 4 has a confirmed code gap)
---
## Required Artifacts
### Plan 00 — DB Migrations, Schema, Shared Types
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `packages/db/src/migrations/0050_add_branch_columns.sql` | Branch columns migration | ✓ VERIFIED | Contains `parent_conversation_id` FK + `branch_from_message_id` + index |
| `packages/db/src/migrations/0051_add_message_search_vector.sql` | tsvector + GIN index migration | ✓ VERIFIED | `content_search` generated column + GIN index present |
| `packages/db/src/migrations/0052_create_chat_message_bookmarks.sql` | Bookmarks table migration | ✓ VERIFIED | Table + two compound indexes for company+message and company+conversation |
| `packages/db/src/schema/chat_message_bookmarks.ts` | Drizzle schema for bookmarks | ✓ VERIFIED | `chatMessageBookmarks` exported; compound indexes defined |
| `packages/db/src/schema/chat_conversations.ts` | Branch columns added | ✓ VERIFIED | `parentConversationId` with `AnyPgColumn` for self-referential FK; `branchFromMessageId`; `parentIdx` |
| `packages/db/src/schema/index.ts` | Re-exports chatMessageBookmarks | ✓ WIRED | Line 61: `export { chatMessageBookmarks } from "./chat_message_bookmarks.js"` |
| `packages/shared/src/types/chat.ts` | Search, bookmark, branch types | ✓ VERIFIED | `ChatMessageSearchResult`, `ChatBookmark`, `ChatBookmarkWithMessage`, `ChatBookmarkToggleResponse`; `parentConversationId` on `ChatConversation` |
| `packages/shared/src/validators/chat.ts` | searchMessagesSchema, branchConversationSchema | ✓ VERIFIED | Both validators present with correct shapes |
| `packages/shared/src/index.ts` | Re-exports new types and validators | ✓ WIRED | Lines 565566, 581: all new symbols exported |
| `server/src/__tests__/chat-service.test.ts` | Wave 0 test stubs | ✓ VERIFIED | 4 describe blocks with it.todo entries (searchMessages, toggleBookmark, branchConversation, exportConversation) |
### Plan 01 — Server Service Methods and Routes
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `server/src/services/chat.ts` | Six service methods | ✓ VERIFIED | searchMessages (tsvector FTS), toggleBookmark (transactional), getBookmarks (join), branchConversation (message copy), listBranches, exportConversation (agent LEFT JOIN) |
| `server/src/routes/chat.ts` | Six route handlers | ✓ VERIFIED | GET /messages/search, POST /bookmarks, GET /bookmarks, POST /branch, GET /branches, GET /export — all with assertBoard guard |
### Plan 02 — UI API Client, Hooks, Components
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `ui/src/api/chat.ts` | Six new chatApi methods | ✓ VERIFIED | searchMessages, toggleBookmark, getBookmarks, branchConversation, listBranches, exportConversation (returns URL string) |
| `ui/src/hooks/useChatSearch.ts` | Debounced FTS query hook | ✓ VERIFIED | placeholderData, staleTime: 30_000, enabled when query >= 2 chars |
| `ui/src/hooks/useChatBookmarks.ts` | Bookmark query + mutation hooks | ✓ VERIFIED | useChatBookmarks + useToggleBookmark; invalidates both `["chat","bookmarks"]` and `["chat","search"]` |
| `ui/src/components/ChatSearchDialog.tsx` | CommandDialog FTS overlay | ✓ VERIFIED | Uses CommandDialog, shouldFilter={false}, useChatSearch, HighlightedText (XSS-safe) |
| `ui/src/components/ChatMessageBookmark.tsx` | Bookmark toggle button | ✓ VERIFIED | fill-current on isBookmarked; aria-label toggles; ghost h-6 w-6 sizing |
| `ui/src/components/ChatBookmarkList.tsx` | Scrollable bookmark list | ✓ VERIFIED | useChatBookmarks; skeleton loading; empty state; onNavigate callback |
| `ui/src/components/ChatBranchSelector.tsx` | Horizontal branch picker | ✓ VERIFIED | GitBranch icon; bg-accent for active; renders null when no branches |
### Plan 03 — Integration Wiring
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `ui/src/context/ChatPanelContext.tsx` | scrollToMessageId state | ✓ VERIFIED | State + setter in interface, provider value, and useState |
| `ui/src/components/ChatPanel.tsx` | Full feature integration | ✓ VERIFIED | ChatSearchDialog, ChatBranchSelector, export (markdown only — see gap), bookmarks panel, branch-on-edit, useToggleBookmark, bookmarkedMessageIds Set |
| `ui/src/components/ChatMessageList.tsx` | Scroll-to-message support | ✓ VERIFIED | useEffect on scrollToMessageId; virtualizer.scrollToIndex(index, {align:"center"}); resets to null after scroll |
| `ui/src/components/ChatMessageActions.tsx` | Bookmark button on messages | ✓ VERIFIED | ChatMessageBookmark rendered as last action; onBookmark + isBookmarked props |
| `ui/src/components/ChatMessage.tsx` | Bookmark prop threading | ✓ VERIFIED | onBookmark={id && onBookmark ? () => onBookmark(id) : undefined} — real messageId passed |
| `ui/src/components/CommandPalette.tsx` | "Search chat messages" command item | ✓ VERIFIED | value="search-chat"; dispatches `nexus:open-chat-search` custom event |
| `ui/src/components/ChatConversationList.tsx` | Branch indicators | ✓ VERIFIED | GitBranch icon + pl-4 indent when parentConversationId is non-null |
---
## Key Link Verification
| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| `packages/db/src/schema/chat_message_bookmarks.ts` | `packages/db/src/schema/index.ts` | re-export | ✓ WIRED | Line 61 of schema/index.ts |
| `packages/shared/src/types/chat.ts` | `packages/shared/src/index.ts` | re-export | ✓ WIRED | Lines 565581 |
| `server/src/routes/chat.ts` | `server/src/services/chat.ts` | svc.searchMessages, svc.toggleBookmark, svc.branchConversation, svc.exportConversation | ✓ WIRED | All four service calls confirmed in route handlers |
| `server/src/services/chat.ts` | `packages/db/src/schema/chat_message_bookmarks.ts` | import chatMessageBookmarks | ✓ WIRED | Line 3 of services/chat.ts |
| `ui/src/hooks/useChatSearch.ts` | `ui/src/api/chat.ts` | chatApi.searchMessages | ✓ WIRED | Line 7 of useChatSearch.ts |
| `ui/src/components/ChatSearchDialog.tsx` | `ui/src/hooks/useChatSearch.ts` | useChatSearch | ✓ WIRED | Line 3 import + line 78 usage |
| `ui/src/components/ChatMessageBookmark.tsx` | `ui/src/hooks/useChatBookmarks.ts` | useToggleBookmark | ✓ WIRED | Indirectly — onToggle is wired at ChatPanel → handleBookmark → toggleBookmark; ChatMessageBookmark itself only needs onToggle callback |
| `ui/src/components/CommandPalette.tsx` | `ui/src/components/ChatSearchDialog.tsx` | nexus:open-chat-search custom event | ✓ WIRED | CommandPalette dispatches; ChatPanel listens and sets searchOpen(true) |
| `ui/src/context/ChatPanelContext.tsx` | `ui/src/components/ChatMessageList.tsx` | scrollToMessageId | ✓ WIRED | useChatPanel() in ChatMessageList; useEffect scrolls virtualizer |
| `ui/src/components/ChatPanel.tsx` | `ui/src/components/ChatBranchSelector.tsx` | branches data from useQuery | ✓ WIRED | listBranches query feeds ChatBranchSelector; onSelectBranch calls setActiveConversationId |
| `ui/src/components/ChatMessage.tsx` | `ui/src/components/ChatMessageBookmark.tsx` (via ChatMessageActions) | onBookmark prop chain | ✓ WIRED | ChatPanel → handleBookmark → ChatMessageList → ChatMessage → ChatMessageActions → ChatMessageBookmark.onToggle |
| `ui/src/components/ChatPanel.tsx` | JSON export | handleExport("json") | ✗ NOT_WIRED | handleExport callback accepts "json" but no UI element calls it; only markdown button rendered |
---
## Data-Flow Trace (Level 4)
| Artifact | Data Variable | Source | Produces Real Data | Status |
|----------|---------------|--------|--------------------|--------|
| `server/src/services/chat.ts` :: searchMessages | rows | Drizzle query with tsvector @@ plainto_tsquery, ts_rank ORDER BY, LIMIT | Yes — real DB join + FTS WHERE clause | ✓ FLOWING |
| `server/src/services/chat.ts` :: toggleBookmark | existing | SELECT then INSERT or DELETE in transaction | Yes — real DB read-modify-write | ✓ FLOWING |
| `server/src/services/chat.ts` :: branchConversation | messagesToCopy | SELECT lte(createdAt) + INSERT returning | Yes — real DB copy + returns new conversation | ✓ FLOWING |
| `server/src/services/chat.ts` :: exportConversation | rows | SELECT + LEFT JOIN agents | Yes — all messages + agent names from DB | ✓ FLOWING |
| `ui/src/hooks/useChatSearch.ts` | data | chatApi.searchMessages → GET /companies/:id/messages/search | Yes — enabled when query >= 2 chars, real endpoint | ✓ FLOWING |
| `ui/src/hooks/useChatBookmarks.ts` | data | chatApi.getBookmarks → GET /companies/:id/bookmarks | Yes — real endpoint | ✓ FLOWING |
| `ui/src/components/ChatPanel.tsx` :: bookmarkedMessageIds | Set<string> | useChatBookmarks(companyId, activeConversationId).data | Yes — rebuilt per-conversation from server data | ✓ FLOWING |
| `ui/src/components/ChatPanel.tsx` :: branches | ChatConversation[] | useQuery → chatApi.listBranches → GET /conversations/:id/branches | Yes — real endpoint | ✓ FLOWING |
| `ui/src/components/ChatPanel.tsx` :: JSON export | — | handleExport("json") | N/A — UI trigger missing | ✗ DISCONNECTED (no UI entry point) |
---
## Behavioral Spot-Checks
| Behavior | Method | Result | Status |
|----------|--------|--------|--------|
| All 8 Phase 24 commits exist in git | git log for 8 commit hashes | All 8 found (430bbbb8 through 2b526e78) | ✓ PASS |
| Migration journal has 0050/0051/0052 entries | grep in _journal.json | All three tags present | ✓ PASS |
| Shared package exports key symbols | node -e require check | ChatMessageSearchResult, ChatBookmark, searchMessagesSchema, branchConversationSchema — all FOUND | ✓ PASS |
| searchMessages has real FTS WHERE clause | grep in service | `"content_search" @@ plainto_tsquery` with ts_rank ORDER BY found | ✓ PASS |
| export route sets Content-Disposition header | grep in routes | Line 273: res.setHeader("Content-Disposition", ...) confirmed | ✓ PASS |
| JSON export has UI trigger in ChatPanel | grep for handleExport("json") | Only markdown button at line 261; no JSON trigger | ✗ FAIL |
---
## Requirements Coverage
| Requirement | Source Plan(s) | Description | Status | Evidence |
|-------------|---------------|-------------|--------|----------|
| CHAT-07 | 00, 01, 02, 03 | Full-text search across all conversations | ✓ SATISFIED | tsvector GIN index, plainto_tsquery service, ChatSearchDialog, Cmd+K integration all wired |
| CHAT-13 | 00, 01, 02, 03 | Message bookmarks: mark important messages | ✓ SATISFIED | ChatMessageBookmark on every message, toggleBookmark service+route, ChatBookmarkList navigation |
| CHAT-14 | 01, 03 | Conversation branching on edit | ✓ SATISFIED | branchConversation service, POST /branch route, handleEdit branch-on-edit logic, ChatBranchSelector |
| HIST-04 | 00, 01, 02, 03 | Conversation export as Markdown or JSON | ✗ BLOCKED | Server and API client support both formats; ChatPanel only exposes Markdown button — JSON has no UI trigger |
| PERF-04 | 01 | FTS returns results <500ms across 10,000+ messages | ? NEEDS HUMAN | GIN index in place; ts_rank ordering correct; runtime perf requires load test or browser timing |
### Phantom Requirement IDs in Plan Frontmatter
The following requirement IDs appear in plan frontmatter (`24-00-PLAN.md` through `24-03-PLAN.md`) but **do not exist** in `REQUIREMENTS.md`:
- `HIST-07`, `HIST-08`, `HIST-09`, `HIST-10`, `HIST-11`, `HIST-12`
`REQUIREMENTS.md` defines only HIST-01 through HIST-06. These IDs were invented in the plan documents but have no corresponding requirement definitions. The authoritative requirement set for Phase 24 per ROADMAP.md is: **CHAT-07, CHAT-13, CHAT-14, HIST-04, PERF-04** all of which are accounted for above.
---
## Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| `ui/src/components/ChatMessageActions.tsx` | 37, 71 | `messageId=""` and `conversationId=""` passed to `ChatMessageBookmark` | Info | Not a functional stub `ChatMessageBookmark` does not use these props in its body (destructures only `isBookmarked` and `onToggle`); real messageId flows via `onToggle` callback. Props are dead interface noise, not a bug. |
| `ui/src/components/ChatMessageList.tsx` | (comment) | `// TODO: if message not found, best-effort only` | Info | Intentional plan specified this as acceptable for scroll-to on unpaginated messages. Does not block any success criterion. |
No blocking stubs found. All core implementations contain real DB queries, real HTTP calls, and real state management.
---
## Human Verification Required
### 1. Full-text search round-trip with scroll-to-message
**Test:** Press Cmd+K in the Nexus UI. Select "Search chat messages". Type a search term that exists in a past conversation message. Verify results appear with conversation title and message snippet. Click a result.
**Expected:** ChatSearchDialog closes; the chat panel switches to the target conversation; the virtualizer scrolls to the target message (centered in view) within ~500ms total.
**Why human:** Keyboard shortcut dispatch, dialog rendering, virtualizer scroll, and sub-500ms timing cannot be verified without a running browser session.
### 2. Bookmark persistence across page reload
**Test:** Open a conversation. Hover a message. Click the bookmark icon (should fill solid). Reload the page. Return to the same conversation and hover the same message.
**Expected:** The bookmark icon is still filled, confirming the DB write via POST /conversations/:id/bookmarks persisted and the GET /companies/:id/bookmarks query on reload returns it.
**Why human:** Requires live server, DB write, and page reload cycle.
### 3. Branch-on-edit creates visible branch in sidebar
**Test:** In a conversation with at least two messages (one user, one assistant reply), click edit on the user message, change the text, and submit.
**Expected:** A new branch conversation appears in the sidebar with a GitBranch icon and pl-4 indent under the original. The branch selector bar appears above the message list. Clicking "Original" in the branch selector switches back to the original conversation.
**Why human:** Requires an active agent session, streaming, and visual sidebar inspection.
### 4. Markdown export produces correct agent names (not UUIDs)
**Test:** In a conversation that involved a named agent, click the Download icon in the chat header. Open the downloaded .md file.
**Expected:** The file is named `<title-slug>-<date>.md`; assistant messages show the agent's name (e.g. "Brainstormer") not a UUID; user messages show "You"; format matches `**Speaker** (timestamp)\ncontent\n\n---`.
**Why human:** Requires browser file download and manual inspection of content.
---
## Gaps Summary
**1 gap blocks full goal achievement:**
**JSON export has no UI trigger (Success Criterion 4, HIST-04)**
The complete JSON export pipeline exists the server service (`exportConversation`), the Express route (`GET /conversations/:id/export?format=json`), and the API client method (`chatApi.exportConversation(id, "json")`) all work correctly. The `handleExport` callback in ChatPanel also accepts "json" as a format. However, `ChatPanel.tsx` only renders one export button (line 261) that hardcodes `handleExport("markdown")`. No UI element triggers `handleExport("json")`.
**Fix required:** Add a second export button (or a dropdown with two options) in the ChatPanel header that calls `handleExport("json")`. The server, service, route, API client, and handler are all already correct only the UI trigger is missing.
---
_Verified: 2026-04-01_
_Verifier: Claude (gsd-verifier)_