FUN-392 · verification record · 2026-07-24
On iframe-capable hosts (Claude.ai, ChatGPT) an ambiguous or no-match entity lookup now stops asking in chat and opens the right dashboard surface with the search box pre-seeded. Text-only hosts (Cursor, Gemini CLI, MCP inspector) are unchanged. This record captures what changed, the evidence behind it, and — honestly — what is not yet verified.
PR #796 → main 77 files · +2610 / −751 2991 / 0 tests green 29 acceptance criteria 5 review findings resolved
Every row is a real user path. Was is the behaviour before this PR; Now is what ships. Unless a row says all hosts, the change applies only to Claude / ChatGPT — text-only clients keep the Was column.
| Path | Was | Now (iframe host) |
|---|---|---|
| open_crews / contacts list_events — ambiguous name |
Chat asks “which one?” or prints a numbered list; nothing opens. | One line Found more than one [name]. Opening your [surface]. then the tab opens with the search box seeded to the typed name. No chat question. |
| open_crew / contact / event — name matches nothing |
Emitted no routing — nothing opened at all. | No [entity] found matching "[name]". Opening [list]... and the list opens with the term seeded. reverses a prior lock |
| create_event / _form — crew_name matches 2+ · all hosts |
A multi-match wrongly fired the no-match line. | Found more than one [crew]. Opening the event builder. (combined with the parsed-fields echo), crew row left empty. |
| add_people_to_event_form — ambiguous / no-match contact |
Recency guess (ambiguous_assumed_first) pre-selected a contact. |
Locked line fires, the Shared Picker opens seeded with the name, nothing pre-selected. |
| list_events search — all hosts |
Filtered client-side over page 1 only (25/pg) — a match on a later page was missed; box not clearable. | DB-side ilike on event_name (LIKE metachars escaped); the live box drives the query and clears to reveal all. |
| create_contact — name + phone, no confirmed |
Committed the contact immediately. | Opens the pre-filled Add-manually sheet; the organizer’s Save commits. A call carrying email or crew_id is exempt and commits directly (the 3-field prefill can’t round-trip those). |
| update_profile — standalone edit, no confirmed |
Committed the edit immediately. | Opens the Profile visual; the organizer’s Save commits Profile updated. A mid-create timezone capture still commits in one call via confirmed:true in its hint. |
| delete_crew · update_crew revoke_crew_link delete_contact · update_contact — ambiguous identifier |
Chat asked “I found N … Which one?”. | Found more than one [name]. Opening your [surface]. directs the follow-up that opens the filtered list. Never commits a delete/update on a disambiguation turn. |
8b6a9434. Every one of the 20 commits was gated on deno check / lint / fmt.search reaches the engine through listEventsHandler (O-01), and a redirect-not-commit assertion on the iframe profile path (O-04).FUN-392-verify-in-host.md.ui/CLAUDE.md requires before merge — see the last section.The full list, verbatim from the repo doc. Host scopes each AC; Status folds in the method — verified = green, and the amber rows are unit-proven but their live-host half is a Track-2 check. Method per section is on the grey group row.
| AC | Host | Criterion | Status |
|---|---|---|---|
| §1 · search_query + locked ambiguity line — unit + local render | |||
| AC-01 | iframe | A crew name matching 2+ crews returns exactly Found more than one family. Opening your crews. and opens the Crews tab seeded family — no chat question, no numbered list. | verified |
| AC-02 | iframe | Same for contacts (Opening your contacts.) and events (Opening your events.). | verified |
| AC-03 | iframe | The pre-filled search box is clearable — deleting the text reveals the full list. | verified |
| AC-04 | iframe | A second consecutive ambiguous lookup re-seeds the box to the new term (does not keep the old one). | verified |
| AC-05 | text-only | The same lookups return the existing text output — no search_query, no behaviour change. | verified |
| §2 · create_event ambiguous crew — unit | |||
| AC-06 | all | A crew_name matching 2+ crews narrates Found more than one [crew_name]. Opening the event builder. (with the parsed-fields echo) and opens the builder with the crew row empty — not the no-match line. | verified |
| AC-07 | all | A crew_name matching zero crews narrates No crew found matching "[crew_name]". Opening the event builder. (unchanged Jul-9 behaviour). | verified |
| §3 · add_people opener — unit | |||
| AC-08 | iframe | A contact_name matching 2+ contacts narrates Found more than one [name]. Opening your contacts., opens the Shared Picker seeded with the name, and pre-selects nothing. | verified |
| AC-09 | iframe | Matching one contact narrates Adding [name] to event: [event]. and the picker opens (no search pre-fill). | verified |
| AC-10 | iframe | Matching zero contacts narrates No contact found matching "[name]". Opening contacts... and the picker still opens (never a silent generic opener). | verified |
| §4 · no-match opens the list — unit reverses a prior lock | |||
| AC-11 | iframe | open_crew / _contact / _event with a name matching nothing opens the corresponding list seeded. Previously these emitted no routing and nothing opened. | verified |
| AC-12 | text-only | The same lookups return the not-found text with no surface change. | verified |
| §5 · UI search-box seeding — local Chrome (Track 1) | |||
| AC-13 | iframe | Crews tab with a seeded search_query shows the term in “Search crews and members” and filters the list to matches. | verified |
| AC-14 | iframe | Same for the Contacts tab (“Search contacts”). | verified |
| AC-15 | iframe | Events tab opens the search field automatically (seed non-empty) and filters the events list. | verified |
| AC-16 | iframe | The Shared Picker opens seeded from form_context.search_query with nothing pre-checked. | verified |
| §6 · events DB-side search — handler + engine unit | |||
| AC-17 | iframe | A seeded event lookup returns the matching event(s) even when the match is on a later page — filtered DB-side (ilike on event_name), not client-side over page 1. | unit ✓ · cross-page = Track 2 |
| AC-18 | all | search terms containing %, _ or \ match those characters literally (LIKE metacharacters escaped), not as wildcards. | logic ✓ · direct test deferred (O-05) |
| §7 · contact-create reversal (L53) — unit | |||
| AC-19 | iframe | create_contact with a name + phone and no confirmed does not commit; it opens the pre-filled Add-manually sheet (Creating contact: [name].). The organizer’s Save commits. | verified |
| AC-20 | iframe | A create carrying an email or a crew_id commits directly (exempt — the 3-field prefill can’t round-trip those). No data dropped. | verified |
| AC-21 | text-only | The two-step chat confirm (Add [name] with phone …? → confirmed:true) is unchanged. | verified |
| §8 · profile visual-first commit — unit | |||
| AC-22 | iframe | A standalone chat profile edit does not commit from chat — it opens the Profile visual (funlet_open_profile); the organizer’s Save commits Profile updated. No chat confirm turn. | verified |
| AC-23 | iframe | A first-time timezone capture mid-create (TIMEZONE_REQUIRED) commits in one call — its hints pass confirmed:true so the builder flow proceeds and the builder opens on retry (not routed through the Profile visual). | verified |
| AC-24 | text-only | The two-step confirm (confirmed:false → awaiting_confirmation → confirmed:true) is unchanged. | verified |
| AC-25 | all | A name-gate save (source:"name_gate") still commits single-step and the FUN-388 NAME_REQUIRED enforcement is intact. | verified |
| §9 · write-tool disambiguation — unit | |||
| AC-26 | iframe | delete_crew / update_crew / revoke_crew_link with an ambiguous crew_identifier returns Found more than one [name]. Opening your crews. and directs a follow-up that opens the filtered Crews list — no “Which one?”, no commit. | unit ✓ · open-rate = Track 2 |
| AC-27 | iframe | delete_contact / update_contact with an ambiguous contact_identifier does the same for the Contacts list (Opening your contacts.). | unit ✓ · open-rate = Track 2 |
| AC-28 | text-only | All five tools keep the existing chat disambiguation question (I found N … Which one: …?) with candidates — unchanged. | verified |
| AC-29 | all | None of the five tools commits a delete/update on a disambiguation turn (destructive-safety rule preserved). | verified |
| # | Finding | Sev | Resolution |
|---|---|---|---|
| O-01 | The search param was dropped in the funlet_list_events handler — inert end-to-end. | major | fixed 398042ea + handler test |
| O-02 | Events search box not clearable; didn’t drive the DB query. | major | fixed 2326403c · Chrome-verified |
| O-03 | Write-tool redirect is model-dependent (error + agent_hint, no routing _meta): if the host model skips the follow-up, the line shows but the list doesn’t open. | major | accepted Q1a best-effort; open-rate is a Track-2 check |
| O-04 | Standalone profile edit committed from chat on iframe hosts. | major | resolved 8b6a9434 — opens the Profile visual |
| O-05 | No repo-level test for LIKE-escaping; no UI tests for seed/clear. | minor | noted deferred |
Verified against the ticket’s authority: the description + comments, then the Notion link they cite (mirrored in funlet-docs). Older Notion docs are superseded. Two conventions QA checks byte-exact — the ambiguity name is not quoted; the no-match name is.
funlet-mcp first (no
Supabase-MCP writes are permitted from here). Until then, the routing _meta and the
write-tool redirect open-rate are proven only by unit tests and a local render — not by a live host.
Source of truth — FUN-392 ticket description + comments, then the Notion lock cited there (mirrored in funlet-docs as nl-visual-first-routing-universal-lock-2026-07-21.md).
Repo record — docs/urgent-2026-07/FUN-392-acceptance-criteria.md · FUN-392-verify-in-host.md · mcp-build-gap-log.md.
Verdict — code & automated suite verified against 8b6a9434; in-host round-trip pending a staging deploy. Generated 2026-07-24.