← Back to AAI for MerusCase

Search Contacts New in 4.5.508

Firm-wide lookup by phone, email, or name. The flat contact index only carries last + first names, so phone and email searches require walking every case’s deep view record. This skill does that — finds the case where a phone or email lives, even when it’s buried on an applicant, employer, witness, or defense sub-record that the index doesn’t surface.

The names, phone numbers, and case references in this page are fictitious demonstration data. Your install runs against your real contacts.

On this page

What it is

At the prompt:

aaicase> find by phone 310-555-0100
aaicase> look up phone (555) 555-0166
aaicase> case for 3105550100
aaicase> who has phone 555-0177
aaicase> find email doctor@example.com
aaicase> search by email j.sample@example.com
aaicase> find contact Sample
aaicase> find Dr. Sample across all cases

About 30–60 seconds later you have every case where the phone, email, or name appears — with the role made explicit. If a phone is the applicant’s, the result tags it applicant. If it’s the employer’s, employer. Defense, witness, carrier, medical provider, attorney — each gets the right label so the attorney sees not just “this case has the number” but “this case has the number on the applicant record.”

Why a new skill — what find-contact couldn’t do

The flat /contacts/index endpoint returns minimal positional records: position 0 is last_name, position 1 is first_name. That’s it — no phone, no email, no address, no role. The pre-508 find contact skill searched only this index, so phone and email searches silently returned zero matches even when the value was clearly in Merus.

The gap that motivated this skill: A phone-number lookup against /contacts/index returned zero matches. The phone was sitting on a case applicant record at Applicant.Phone[0].digits — a deep field the index does not carry. Without the deep walk, the skill couldn’t find what was clearly there.

Search Contacts adds the second phase: after the flat index sweep, it walks /caseFiles/view/CASE_ID for every case in the firm and recursively traverses every nested string field. Phones, emails, addresses on applicants, employers, witnesses, defendants, carriers — anything stored as a string in the deep view record is searchable.

How it runs (under the hood)

The new bin aai-contact-find.mjs runs the two-phase search:

node bin/aai-contact-find.mjs --phone "3105550100"
node bin/aai-contact-find.mjs --email "doctor@example.com"
node bin/aai-contact-find.mjs --name "Sample"
node bin/aai-contact-find.mjs --any "QUERY"  # auto-detect shape

Auto-detection rules:

Phase 1 fetches /contacts/index (single endpoint, ~2k records, in-memory match). Phase 2 fetches /caseFiles/view/CASE_ID for every case in concurrent batches of 10, traverses every nested string field, and reports per-case matches with role attribution.

Two phases — index sweep + deep walk

Phase 1 — /contacts/index (name only, fast)

The flat contact index returns positional records: last_name at index 0, first_name at index 1. Phase 1 normalizes the query (NFD-decompose, strip accents, lowercase, strip non-alphanumeric) and matches against the normalized name fields. Same normalizer used in find contact, lib/binding.mjs, and merus-search.mjs — shared across the codebase so name matching is consistent.

Phase 1 finishes in under 2 seconds. It catches name matches at the contact-record level (good for “find Dr. Sample” queries). It does NOT catch phone or email queries — those fields are absent from the index.

Phase 2 — /caseFiles/view deep walk

For every case in the firm, fetch the deep /caseFiles/view/CASE_ID record. Recursively traverse every nested string field via a generator that yields each string with its dotted path (e.g. Applicant.Phone[0].digits"310-555-0100"). Normalize per query shape (digits-only for phone, lowercase for email, alphanumeric for name) and compare.

Concurrent batches of 10 keep total wall-clock to 30–60 seconds on a typical firm load (200–400 cases). Progress prints to stderr so the operator knows it’s working — not hung.

Phone and email normalization

Phones get all non-digit characters stripped before comparison, on both the query and the stored value. So all of these match each other:

The skill strips everything that isn’t a digit, so format mismatches between the way an attorney types a number and the way Merus stores it never block a match.

Emails get lowercased on both sides. Exact substring match on the normalized value, so doctor@example.com matches Doctor@Example.com and a search for the local-part alone (doctor) matches the email field but also matches anything else containing “doctor.”

Role inference — why the match surfaced

Every Phase 2 match includes the field path that surfaced it. The skill maps each path to a human-readable role:

Field path starts withRole
Applicant.applicant
Employer. or Employment.employer
Defendant. or contains Defendantdefendant
Witness. or contains Witnesswitness
contains Carrier or Insurancecarrier
contains Physician, Doctor, or QMEmedical
contains Attorney or Counselattorney
CaseDetailsDisplayParty.display-rollup
anything elseother

The role tells the attorney WHY this case surfaced. A phone match tagged applicant means the attorney’s client has that number. A match tagged medical means a treating doctor or QME. A match tagged display-rollup is usually a duplicate signal (the same value appearing in two roll-up paths) — the per-case deduplication step keeps each (role, value) pair once.

No cache — fresh data every time

Every invocation fetches fresh from Merus. No 5-minute cache, no overnight refresh, no “last good state.”

The deep walk costs 30–60 seconds per lookup. A cache would mask that cost but risk surfacing stale data right when freshness matters most — an attorney looking up a phone right after their assistant logged it. The trade-off is intentional: honest cost, fresh data. The progress signal on stderr lets the attorney see the work happening.

Two lookups in a row cost two full walks. If that becomes painful in practice, the bin supports a --quiet flag (suppress progress to stderr) and a --json flag (structured output suitable for piping). It deliberately does not support a cache flag.

Example output

$ aai-contact-find --phone "3105550100"
[aai-contact-find] phase 1: /contacts/index sweep...
[aai-contact-find] phase 1: 2014 contacts loaded
[aai-contact-find] phase 1: 0 matches at index level
[aai-contact-find] phase 2: fetching case index...
[aai-contact-find] phase 2: 219 cases to walk
[aai-contact-find] phase 2: 50/219 cases walked, 0 hits so far
[aai-contact-find] phase 2: 100/219 cases walked, 0 hits so far
[aai-contact-find] phase 2: 150/219 cases walked, 0 hits so far
[aai-contact-find] phase 2: 219/219 cases walked, 2 hits so far

CONTACT LOOKUP — "3105550100" (detected as phone)
────────────────────────────────────────────────────────────

Phase 2 — /caseFiles/view deep walk (1 case of 219 scanned, 2 field hits):
  Case #[CASE-#]    Sample, Alex
    applicant       310-555-0100   (Applicant.Phone[0].digits)
    display-rollup  310-555-0100   (CaseDetailsDisplayParty.Phone[0].digits)

One case found. Both field paths reported — the operator sees the phone surfaces twice on the same case (the applicant record and its display rollup), each tagged with the right role. The case_file_id is the actionable handle: open it directly to see the full picture.

When to run it

What it does NOT search

The deep walk covers structured fields in /caseFiles/view. It does NOT search:

A phone number mentioned only in a deposition transcript or an attorney’s note will not surface. For content-level search, fetch the relevant endpoint via merus-fetch and grep client-side. A future iteration may add --include-content; the current v1 is structured-field-only because content-level matches get noisy fast (phone numbers appear everywhere in transcripts).

Troubleshooting

The phone is in Merus but I get no matches

First check: is the phone actually on a structured field? If it’s only in an activity description body, the deep walk won’t catch it. Try fetching the case directly: node bin/merus-fetch.mjs /caseFiles/view/CASE_ID | grep "DIGITS" — that’ll show the path. Second: try the digits-only form. The skill normalizes both sides, but if you typed a number with too few digits (5-digit area code by mistake), the 7-digit minimum filter rejects it.

The walk is taking forever

The 30–60 second window is for a typical 200–400 case firm. For a 1000+ case firm, expect 2–3 minutes. Concurrent batches keep it bounded; the bottleneck is Merus latency, not parallelism. If your Merus instance is slow, the walk inherits that.

Multiple cases match the same phone

Common with carrier or defense-attorney numbers — one phone covers many cases. The output groups by case_file_id so each case’s context is clear. The role attribution helps: if the phone is tagged carrier across all matches, you’re looking at the carrier’s number; if it’s tagged applicant on one case but witness on another, you’re looking at someone who is the applicant in their own case and a witness in someone else’s.

I want to also search activity bodies / transcript text

Not in v1. Deferred because content-level matches get noisy. If you have a concrete use case, the right path is a follow-on pass with --include-content as an opt-in flag that sweeps /activities/index + selected upload metadata. Until then, fetch the specific endpoint and grep client-side.

Part of AAI for MerusCase — code-guarded AI case intelligence for California Workers’ Comp attorneys.