Skip to main content

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.html mock. 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), and realtor-mockups/leads.html. Format matched to acceptance/ai-front-desk.md and acceptance/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):

SurfaceUnderlying read
List rowsre_contacts (role-scoped) — display_name, pipeline_status, source_category, preferred_language, lead_score, last_touch_at, assigned_agent_id
Status chipre_contacts.pipeline_status
Source chipre_contacts.source_category (taxonomy data-model.md §4)
Language chipre_contacts.preferred_language (BCP-47, AI-captured)
Scorere_contacts.lead_score
Last touchre_contacts.last_touch_at + the originating re_interactions.channel
Smart Listsdynamic saved filters over re_contacts (tags/stage/language/source)
Drawer summaryre_contacts.ai_summary
Drawer timelinere_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 = true contact, 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 rollup data-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:

BucketRolesLeads scope
Brokerage managementbrokerage_owner, broker_admin (brokerage scope)All leads across the brokerage; an Agent column + filter; can assign/reassign
Team managementteam_admin (team scope)All leads within the team; assign/reassign within team
Agent / ISAagent, isa (own scope)Own assigned leads + the unassigned "team pond" (claimable), per routing config; cannot see another agent's assigned book
Support / externaltransaction_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"):
    1. Universal starters everyone gets: 🔥 Hot/act-today, ⏳ Slipping-away, New-unactioned, 🏷️ Sellers·valuation, Past-clients.
    2. 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_language auto-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_status with 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_status optimistically (skeleton/optimistic, not a spinner) and writes a status_change re_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

StateExpected output
No leadsDemo-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 matchesHonest "No leads match this list yet" with the list's criteria shown + a clear filter — not a blank pane.
LoadingTable/Kanban skeleton rows/cards, not a spinner (anti-pattern #7); optimistic on drag/sort.
Read errorInline "Couldn't load leads — retry" with Retry; the rail/toolbar stay usable.
Drawer load errorThe drawer shows an inline error; the list behind stays intact.
Suppressed contactRow + 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 with aria-sort reflecting 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) onto re_*; do not rebuild (do-not-reinvent.md).
  • Language is per-agent, never hardcoded — the single most important correctness rule here (design-direction.md; memory project_realestate_multilingual_voice).
  • Migrate-before-usere_* are proposals; verify columns via information_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.