WiseAI Realtor — Leads Expected Output Spec
⛔ STATUS: DRAFT — NOT APPROVED. CLAUDE.md Rule #17 (HARD GATE) is NOT satisfied.
Stage-1 agent research, pre-populated from the realtor design docs + the
leads.htmlmock. Do not start the customer-facing build until the founder approves and confirms the open items in a Stage-2 interview.
Sourced from (read-only, 2026-06-29):
design-direction.md(Leads archetype = "List-first + Kanban toggle + right drawer"; Smart Lists; chip families; sortable-header rule),information-architecture.md(group A),data-model.md(re_contacts/re_interactions), andrealtor-mockups/leads.html. Format matched toacceptance/ai-front-desk.mdandacceptance/reviews-engine-mvp.md.
0. Scope — what this spec covers, and what it does NOT
This spec covers the Leads workspace of the WiseAI Realtor backend — the
list/table view, the Pipeline (Kanban) toggle, the Smart-List sidebar/tabs, the
filter/sort toolbar, and the right-drawer triage that opens on row click. It
is the sibling of realtor-dashboard-mvp.md, realtor-contact-record-mvp.md
(the full record the drawer's "open record" leads to), and realtor-inbox-mvp.md.
Does NOT cover: the full contact record page (that is
realtor-contact-record-mvp.md — the drawer is the triage tier, the record is
the deep tier of the same two-tier model); the conversation thread (inbox
spec); the dashboard (dashboard spec); lead capture behaviour (voice/chat →
local_business_leads, the approved ai-front-desk.md). Those are assumed and
cross-referenced.
Build posture: the leads inbox already exists for the local-business
platform (InboxTab.tsx); the RE Leads workspace extends that pattern onto
the re_* CRM spine — do not rebuild (do-not-reinvent.md). The re_* spine is
NEW (WS1a); until it ships, the list falls back to local_business_leads rows.
1. The mock screen this spec governs + what it reads
Governs: realtor-mockups/leads.html — Workflow-Rail shell with a Leads
header ("42 active · 12 new this week"), a Smart-List tab row (All / 🔥 Hot /
⏳ Slipping / 🏷️ Sellers / ਪੰ Punjabi) mirrored in the left rail, a
Filter/Sort toolbar, a List ⇄ Pipeline view toggle, a sortable-header data
table (Name, Status, Source, Lang, Score, Last touch, Next action, Agent), and
an open right drawer showing the selected lead's AI summary, chips, quick
actions (Call/Text/Email), key facts, and a unified timeline.
Reads (target-state CRM-spine APIs — WS1a; verify exact route names; do not fabricate):
| Surface | Underlying read |
|---|---|
| List rows | re_contacts (role-scoped) — display_name, pipeline_status, source_category, preferred_language, lead_score, last_touch_at, assigned_agent_id |
| Status chip | re_contacts.pipeline_status |
| Source chip | re_contacts.source_category (taxonomy data-model.md §4) |
| Language chip | re_contacts.preferred_language (BCP-47, AI-captured) |
| Score | re_contacts.lead_score |
| Last touch | re_contacts.last_touch_at + the originating re_interactions.channel |
| Smart Lists | dynamic saved filters over re_contacts (tags/stage/language/source) |
| Drawer summary | re_contacts.ai_summary |
| Drawer timeline | re_interactions WHERE contact_id = … ORDER BY occurred_at DESC |
| Drawer "open record" | → realtor-contact-record-mvp.md |
Demo tenant: Terry & Sheri Real Estate (…0c01, terry-and-sheri). The
mock's leads (Harjit Singh / Janet Lowe / Mei Chen / David Tran / Rob Bélanger /
Priya & Karan) are demo content on this tenant; NEVER a real customer tenant
(feedback_never_modify_customer_data).
2. The AI-Bridge / honesty anchor
- AI-captured fields are honest and editable. Score, summary, language, and source are AI/auto-captured but shown as data the human can correct — never an opaque verdict.
- Suppression gates the workspace. A
do_not_contact = truecontact, or one whose channel consent is withdrawn/expired, shows a suppression badge and its quick-action send/call affordances are disabled (audit P0 #3; consent rollupdata-model.md§1.1). No bulk action may contact a suppressed lead. - No advice in chips/labels. "Next action" is an action for the agent; it never asserts a value/legal/mortgage position.
3. Role-based visibility — assigned book vs team pond
Per data-model.md §3 (agent ownership axis: re_contacts.assigned_agent_id)
and the RBAC flags on local_business_agents:
| Bucket | Roles | Leads scope |
|---|---|---|
| Brokerage management | brokerage_owner, broker_admin (brokerage scope) | All leads across the brokerage; an Agent column + filter; can assign/reassign |
| Team management | team_admin (team scope) | All leads within the team; assign/reassign within team |
| Agent / ISA | agent, isa (own scope) | Own assigned leads + the unassigned "team pond" (claimable), per routing config; cannot see another agent's assigned book |
| Support / external | transaction_coordinator, marketing_assistant, external_partner (bounded) | Scoped/read-only subset; no reassignment; external_partner read-only |
Team Beckett: both owners → both see all. The agent-scope narrowing and the pond/claim behaviour must be verified by adding a non-owner agent to the demo tenant. Routing (round-robin / claim / pond / geo / language-based) is the audit's P1 lead-routing need — Beckett needs language-based routing between two agents; the Leads list must reflect whatever routing assigns.
The mock's Agent column (Sheri / Terry / —) is correct for a management view; an agent-scope user would not see the column populated with peers' names.
4. Expected outputs
4.1 — List-first table + the three chip families
Should see:
- A comfortable-density table (default ~44px rows; compact toggle remembered per view — anti-pattern #3) with whole-row click → drawer. Columns: selection checkbox, Name (+ secondary line "address · buyer/seller"), Status, Source, Lang, Score, Last touch, Next action, Agent.
- Three visually distinct chip families (design-direction component):
- Status — filled, stage color paired with a label (New=blue, Contacted=teal, Nurturing=amber, Appointment=violet, Offer=indigo, Won=green, Past client=gray) — colorblind-safe.
- Source — outline + icon (Referral, QR sign, Website, AI chat,
REALTOR.ca…), from
source_category. - Language — subtle pill (EN / FR / ਪੰ / 中文…), from
preferred_language.
- Last touch carries the channel ("2h ago · AI voice", "5h ago · AI chat") so the agent sees not just when but how.
- Next action is a concrete, color-cued cue ("Confirm Sun 2pm", "Call — valuation", "Re-engage · slipping", "Keep-in-touch").
Should NOT see:
- One undifferentiated chip style (the three families must be distinguishable).
- A row that doesn't open the drawer on click.
- Color-only status (every stage color is paired with a label).
Success: an agent can scan the table and, per row, read status + where it came from + language + score + how stale + what to do next — without opening it.
4.2 — Sortable headers, persisted per view (founder requirement)
Should see:
- Every relevant column is click-to-sort (Name, Status, Source, Lang, Score, Last touch, Agent) — the founder requirement (design-direction "Sortable table header", 2026-06-27). Neutral columns show a faint double-chevron on hover; the active sort column shows a single teal up/down arrow + bold label.
- The sort persists per view — switching Smart Lists/tabs and returning preserves the chosen sort; it is remembered across sessions per view.
Should NOT see:
- A header that looks sortable but isn't, or a sort that resets on navigation.
Success: sort state is real, obvious, and sticky per view — verified by sorting by Score, navigating away and back, and confirming the order + indicator persist (behaviour at ≥2 timepoints).
4.3 — Smart Lists (dynamic, per-agent, language NEVER hardcoded)
Should see:
- Smart Lists as dynamic auto-updating saved filters (not static groups), available both as the left-rail "Smart Lists" group and the tab row atop the table, each with a live count.
- Two tiers (design-direction "Smart Lists"):
- Universal starters everyone gets: 🔥 Hot/act-today, ⏳ Slipping-away, New-unactioned, 🏷️ Sellers·valuation, Past-clients.
- Per-agent personalized lists — including language-preference Smart
Lists generated from the languages THAT agent serves (their enabled
AI/voice languages) crossed with the
preferred_languageauto-captured on each lead. So Beckett sees "Punjabi", a GTA agent would see "Mandarin/Punjabi", a Montreal agent "French". A language is NEVER hardcoded — the "ਪੰ Punjabi" item in the mock is Beckett's instance, not a fixed feature.
- The agent can create / rename / reorder / pin / share custom lists (farm-area, source, price band, investors).
Should NOT see:
- A hardcoded language list (e.g. a literal "Punjabi" tab shipped to every
tenant regardless of the agent's languages) — this is the explicit anti-pattern
(
design-direction.md;project_realestate_multilingual_voice). - A "Smart List" that is actually a static, non-updating group.
Data touchpoints: language lists derive from local_business_agents.languages[]
(enabled per agent) ∩ re_contacts.preferred_language. Custom lists persist as
saved filter definitions (per data-model.md, Smart Lists = saved filters over
re_contacts tags/stage/language/source).
Success: adding a language to an agent's enabled set makes a corresponding language Smart List appear for that agent; removing it removes the list — proving the lists are market-driven, not hardcoded.
4.4 — List ⇄ Pipeline (Kanban) toggle
Should see:
- A List / Pipeline view toggle. Pipeline renders a Kanban over
re_contacts.pipeline_statuswith columns (New / Contacted / Nurturing / Appointment / Offer / Closed-Won / Lost), each header showing stage name, count, and summed value + a color bar; cards are draggable and show name, value, days-in-stage, next task, and a language chip (design-direction "Pipeline column"). - Dragging a card updates
pipeline_statusoptimistically (skeleton/optimistic, not a spinner) and writes astatus_changere_interaction. - The active Smart List / filters carry across both views.
Should NOT see:
- A Kanban that doesn't persist a drag, or that loses the current filter when toggled.
- Commission/value on cards for an agent-scope user (value summing must respect RBAC — §3 / record spec §commission).
Success: toggling to Pipeline shows the same filtered set as columns; a drag moves the lead's stage durably (verify at ≥2 timepoints).
4.5 — Row → right-drawer triage
Should see (on row click):
- A right drawer (one elevation/float, design-direction; not a modal-on-modal) with: header (avatar, name, "Buyer · address"), the chip cluster (status / source / language / score), quick actions Call / Text / Email, an "Aria's summary" AI panel (teal left accent, "updated Nh ago", 2–4 sentences + a ⚡ Next best action button), a key-facts grid (Budget / Looking for / Timeline / Phone), and a unified activity timeline (showing booked, AI voice call + transcript link, QR scan, first-seen) — newest first.
- An Open record → (or equivalent) into the full record page
(
realtor-contact-record-mvp.md) — the drawer is triage, the record is the deep dive (two-tier model, no tab-sprawl anti-pattern #1).
Should NOT see:
- A drawer that buries the AI summary below the fold or splits the record into tabs (anti-pattern #1).
- A Call/Text/Email action enabled for a suppressed contact (consent gate).
- Quick actions that send without the human (compose opens; nothing auto-sends).
Success: clicking a row opens a 20-second-comprehension drawer (summary + facts + timeline + actions); "open record" deep-links to the full page for the same contact.
4.6 — Filters, search, bulk select
Should see:
- Filter (by source/status/language/listing/campaign/agent/date — IA group A) and Sort controls; a Search leads… (⌘K) box; row checkboxes for bulk select.
- Bulk actions respect RBAC and consent (no bulk send/assign that an agent-scope user isn't allowed; suppressed contacts excluded from any bulk outbound).
Should NOT see:
- A bulk "email all" that bypasses consent/suppression or triggers an
unattended send (
feedback_never_cron_send_campaigns,feedback_db_writes_ok_sends_need_blessing).
5. Empty / loading / error states
| State | Expected output |
|---|---|
| No leads | Demo-seeded preview + onboarding checklist + one CTA ("Connect your voice line / import past clients to start capturing leads") — never a blank table (anti-pattern #6). |
| Smart List with 0 matches | Honest "No leads match this list yet" with the list's criteria shown + a clear filter — not a blank pane. |
| Loading | Table/Kanban skeleton rows/cards, not a spinner (anti-pattern #7); optimistic on drag/sort. |
| Read error | Inline "Couldn't load leads — retry" with Retry; the rail/toolbar stay usable. |
| Drawer load error | The drawer shows an inline error; the list behind stays intact. |
| Suppressed contact | Row + drawer show a suppression badge; send/call disabled with a tooltip reason. |
6. Accessibility (AODA → WCAG 2.1 AA)
- Table semantics: real
<table>header/row semantics; sort controls are buttons witharia-sortreflecting state; the active-sort indicator is not color-only (teal arrow + bold label). - Chips: status/source/language convey meaning by text, not color alone (colorblind-safe stage palette + label).
- Keyboard: rows openable via keyboard; the drawer traps focus, is dismissible with Esc, and returns focus to the originating row; Kanban drag has a keyboard alternative (move-to-stage menu).
- Contrast: ≥4.5:1 text; gold never used as text; teal action text/links meet AA.
- Motion: drag/skeleton animation respects
prefers-reduced-motion. - Targets: compact density keeps interactive targets at AA size.
7. Acceptance checklist (QA runs on the deployed URL)
Behavioural verification on wiseaiagency.com (real host) against the demo
tenant (…0c01); sample at ≥2 timepoints; assert change — not "build passes".
// List + chips
test.fixme('rows show three distinct chip families (status filled+labelled, source outline+icon, language pill)', () => {});
test.fixme('whole row is clickable and opens the right drawer; status colors are paired with labels (colorblind-safe)', () => {});
// Sortable headers persist per view
test.fixme('every relevant column sorts; active column shows teal arrow + bold label, neutral shows hover double-chevron', () => {});
test.fixme('sort persists across Smart-List switches and across sessions for that view', () => {});
// Smart Lists — language NEVER hardcoded
test.fixme('universal starter lists (Hot/Slipping/New/Sellers/Past) render with live counts', () => {});
test.fixme('a language Smart List appears ONLY for agents who serve that language; none are hardcoded per-tenant', () => {});
test.fixme('adding a language to an agent enables a matching language list; removing it removes the list', () => {});
test.fixme('agent can create/rename/reorder/pin/share a custom list and it persists', () => {});
// Kanban
test.fixme('List⇄Pipeline toggle preserves the active filter; Kanban columns show count + summed value + color bar', () => {});
test.fixme('dragging a card durably updates pipeline_status and writes a status_change interaction', () => {});
// Drawer → record
test.fixme('row→drawer shows Aria summary + key facts + unified timeline + quick actions; "open record" deep-links the full page', () => {});
test.fixme('Call/Text/Email are DISABLED for a do_not_contact contact; no bulk action contacts a suppressed lead', () => {});
// RBAC
test.fixme('agent-scope user sees only own assigned + team pond; cannot see a peer’s assigned book; no commission/value leak', () => {});
// States + a11y
test.fixme('empty/0-match/loading/error states render per §5 (never blank, never a full-page spinner)', () => {});
test.fixme('table is keyboard-navigable; aria-sort reflects state; drawer traps+restores focus; reduced-motion honored', () => {});
8. Guardrails for agents building against this spec
- Rule #17 not satisfied — do not build until founder approval.
- Extend the existing inbox/list pattern (
InboxTab.tsx) ontore_*; do not rebuild (do-not-reinvent.md). - Language is per-agent, never hardcoded — the single most important
correctness rule here (
design-direction.md; memoryproject_realestate_multilingual_voice). - Migrate-before-use —
re_*are proposals; verify columns viainformation_schema(Rule #18); founder-gated DDL. - RBAC + consent gate every action — assigned/pond scoping; suppression disables send/call; no unattended sends (Rule: sends need founder/owner press).
- Honest metrics; verify on the real host; behaviour not DOM presence; evidence-or-nothing.
- If code diverges, update the spec first (founder approval), then the code.
End of spec. STATUS: DRAFT — NOT APPROVED. Open items for Stage-2: (1) the team-pond / claim + routing rules (round-robin vs claim vs language-based) that drive agent-scope visibility; (2) which universal starter Smart Lists ship in the MVP; (3) the exact bulk-action set and its consent gating.