feat: prefer stable id when attaching demos; warn on name-only/collision (additive)#36
Open
Dukotah wants to merge 2 commits into
Open
feat: prefer stable id when attaching demos; warn on name-only/collision (additive)#36Dukotah wants to merge 2 commits into
Dukotah wants to merge 2 commits into
Conversation
…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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.id:<id>entry; old entries keep working.Changes
src/lib/db.ts—setLeadPreviewaccepts optionalid+matchKey, stores them inside the preview value JSON, and additionally indexes the same value under anid:<id>field in the existinglead_previewshash. The back-compat name-key entry is still written every time. NewidPreviewKey(id)helper;LeadPreview/extras gain optionalid/matchKey.src/app/api/crm/leads/route.ts— each lead resolves its preview by stable id first (previews["id:"+lead.id]), falling back topreviews[previewKey(l.name)]when there's no id hit. Behavior is identical whenever the id path misses.console.warnwith a count of demos matched by name only (no stable id).src/app/api/crm/admin/preview-url/route.tsandscripts/sync-demos-to-crm.mjs— read and pass throughid/matchKeyfrom the /websites manifest when present; absent = behaves exactly as today. The sync script also writes theid:<id>index entry alongside the name key.src/lib/crm/matchKey.ts(+ vitest) — shared canonical normalizer for the future cutover, documented as NOT wired intopreviewKey()(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 wordrealtyalongside the legal suffixllc, so the true output is"acme". The neither upstream file (scraper-app/contract/normalize.js, Websitesmatch-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 passednpx vitest run src/lib/db.test.ts— 5 passed (no regression)npx tsc --noEmit— clean (exit 0)🤖 Generated with Claude Code