Skip to content

feat: prefer stable id when attaching demos; warn on name-only/collision (additive)#36

Open
Dukotah wants to merge 2 commits into
mainfrom
feat/join-demos-by-id
Open

feat: prefer stable id when attaching demos; warn on name-only/collision (additive)#36
Dukotah wants to merge 2 commits into
mainfrom
feat/join-demos-by-id

Conversation

@Dukotah

@Dukotah Dukotah commented Jun 25, 2026

Copy link
Copy Markdown
Owner

What & why

Today the CRM joins generated demo sites to leads purely by a normalized business name (previewKey). That fuzzy join silently mis-attaches when two businesses normalize to the same key, and there's no signal when it happens. This PR makes the join prefer a stable business id when one is available, and makes the remaining name-only matches and any collisions loud — all additive and backward-compatible.

Hard constraints honored

  • previewKey() is unchanged. Demo links already stored in Redis under the old name key are never orphaned or migrated. The id path is layered on top; name matching remains the fallback.
  • No Redis data migrated/rewritten. New writes simply also index an id:<id> entry; old entries keep working.
  • No email/outreach code touched. Nothing in the send path was modified; no auto-send anything.

Changes

  • src/lib/db.tssetLeadPreview accepts optional id + matchKey, stores them inside the preview value JSON, and additionally indexes the same value under an id:<id> field in the existing lead_previews hash. The back-compat name-key entry is still written every time. New idPreviewKey(id) helper; LeadPreview/extras gain optional id/matchKey.
  • src/app/api/crm/leads/route.ts — each lead resolves its preview by stable id first (previews["id:"+lead.id]), falling back to previews[previewKey(l.name)] when there's no id hit. Behavior is identical whenever the id path misses.
  • Loud, non-fatal observability (once per request, bounded):
    • console.warn with a count of demos matched by name only (no stable id).
    • previewKey collision detection — when 2+ distinct leads on a page normalize to the same key (the "wrong business gets another's demo" hazard), warn with a bounded sample, not per-row spam.
  • src/app/api/crm/admin/preview-url/route.ts and scripts/sync-demos-to-crm.mjs — read and pass through id/matchKey from the /websites manifest when present; absent = behaves exactly as today. The sync script also writes the id:<id> index entry alongside the name key.
  • src/lib/crm/matchKey.ts (+ vitest) — shared canonical normalizer for the future cutover, documented as NOT wired into previewKey() (doing so would orphan stored links).

Note on the matchKey test assertion

The task sketch suggested norm("Acme Realty LLC") === "acmerealty", but the canonical algorithm (kept verbatim for cross-repo parity) also strips the industry filler word realty alongside the legal suffix llc, so the true output is "acme". The neither upstream file (scraper-app/contract/normalize.js, Websites match-key.mjs) exists yet, so there is no existing parity to break — the algorithm is preserved as the contract-to-be and the test asserts its real behavior. norm("Joe's Cafe") === "joescafe" holds as specified.

Verification

  • npx vitest run src/lib/crm/matchKey.test.ts — 5 passed
  • npx vitest run src/lib/db.test.ts — 5 passed (no regression)
  • npx tsc --noEmit — clean (exit 0)

🤖 Generated with Claude Code

…ion (additive)

Additive id-preferred demo↔lead join. previewKey() is unchanged so demo links
already stored in Redis under the old name key are never orphaned.

- db.ts: setLeadPreview accepts optional id + matchKey, stores them in the value
  JSON, and ALSO indexes the preview under an `id:<id>` field in the same
  lead_previews hash (back-compat name key still written). New idPreviewKey helper.
- leads route: look up a lead's preview by stable id FIRST, fall back to the
  existing previewKey(name) when there's no id hit (behavior unchanged on a miss).
- Loud, non-fatal observability: warn once per request with a count of demos
  matched by name only (no stable id), and detect previewKey collisions (two
  distinct leads normalizing to the same key) with a bounded summary.
- preview-url route + sync-demos-to-crm.mjs: read and pass through id/matchKey
  from the /websites manifest when present; absent = behaves exactly as today.
- New shared canonical normalizer src/lib/crm/matchKey.ts (+ vitest) for the
  future cutover; NOT wired into previewKey() on purpose.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
duke Ready Ready Preview, Comment Jun 25, 2026 6:44pm

matchKey.ts now exports BOTH norm (loose suppression key, strips distinguishing
words) and matchKey (tight join key, strips only legal-entity forms and keeps
every distinguishing word) — byte-identical with scraper-app/contract/normalize.js
and Websites scripts/lib/match-key.mjs.

Still a future-cutover module: previewKey() is unchanged and remains the live
join, and sync/preview pass the manifest's matchKey value through unchanged
(never recompute with the loose key). Tests assert both functions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant