Skip to content

A lead flagged duplicate_status: suspected looks clean on its record page, and Convert Lead never mentions it — the duplicate converts into a second account, contact and opportunity #1207

Closed

Description

@os-zhuang

A lead flagged duplicate_status: suspected looks exactly like a clean lead on its own record page, and Convert Lead never mentions it — the duplicate is converted into a second account, contact and opportunity

What happens

  1. crm_lead already holds Theo Park / Skyline Media / theo.park@skylinemedia.example.com (seed data).
  2. Create a new lead through the UI with the same email and company (Wei Zhang / Skyline Media / theo.park@skylinemedia.example.com). It is accepted with no warning.
  3. Open the new lead. The record page shows status, score, owner, email — and nothing else. Nothing indicates it is a suspected duplicate.
  4. Press Convert Lead → confirm → tick Create Opportunity → Submit. It converts.

The stored record after step 2 is not ambiguous — lead_duplicate_check did its job:

"duplicate_of_type": "crm_lead",
"duplicate_of_lead": "dfSlnObNu0oXwRk-",
"duplicate_status": "suspected"

and after step 4:

"converted_account":     "2y7-PvxbS1o08j7A",   // new account ACC-000010 "Skyline Media"
"converted_contact":     "5B0nItHGRr768EfD",
"converted_opportunity": "VLEnmZCSf7BkT1xA"

So the app detected the duplicate, wrote the flag, and then converted it anyway — creating a second account for a company it already had a lead for, plus a contact and a pipeline opportunity behind it. Two reps now work the same buyer, and the pipeline is double-counted.

Where the flag does and does not surface

src/views/lead.view.ts:446 ships a suspected_duplicates grid, and the comment above it is explicit about the intent: "Without a place to SEE that set, the flag would be a column nobody reads." That queue exists and drains — but it is one list view behind the view switcher. Nothing carries the flag to:

  • the lead record page — no banner, no field, no badge (src/pages/lead_detail.page.ts never mentions duplicate_status);
  • the conversion itself — Convert Lead asks one question ("create an opportunity?") and never looks at duplicate_status.

A reviewer working the queue is the slow path. The rep who is about to convert is the moment the flag is worth something, and it is silent there.

Suggested direction

Two independent pieces, either one useful alone:

  1. Show it on the record. A suspected-conditional banner (or at minimum the duplicate_of_lead link in the highlights) on lead_detail.page.ts, so a rep opening the lead sees "this repeats an existing lead" with a link to compare.
  2. Say it at conversion. The convert screen already collects input; a suspected duplicate should surface there — at minimum a warning line naming the record it repeats, so converting is a decision rather than an accident. Whether it should block is a product call worth deciding explicitly.

Found by driving the app as a seller in a browser against a fresh pnpm dev on 17.1.0.

Activity

  1. added
    bugSomething isn't working
    metadataDeclarative metadata — schema, security posture, UI surfaces
    on Aug 20, 2026
  2. huangyiirene commented on Aug 25, 2026

    @huangyiirene
    Collaborator

    Triage (代扫, vacancy clause) → pm:queue. ⚠️ Serialised behind #1185 this round — deferred, not shelved, with the trap recorded here rather than in a PM's memory.

    Filed 2026-08-20 with bug + metadata + prio:p1 and no pm-state at all. So the highest-priority open Bug in this repo has been invisible to every candidate query for five days. That is the measured cost of hotcrm having no triage producer (objectstack#6015 covers four repos, not this one) — recorded again because it keeps being paid. Acting under the vacancy clause: triage actions only, type left alone.

    Premise re-verified against origin/main @ f685136

    $ git grep -c "duplicate" origin/main -- src/pages/lead_detail.page.ts   →  0
    $ git grep -ln "duplicate_status" origin/main -- src/                    →  8 files
         src/objects/lead.hook.ts · lead.object.ts · opportunity.object.ts
         src/views/lead.view.ts · src/translations/{en,zh-CN,ja-JP,es-ES}.ts
    

    Stating the reverse lookup so the zero counts: the same term returns hits in eight sibling files, so the pattern reaches this tree — the record page really does not mention the flag, and the view-level queue really is its only consumer. The card's central claim holds in full.

    Why it is not dispatched in this round's batch

    Its file surface intersects #1185 (dispatched this round) on two independent surfaces, not one:

    surface #1185 this card
    src/flows/lead-conversion.flow.ts carries one of the 16 notify nodes being converted to email templates the conversion path that must surface the flag
    src/translations/{en,zh-CN,ja-JP,es-ES}.ts adds template/locale rows for every converted notification needs label rows for the banner / warning copy

    Two agents editing all four locale bundles and the same flow file in the same window is precisely what the serialisation rule exists to prevent. Same-file cards serialise hard across rounds. This card goes out in the next batch, as the top item — under the lane's selection total order it outranks everything else queued (prio:p1 + type Bug beats the prio:p2 Bugs and the unprioritised cards), and the only thing keeping it back is the file collision.

    Scope note for whoever takes it — one half is a product call and must not be assumed

    The card proposes two independent pieces. They are not equal:

    1. Show the flag on the record page — the app already detects the duplicate and writes duplicate_status / duplicate_of_lead; the record page simply never reads them. Surfacing a value the app already computes is invariant restoration, needs no ruling, and is the core of this card.
    2. Say it at conversion — a warning naming the record it repeats is the same invariant restoration and is in scope. ⛔ Whether conversion should be BLOCKED is not. The card says so itself ("Whether it should block is a product call worth deciding explicitly"), and it is: blocking changes what the app refuses, which is product semantics, not a defect repair. If the implementing round concludes a block is warranted, that is a needs-user-decision card, ⛔ never a decision taken inside the PR.

    Re-check before implementing (the dispatch will carry these; they are recorded here so the card is self-contained): git grep -n "duplicate_status" origin/main -- src/views/lead.view.ts for the existing queue view, and read src/actions/lead.actions.ts alongside src/flows/lead-conversion.flow.ts — the convert gesture spans both and which one owns the screen is a reading, not an assumption.


    Generated by Claude Code

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 25, 2026
  4. self-assigned this
    on Aug 25, 2026
  5. huangyiirene commented on Aug 25, 2026

    @huangyiirene
    Collaborator

    Claim: PM loop round 2
    Session: session_01STpYzSP2LiZwZinh6m6Qgf
    Branch: claude/issue-1207-duplicate-lead-visibility
    Worktree: hotcrm-issue-1207
    Domain: (hotcrm has no domain:* taxonomy — repo-wide seat, objectstack#10282)
    File surface: src/pages/lead_detail.page.ts, src/actions/lead.actions.ts, src/flows/lead-conversion.flow.ts, src/translations/{en,zh-CN,ja-JP,es-ES}.ts, plus one test file for the pins — name it in the report (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus
    Clause-②: no — app metadata and locale rows. No contract accept/reject behaviour changes, no public surface widens; this repo ships no packages.
    Serial constraints cleared: 4 open PRs, all dependabot (#658, #1058, #1178, #1264), none touching src/. Batch siblings: #1169 takes .github/workflows/docs-app.yml — disjoint; #1196 takes test/docs-drift.test.ts and its split siblings — disjoint, and its reference sweep is fenced out of this card's files by name (see its claim). ⚠️ #1214 item 1 concerns the very confirmText on src/actions/lead.actions.ts:26 that this card also touches — #1214 is pm:queue, not dispatched, and is serialised behind this card. ⛔ Do not implement #1214's item 1 here (removing the confirm is a separate ruling); just do not be surprised when you read that line.

    Read the full issue body and every comment on GitHub yourself before starting, and verify the body is not truncated.


    Ruling (⛔ not re-adjudicable) — two pieces are in scope, a third is not

    The app already detects the duplicate and writes the flag. The defect is that nothing surfaces it. So:

    1. ✅ Show it on the record. A suspected-conditional banner, or at minimum the duplicate_of_lead link in the highlights, on src/pages/lead_detail.page.ts. Invariant restoration — surfacing a value the app already computes.
    2. ✅ Say it at conversion. A warning naming the record it repeats, so converting is a decision rather than an accident.
    3. ⛔ Whether conversion should be BLOCKED is OUT OF SCOPE. The card says so itself ("Whether it should block is a product call worth deciding explicitly") and it is right: blocking changes what the app refuses, which is product semantics, not a defect repair. If your work convinces you a block is warranted, report it — it becomes a needs-user-decision card, ⛔ never a decision taken inside this PR.

    PM mechanism assumptions (measured this round — falsify them and say so)

    Measured on origin/main @ 6ed7b8d:

    1. git grep -c duplicate src/pages/lead_detail.page.ts → 0. Control: duplicate_status returns hits in 8 sibling files (lead.hook.ts, lead.object.ts, opportunity.object.ts, lead.view.ts, and all four locale bundles), so the pattern reaches this tree. The record page really does not mention the flag.
    2. The convert gesture spans two files, and you need both: convert_lead is an action (src/actions/lead.actions.ts:14) carrying confirmText (:26) and visible (:25), and it launches the lead-conversion screen flow (src/flows/lead-conversion.flow.ts, screen_1 at :55). ⚠️ Which of the two should carry the warning is a reading you must take, not something I have decided — say which and why.
    3. src/views/lead.view.ts already ships a suspected_duplicates grid, with a comment stating the intent: "Without a place to SEE that set, the flag would be a column nobody reads." That queue exists and drains. ⇒ This card is about the fast path (the rep about to convert), not about replacing that queue. ⛔ Do not touch the queue view.

    PM's suggested route (optional — measurement beats it)

    Non-negotiable

    • Gates (read off package.json on origin/main at dispatch time): pnpm verify = validate → typecheck → lint → lint:i18n-gate → hygiene → hygiene:tokens → build → test. Exit codes captured after redirect, never through a pipe.
    • ⚠️ hygiene:tokens is a shrink-only ratchet and this card adds authored content. ⛔ Never raise a ceiling — stop and report with numbers. Note src/translations/ is EXCLUDED from the measured surface by maintainer ruling of 2026-08-17 (quoted in scripts/check-source-token-ratchet.mjs), so locale rows are free; the page/action/flow changes are not.
    • Live changeset gate (newly-added .changeset/*.md vs base sha, or skip-changeset label, or empty frontmatter). private: true does not exempt it.
    • New user-visible copy needs all four locales. lint:i18n-gate will tell you, but author them deliberately rather than to satisfy the gate.
    • Pin both pieces. A test that asserts the banner/warning is present and names the duplicate — and reverse-verify from a committed state (⚠️ git checkout HEAD -- <path>, never the bare form which reads the index; verify the tree afterwards).
    • PR draft, base main, first body line Fixes #1207 if you deliver both pieces; Part of #1207 if you deliberately deliver one — and say which in the report. ⚠️ A squash pulls commit subjects into the merge commit, so keep Fixes out of commit subjects on a Part of PR.

    Report

    Terminal JSON report as a comment on this issue first (first line the literal os-dev-report), then as the return message. Machine-checkable fields only. Leave your own claim comment with your own session ID and branch — this one identifies the PM session, not you.

    ⚠️ If you find an out-of-scope issue, report it to the PM rather than filing it: os-dev seats get HTTP 403 from api.github.com (measured this round). Reporting is required; filing is mine.

    premise_still_valid: false with pr: null is a legitimate delivery.


    Generated by Claude Code

  6. huangyiirene commented on Aug 25, 2026

    @huangyiirene
    Collaborator

    Claim: os-dev seat for #1207 (dispatched by the PM claim above).
    Session: session_01STpYzSP2LiZwZinh6m6Qgf — ⚠️ the same id as the PM comment above: this seat runs as a subagent inside the PM session, so the session id cannot distinguish us. What identifies this claim is the branch + worktree below; the PM claim identifies the dispatching loop.
    Branch: claude/issue-1207-duplicate-lead-visibility (pushed empty at start, base 6ed7b8dd)
    Worktree: hotcrm-issue-1207
    Files touched (the dispatch's declared surface, minus one): src/pages/lead_detail.page.ts, src/flows/lead-conversion.flow.ts, src/translations/{zh-CN,ja-JP,es-ES}.ts, .changeset/duplicate-flag-reaches-the-rep.md, and the pin file test/lead-duplicate-visibility.test.ts.

    src/actions/lead.actions.ts is NOT touched — the dispatch listed it, and the reading it asked for came out the other way: the warning belongs on the conversion screen, not on convert_lead's confirmText. Reasons in the PR body. That also leaves #1214 item 1's line untouched, so the two cards no longer collide on it.

    Assignee left alone. Report to follow on this issue as os-dev-report.

    Generated by Claude Code


    Generated by Claude Code

  7. huangyiirene commented on Aug 25, 2026

    @huangyiirene
    Collaborator

    os-dev-report
    {
    "issue": 1207,
    "status": "done",
    "branch": "claude/issue-1207-duplicate-lead-visibility",
    "pr": "#1286",
    "premise_still_valid": true,
    "summary": "Both in-scope pieces shipped. On the record page (src/pages/lead_detail.page.ts): a record:alert banner gated on has(record.duplicate_status) && record.duplicate_status == \"suspected\" via P (an explicit CEL envelope), plus a self-hiding duplicates details section carrying duplicate_status, duplicate_of_type and BOTH survivor links. At conversion (src/flows/lead-conversion.flow.ts): get_lead moved ahead of screen_1, a decision_duplicate node and two assignments set a duplicateWarning variable, and screen_1's description reads it — so the warning appears only on a flagged lead and the clean branch renders no description at all. Two readings the dispatch asked me to take came out against its suggested route, both measured and both explained in the PR: the warning belongs on the flow SCREEN, not on convert_lead's confirmText (static, unconditional, cry-wolf on every clean lead, and #1214 is queued to delete that line), so src/actions/lead.actions.ts is untouched and the two cards no longer collide; and the survivor links do NOT go in the highlights strip, because record:highlights caps fields at 7 and the strip already carries 6 (objectstack validate: fields: Too big: expected array to have at most 7 items). Conversion is NOT blocked — that product call is left open per the ruling.",
    "tests": "Full pnpm verify union run at final head cb9a37a (exit codes captured after redirect, never through a pipe): validate exit 0 (✓ Validation passed (1084ms), 50 warnings, all pre-existing — the only lead_detail_page ones are the known #1279 actions[] rows); typecheck exit 0; lint exit 0 (45 warning(s), 10 suggestion(s)); lint:i18n-gate exit 0 (✓ i18n lint gate: 0 i18n/missing-* issues (55 total lint issue(s)) — byte-identical count to the base run before my change); hygiene exit 0 (✓ source hygiene clean, incl. its own no record id interpolated into user-visible prose check); hygiene:tokens exit 0, NO ceiling touched — business 82,220 to 82,579 (ceiling 85,000), interaction 38,411 to 38,872 (ceiling 42,000), authored total 134,558 to 135,377 (ceiling 140,000); build exit 0 (✓ Build complete (1304ms)); pnpm test exit 0 — Test Files 125 passed (125) / Tests 2913 passed | 1 skipped (2914). New pins: test/lead-duplicate-visibility.test.ts, 11 tests. ABLATION (no dist is involved — vitest imports ../src and ../objectstack.config as TypeScript source, so there is nothing to rebuild; the mutation was confirmed ON DISK both times by grep counts plus git diff --stat, and each restore was proven byte-identical with git hash-object against the HEAD blob, both hashes non-empty and equal). Ablation A, whole feature reverted to the base blobs (record:alert count 1 to 0, duplicateWarning count 3 to 0): 8 of 11 red; the 3 that stayed green are the absence/anti-regression pins, which is the correct direction. Ablation B, the has() guards alone removed (guard count 2 to 0, comparisons still present): 3 red with the engines' own words — No such key: duplicate_status from the page predicate (the record-alert call site is fail-soft, so that fault SHOWS the banner on every clean lead) and condition failed to evaluate as CEL: No such key: duplicate_status from the flow (which fails the whole run, i.e. an ordinary lead becomes unconvertible). Both trees restored, git diff HEAD empty. One real defect in my own first draft was caught by an EXISTING repo gate, not by me: test/flow-variable-conditions.test.ts rejected vars.leadRecord != null because vars.leadRecord is a KEY, so that read itself aborts with Unknown variable when nothing was bound; the edges now read has(vars.leadRecord) && has(vars.leadRecord.duplicate_status) && ... with the other edge as its De Morgan complement.",
    "open_questions": [
    {
    "question": "Should converting a lead flagged duplicate_status: suspected be BLOCKED (or gated behind an explicit override), rather than only warned about? Explicitly out of scope for this PR and NOT decided here — reporting it as the ruling requires.",
    "options": [
    "A. Warn only (what shipped): the screen names the repeated record, the rep decides. Keeps conversion possible when the flag is a false positive, which intake email matching will produce (shared mailboxes, info@, a genuinely new lead at the same company).",
    "B. Block while suspected: the convert action's visible/disabled predicate or a flow refusal stops the conversion until a human clears or confirms the flag. Strongest guarantee, but it makes a machine's guess authoritative and gives the rep no path except the review queue.",
    "C. Block only on confirmed (a human verdict), warn on suspected. The block then rests on a decision a person actually took, and crm_lead's existing validation already requires a survivor to be named for a duplicate disqualification."
    ],
    "recommendation": "C, if anything at all. Real business need: the reported harm is a duplicate that gets converted, and B stops it — but B also stops the false positives, and email-equality matching WILL fire on shared mailboxes, so B trades a rare double-count for a routine dead end with no self-service way out. Long-term soundness: C keeps the machine's suspicion advisory and the human's verdict enforceable, which is the same split lead_duplicate_check already encodes in one column (it stands down on any record already carrying a verdict). Hard to get wrong for AI-authored metadata: C is declarative on a value a person wrote, so declared = enforced holds without a consumer-side tolerance; B would create pressure for an override flag, and an override flag is exactly the kind of escape hatch that gets set by default. Startup scope discipline: A is already shipped and closes the reported defect; C is a small follow-up on one predicate; B is a behaviour change to what the app refuses and should not ride a defect card. Maintainer call either way — I have not implemented any of them."
    },
    {
    "question": "The banner and the conversion warning are suspected-only, per the ruling's wording. A confirmed duplicate that has not yet been disqualified raises the same hazard and gets no banner and no warning line. Widen both predicates to any set duplicate_status, or leave the boundary?",
    "options": [
    "A. Leave as shipped (suspected-only). The duplicates details section already renders for ANY duplicate state, including confirmed and the erased tombstone, so the record page is not silent — only the banner and the conversion line are.",
    "B. Widen both to has(record.duplicate_status) (any state), with copy that reads for both states.",
    "C. Widen only the conversion warning (the irreversible moment), leave the banner suspected-only."
    ],
    "recommendation": "B is a one-word change to two predicates and I would have written it that way; I did NOT, because the ruling said suspected-conditional and a ruling is not mine to widen. The gap is narrow in practice — the usual next step after confirmed is disqualification, which hides the convert action (visible excludes status unqualified) — so this is a small, deliberate hole rather than a live defect. Both boundaries are pinned in test/lead-duplicate-visibility.test.ts so widening is a deliberate edit, not a silent drift."
    }
    ],
    "out_of_scope_findings": [
    "NOT FILED (os-dev seats get HTTP 403 from api.github.com — filing is the PM's) — Flow copy has no locale channel at all. The conversion warning I added is English-only, like every other user-visible string in src/flows/ (notify titles/messages, screen field labels): flows carry no entry in the locale packs, and the i18n coverage extractor never walks them, so lint:i18n-gate cannot see the gap. The app ships four locales; a zh-CN seller reading an English conversion warning is the visible symptom. Worth a card of its own: either flows get a locale surface, or the boundary gets documented where an author will meet it.",
    "NOT FILED (403) — test/detail-section-dedup.test.ts still exempts lead_detail_page, and this PR does not clear that exemption. Its comment names #1209 and #1207 as the owners; my change is disjoint from the pre-existing duplicates (email/phone in the Contact section, status/rating/lead_source/owner_id in Lead Detail, all six also on the highlights strip), and the section I added is disjoint from the strip by construction and asserted so in my own pins. So the exemption is now owned by #1209 alone — worth updating that comment when #1209 lands.",
    "NOT FILED (403) — observation, no action requested: record:highlights capping fields at 7 is invisible until objectstack validate prints Too big. Nothing in the authored file hints at it and the strip silently self-truncates nothing (the parse just warns), so the next author to add a chip meets the same surprise. If the PM wants it recorded as a finding, the measurement is in PR #1286's body."
    ]
    }


    Generated by Claude Code

  8. huangyiirene commented on Aug 25, 2026

    @huangyiirene
    Collaborator

    PM review — ACCEPT. PR #1286. Both readings went against my suggested route and both are right.

    Reviewed against the repo and against GitHub, not against the report.

    Checklist: draft ✓ · base main ✓ · first body line Fixes #1207 ✓ · 7 files, inside the declared surface ✓ · src/actions/lead.actions.ts untouched ✓ · changeset present, Check Changeset green ✓ · mergeable_state: clean ✓ · all 9 checks green, Build and Test (22.x) confirmed by NAME ✓ · path fork: src/, test/, .changeset/ — none governed ⇒ loop-mergeable ✓ · conversion not blocked, per the ruling ✓ · ratchet: no ceiling touched (business 82,220→82,579 / 85,000; interaction 38,411→38,872 / 42,000) ✓

    ⚠️ My suggested route was wrong twice, and the dev measured rather than complied

    My claim said: "The highlights strip is the cheapest honest surface for piece 1" and the card said "at minimum the duplicate_of_lead link in the highlights." Both halves are falsified.

    1. The strip cannot hold them. record:highlights caps fields at 7 and the strip already carries 6 — I verified this myself: ['status','rating','lead_source','owner_id','email','phone']. The duplicate fields do not fit without evicting a chip every lead needs. The dev measured the cap the right way, at runtime: objectstack validate → fields: Too big: expected array to have <=7 items, and test/metadata-references.test.ts parses the same props strictly. A hard cap, not advice — and stronger evidence than reading the .d.ts, which does not surface the refinement.
    2. duplicate_of_lead alone would have covered the rarer half. lead_duplicate_check scans crm_contact before open leads, so a suspected lead's survivor is a contact at least as often as a lead — duplicate_of_type is a discriminator precisely because both are legal. Shipping only the lead link would have left the commoner half of the flagged population with a banner and nothing to click. The card's own suggested direction had this backwards, and the dev says so plainly instead of implementing it.

    ⚠️ I also told the dev to "read the renderer before conforming" — it did, and that is what produced the better answer. The instruction was right; my specific suggestion under it was not. Third defective suggestion I have authored today.

    The confirmText reading — better than what I asked for

    I left "which of the two carries the warning" as a reading for the dev. It chose the screen, and the argument is complete: confirmText is a static unconditional string that would warn identically on every clean lead — the cry-wolf that turns a confirm dialog into furniture — and it cannot name the record. The screen's description is interpolated per run, and a whole-string sole token maps null to undefined, so the clean branch renders no description at all rather than an empty paragraph.

    ⭐ And it dissolves a serialisation I had recorded. Because src/actions/lead.actions.ts is untouched, #1214 item 1 no longer collides with this card — the queue is one constraint lighter than when I wrote it down.

    The totality table is the best thing in this PR

    Two surfaces that fail in opposite directions, both guarded, both guards ablated:

    surface mechanism unevaluable predicate ⇒
    record:alert properties.visible client CEL, fail-soft banner SHOWN on every clean lead
    flow edges e21/e22 server CEL run FAILS — the lead cannot be converted

    Ablation B removed only the has() guards, leaving the comparisons, and got the engines' own words back: No such key: duplicate_status from the page and condition failed to evaluate as CEL: … from the flow. That is exactly the class #1115 is queued about — a predicate that cannot answer refusing the write — caught prospectively here rather than in production.

    Two more measurements worth keeping:

    • P and not a bare string: ExpressionEvaluator.evaluateCondition routes only an explicit { dialect: 'cel' } envelope to @objectstack/formula; a bare string takes the legacy JS path whose FormulaFunctions has no CEL has(). On the fail-soft call site the guard would itself have been the fault.
    • visibleWhen would not have worked: it is evaluated one tier up by SchemaRenderer against the data-source adapter, so it cannot see duplicate_status at all. record:alert's properties.visible is the one record component whose props carry a real row predicate.

    An existing gate caught a real defect in the first draft, and the dev reported it

    test/flow-variable-conditions.test.ts rejected vars.leadRecord != null because vars.leadRecord is a key — that read itself aborts with Unknown variable on the run where nothing was bound, so the guard would have been the fault it was written to prevent. Now has(vars.leadRecord) && has(vars.leadRecord.duplicate_status) && … with the other edge as its De Morgan complement, so the two partition. Reported rather than quietly fixed, which is what makes it useful.

    The prose/ID discipline also held: the warning names the shared email, never the id, per test/record-id-not-in-prose.test.ts — and the id goes where it belongs, in the relationship field, rendered as a link.

    Both open questions are routed, not decided

    1. Should conversion be blocked? ⛔ Out of scope by the ruling and correctly left open. → Filing as a decision card with the dev's option set; its recommendation (C — block on confirmed, warn on suspected) is well-argued and goes in as input, not as an answer.
    2. The suspected-only boundary. ⭐ The dev is right and my ruling was needlessly narrow: a confirmed duplicate not yet disqualified raises the same hazard and gets no banner and no warning line. It says it would have written it wider and did not, because "a ruling is not mine to widen." That is exactly the discipline I want, and the cost is mine, not the dev's. → Filing as a follow-up; the boundary is pinned in test/lead-duplicate-visibility.test.ts, so widening is a deliberate edit rather than silent drift.

    Landing now.


    Generated by Claude Code

  9. added
    priority:p1High: required for production / M2
    and removed on Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingmetadataDeclarative metadata — schema, security posture, UI surfacespm:dispatchedDispatched to a dev agent by /pm-dispatchpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions