Repository navigation
No affordance for case-variant duplicate accounts created outside lead conversion #655
Description
Activity
- addedenhancementNew feature or requestNew feature or requestmetadataDeclarative metadata — schema, security posture, UI surfacesDeclarative metadata — schema, security posture, UI surfacespm:queueReady for the PM dispatch loopReady for the PM dispatch loop
on Aug 2, 2026 - addedneeds-user-decisionNeeds the maintainer's call before work proceedsNeeds the maintainer's call before work proceedsand removedpm:queueReady for the PM dispatch loopReady for the PM dispatch loop
on Aug 5, 2026 分诊(PM 循环,17.0 发布修复线,会话
session_01VHrPAGEgFDoHjphqYG4BMa):摘pm:queue,改挂needs-user-decision。理由:本单是能力扩张类提案(为 lead conversion 之外创建的重复客户提供治理面),不属于 17.0 发布验收的 bug 收敛范围。按维护者本轮指示,enhancement 提案(#594–#601、#639、#655 等)统一进决策收件箱,由维护者按创业阶段聚焦原则拍板是否立项;拍板前不派发。这不是对提案价值的否定,是排期归属的判断。
Generated by Claude Code
- added and removedneeds-user-decisionNeeds the maintainer's call before work proceedsNeeds the maintainer's call before work proceeds
on Aug 11, 2026 huangyiirene commented
on Aug 11, 2026 CollaboratorMore actionsRuling (maintainer, 2026-08-11, PM chat, verbatim: 「接受你的全部建议」): reclassified
finding— held, not queued. Dedupe affordances for case-variant duplicate accounts have no real pull in a reference app today; the unique-index route was measured to fail on stock that already holds both spellings; the merge action is the only shape that treats existing stock and it is L-size. The record stays inside the state machine as a finding. Revisit trigger: the next real duplicate-account incident (or the "fresh installs only" premise being formally dropped) — at that point the soft-flag hook (lead-mirror precedent) is the entry-level shape to price first.
Generated by Claude Code
huangyiirene commented
on Aug 31, 2026 CollaboratorMore actionsLedger exit — executing the recorded 2026-08-11 maintainer ruling (verbatim: 「接受你的全部建议」— "reclassified
finding— held, not queued"), whose label transition was never performed:needs-user-decisionremoved,findingapplied. Class-1 execution under the recorded-ruling channel (director seat, summon #7, sessionsession_01KGtaLpkW1mycWgkbSb3H6t): the ruling mechanically determines the state, the flip is one revert away, no manual-floor item moves. The revisit trigger stays as recorded on the ruling comment: the next real duplicate-account incident, or the "fresh installs only" premise being formally dropped.
Generated by Claude Code
- added and removedneeds-user-decisionNeeds the maintainer's call before work proceedsNeeds the maintainer's call before work proceeds
on Aug 31, 2026 Grading →
pm:on-hold,findingretired.⚠️ The disposition is UNCHANGED; only the label encoding is corrected. R18,repo:hotcrmseat.What I am not doing
⛔ I am not re-opening or narrowing the maintainer's ruling of 2026-08-11 (verbatim: 「接受你的全部建议」), which reads:
reclassified
finding— held, not queued. … The record stays inside the state machine as a finding. Revisit trigger: the next real duplicate-account incident (or the "fresh installs only" premise being formally dropped) — at that point the soft-flag hook (lead-mirror precedent) is the entry-level shape to price first.The answer stays "not now", the revisit trigger stays exactly as recorded, and the entry-level shape stays the soft-flag hook. Nothing about the decision moves.
What is wrong, and why it is worth a write
findingin this state machine means awaiting first-touch grading — the count of barefindinglabels is the ungraded-backlog metric. This card has been graded, twice, and ruled on by the maintainer. Encoding "decided: not now" as "nobody has looked at this" has a measurable cost, and I paid it this round: #655 came up in R18's sweep as an ungraded finding and I read the whole thread to discover it had been decided three weeks ago. Every future seat pays the same toll, and the health metric overstates the ungraded backlog by one every round.The state machine already has the word for "decision made, answer is not now":
pm:on-hold.⇒ ⭐ This is not a disagreement with the ruling — it is the ruling, written in the label that means what the ruling said. "Stays inside the state machine" is better served by
pm:on-hold, which is a state with a defined exit, than by a label that means the opposite of what happened here.The exit, made machine-readable
A
pm:on-holdcard is only legal with an exit something can actually fire. The recorded trigger was prose, so here it is in the greppable form:Restart-when: the "fresh installs only" deployment premise is formally dropped, OR a real duplicate-account incident is reported against a shipped org. Restart-touch: src/objects/account.object.ts docs/MAINTENANCE.md⭐
Restart-touchis what turns this from an un-exitable hold into a real one. Both named files are where the "fresh installs only" premise physically lives —account.object.tscarries the reasoning for declining the unique index, andMAINTENANCE.md§3.3 carries the deployment-shape statement. Both were written as conditional, not universal, and the card says so. ⇒ Any future dispatch whose file surface touches either one intersects this hold and re-reads this card, which is exactly when the premise would be changing.⚠️ Note for whoever fires it: thecreate_indexhazard is measured and still binding — a unique index onname_normalizedfails on any deployment already holding both spellings. That is precisely why the premise is load-bearing, and why dropping it changes which of the card's three shapes is even available.⛔ Not closed, and that is deliberate
The standing guidance is that a card nobody will develop should be closed rather than held, because a hold with no firing mechanism is a state nobody can leave. That guidance does not apply here: this hold now has a firing mechanism, and the maintainer explicitly asked for the record to stay in the state machine. ⇒ Held, with an exit. If the maintainer prefers it closed, one word and it closes — the record survives either way.
Generated by Claude Code
- addedpriority:p2Medium: important, M3Medium: important, M3and removed
on Sep 9, 2026 objectstack-fleet commented
on Sep 24, 2026 ContributorMore actionsrepo:hotcrmseat,session_01X8U3asekbiC7yWoEPWR4Dg· stock re-triage group 1 (maintainer-confirmed ten-card group; maintainer reply verbatim: 「批1 同意」) · 2026-09-24T23:11ZClosed not_planned — the 2026-08-11 disposition ("not now") is unchanged; only the form changes
The hold's exit ("fresh-installs-only premise formally dropped, OR a real duplicate-account incident") has no machine that can fire it, and the rules say a card nothing can wake is not held but closed not_planned. Reopening is free. The soft-flag hook (the lead-mirror precedent) stays the entry-level shape to price first if it is reopened.
Generated by Claude Code
Restart-when: the "fresh installs only" deployment premise is formally dropped, OR a real duplicate-account incident is reported against a shipped org.
Restart-touch: src/objects/account.object.ts docs/MAINTENANCE.md
Found while implementing #626 (PR #654). Filed unassigned, out of scope there.
What #626 fixed, and what it left
#626 makes lead conversion match accounts on a normalized company name, so the conversion path no longer creates near-duplicates (
Acme Corp/ACME Corp). It deliberately did not add a unique index oncrm_account.name_normalized— the reasoning is insrc/objects/account.object.ts, but the short version is that account-name uniqueness already lives per-tenant onname(#625), and a unique normalized column would subsume that constraint and hard-fail writes that succeed today.So every non-conversion path can still create two accounts whose names differ only in case or spacing: the Console form, the record API, a CSV import, a future seed row. Nothing detects it and nothing merges it.
Two concrete consequences
crm_lead, which already has the soft shape for the same class of problem:lead_duplicate_checkflags a re-captured email asduplicate_status: 'suspected'and thesuspected_duplicatesview is the review queue.crm_accounthas neither half.lead_conversion'sfind_accountnode reuses an arbitrary one — the built-inget_recordexecutor acceptsfilter/fields/limitand has nosortoption (measured on 17.0.0-rc.1), so there is no "oldest wins" to express. Reusing one of N is still better than creating the N+1th, which is what happened before Normalize account matching in lead conversion (split out of #598 scope 2) #626, but it is not a defined outcome.Possible shapes (not a recommendation)
beforeInserthook oncrm_accountthat flags a colliding normalized name as suspected, plus a review view. Soft, consistent with Duplicate management: soft lead dedupe instead of the hard unique email constraint #598's doctrine, no write ever refused.name_normalizedunique per tenant and retire theuniqueonname. Strictly cleaner constraint set (normalized-unique implies name-unique), but it re-decides crm_account.name uniqueness is platform-wide, not per-tenant — two orgs cannot both have an "Acme Corp" #625, hard-fails imports, and reports the violation against a hidden column. Note the measured upgrade hazard:create_indexfails on any deployment already holding both spellings, so this route needs a merge pass first on anything but a fresh install.Worth knowing before picking
The reason #626 could defer this at all is the maintainer's statement that this repo's deployment shape is fresh installs only. That premise is what bounds the
create_indexhazard and what makes "flow-level dedupe is enough" defensible today. If it ever stops holding, re-read this issue together with the index decision inaccount.object.tsanddocs/MAINTENANCE.md§3.3 — both were written as conditional, not universal.