Skip to content

fix(lead): the duplicate banner covers a confirmed duplicate, in words that fit both verdicts - #1626

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-1289-duplicate-banner-covers-confirmed
Sep 6, 2026
Merged

fix(lead): the duplicate banner covers a confirmed duplicate, in words that fit both verdicts#1626
os-steve merged 1 commit into
mainfrom
claude/issue-1289-duplicate-banner-covers-confirmed

Conversation

@os-steve

@os-steve os-steve commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Part of #1289 — deliberately not Fixes. The card has two halves; this PR
delivers the record-page half, and the flow half's premise is gone. See
"What is not here" below. The card should be closed by a person who has read
that section, not by this merge.

Base measured at ff433216.

The banner now covers every verdict

src/pages/lead_detail.page.ts's record:alert was gated on
duplicate_status == "suspected", so the stronger state got no banner at
all: a confirmed duplicate is a reviewer's verdict, and the record page's
alarm stayed silent on it while firing on the machine's guess.

That is worse now than when #1207 set the boundary. Since #1288 a confirmed
duplicate cannot be convertedConvert opens a refusal instead of the
conversion form. So the one state the page was quiet about was also the one
state that stops the rep's next click: they pressed Convert and met a refusal
dialog with no warning on the record behind it.

Both guards are still here, and they still fail in opposite directions

Unchanged and load-bearing, restated because it is the property easiest to lose
while editing the comparison beside it:

surface mechanism an unevaluable predicate ⇒
lead_detail.page.ts record:alert visible client CEL, useCondition FAIL-SOFT — banner SHOWN on every clean lead
lead-conversion.flow.ts edges e21/e22/e25 server CEL, flow condition THE RUN FAILS — an ordinary lead becomes unconvertible

The flow's guards are untouched by this PR. The page's guard is untouched in
substance: has(...) still stands in front of the comparison, and
test/lead-duplicate-visibility.test.ts still proves it is not decorative by
deriving the unguarded spelling from the shipped predicate itself and watching
it fault.

⚠️ The predicate is NOT a bare has(...), and that was measured

The obvious spelling of "any set value" — has(record.duplicate_status) alone,
which is what the card asks for in as many words — is wrong on this surface.
has() is true for a key that is present and null, and a clean lead comes
back from driver-sql as exactly that. Measured on @objectstack/formula:

has(record.duplicate_status)                        vs { duplicate_status: null }
  => { ok: true, value: true }        # banner on every clean driver-sql lead
has(record.duplicate_status) && record.duplicate_status != null
  => { ok: true, value: false }       # what shipped

So dropping the comparison reaches the same cry-wolf banner as dropping the
guard, just by the other road. Ablation, run against the committed tree with the
mutation proved on disk by blob hash and the restore proved the same way:
replacing the shipped predicate with the bare has(...) turns two pins red
— the driver-sql clean lead starts showing the banner, and the load-bearing
pin reports the guard has become the whole predicate.

Copy: it may name neither verdict

One banner now covers two states that mean different things, with one title and
one body and no per-state channel (record:alert carries a single visible,
and pickLocalized picks by language, not by row). So the copy names the fact
both states share and points at Duplicate Status for the verdict and
Duplicate Management for the record it repeats, instead of asserting one.
Keeping #1207's "Intake flagged this lead" would have told every reviewer that
their finished verdict was a machine's guess.

All four locales. A new pin holds the copy to naming neither verdict and reads
the forbidden words out of the locale packs rather than hard-coding them, so
renaming an option re-aims the assertion instead of quietly retiring it.

What is not here — the flow half's premise is gone

The card's ruling asks to widen a second predicate: the per-run warning line on
lead-conversion.flow.ts. I did not, and it should not be done. The card
measured against PR #1286 / 413d9640; at ff433216 the flow no longer has the
shape the ruling describes. #1288 — which the card lists as an open decision —
has been decided, implemented and documented. The decision node now has
three out-edges, not two:

  • e21 suspectedwarn_duplicate (warn, allow)
  • e25 confirmedrefuse_confirmed_duplicate (refuse)
  • e22 everything else → no_duplicate_warning

Widening e21 to any set value would make a confirmed lead match e21 and
e25. A decision node that declares no config.conditions takes every
out-edge whose condition holds, in parallel — the flow's own comment records
this being measured, and records that the same non-partition once "would have
shown the refusal and converted the lead in the same run". So the literal edit
would have re-opened conversion on confirmed duplicates: it would have silently
reverted #1288, implementing the block-versus-warn question the card puts
explicitly out of scope, in the reverse direction. It also contradicts AGENTS.md
metadata-semantics rule 8.

The hazard the card names is closed on that surface regardless: a confirmed
duplicate at conversion now gets a refusal, which is strictly more than the
warning the ruling asked for. Nothing is left undone there — but confirming that
a refusal discharges the ruling is the PM's call, which is why this is
Part of and not Fixes.

Gates

pnpm verify green at the tip of this branch — all eight stages
(validate typecheck lint lint:i18n-gate hygiene hygiene:tokens build test),
160 test files, 3395 passed / 1 skipped. i18n gate: 0 i18n/missing-*.
Token ratchet clean; interaction layer ~37,572 of ~40,000.

test/lead-duplicate-visibility.test.ts is the suite that judges this change,
and it was run red first: with the two pins updated and the predicate still
suspected-only, it failed exactly twice — confirmed answering false, and
the en copy still saying "Suspected". Both went green on the widening and on
nothing else.


Generated by Claude Code

The `record:alert` on the lead detail page was gated on
`duplicate_status == "suspected"`, so the stronger state — a reviewer's
`confirmed` verdict — got no banner at all. Since the conversion flow
began refusing outright on `confirmed`, that was also the one state whose
only announcement was the refusal dialog itself.

Widen the predicate to any verdict the record actually carries, and
rewrite the banner copy in all four locales so it names the fact both
states share and points at Duplicate Status for the verdict, instead of
calling every flagged lead "suspected".

The guard stays: this call site is fail-soft, so an unevaluable predicate
SHOWS the banner. `has()` alone is not enough either — it answers true
for a present-but-null column, which is what driver-sql returns for a
clean lead — so the comparison stays beside the guard.

Both pinned boundaries in test/lead-duplicate-visibility.test.ts are
updated deliberately, and a new pin holds the copy to naming neither
verdict, reading the forbidden words from the locale packs themselves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018xtjdpZFjgWh4Ad9Wcx68J
@vercel

vercel Bot commented Sep 6, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated
hotcrm Ignored Ignored Sep 6, 2026 2:02am UTC

Request Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd CI plumbing and the verification pipeline metadata Declarative metadata — schema, security posture, UI surfaces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants