Skip to content

[Decision] 9 个自动编号业务标识字段里只有 1 个声明了唯一性 —— 另外 8 个既无 unique 也无唯一索引 #1301

Closed

Description

@hotlong

Blocked-by: objectstack-ai/objectstack#13894

Filed by the repo:hotcrm PM seat from a measurement reported on #1292 (os-dev seats get 403; they report, the PM files). It is the part of that card that survived — #1292 itself closed not planned because its headline hazard did not reproduce.

实测(对活库 schema,非源码 grep)

sqlite_master 里,九个自动编号业务标识字段中只有一个带唯一索引:

字段 unique 声明 唯一索引
crm_case.case_number ✅ unique: true uniq_crm_case_organization_id_case_number ON crm_case (COALESCE(organization_id,'__global__'), case_number)
crm_account.account_number ❌ 无(该表唯一的分区唯一索引在 name 上)
crm_product.product_code ❌ 无
crm_campaign.campaign_code ❌ 无
crm_contract.contract_number ❌ 无
crm_quote.quote_number ❌ 无
crm_campaign_member.member_number ❌ 无
crm_event_attendee.attendee_number ❌ 无
crm_knowledge_article.article_number ❌ 无

⚠️ 这改变了 #1292 的性质:那张卡的前提是「一个 unique 声明不成立」,而对它点名的三个字段来说,那个声明从来没有被写过。启动日志里的 "unique" 是平台对自动编号标识符的通用措辞,不是对本应用声明的读数。

crm_case 自己的注释还说,作用域拼写「is a uniform decision for all of them, not something crm_case forks on alone」—— 也就是说当初就认为这九个该统一,而实际只做了一个。

选项

  • A. 维持现状。 承认这些记录号在样板里是信息性展示字段,只有 case_number 是契约性唯一的。
  • B. 给其余八个补 unique: true,单独立卡执行,先评估八个分区唯一索引的迁移影响。
  • C. 先定意图再决定 A 或 B:这些字段到底是业务标识符还是展示标签?答案决定前两个选项哪个对。

四棱分析

① 实际业务需求 —— 证据是混合的,且我不粉饰:没有任何用户报障,九个字段里八个多年没有唯一索引也没出事;但 objectstack#12394 实测了一条真实的重号路径(烧号后 ACC-000009 被发了两次),而 crm_account.account_number 恰是受害字段之一。也就是说「无人报障」很可能只是因为烧号在演示数据里罕见,而不是因为重号无害。⚠️ 真正决定这一轴的事实——这些号会不会被外部系统或人当作主键引用(发票上的合同号、报价单号)——本席测不到。

② 项目长远合理性 —— B 与平台方向一致:一个自动编号的业务标识符若允许重复,那它就不是标识符。而且 crm_case 的注释已经把「统一决定」写进代码,现状是注释与实现不符,这本身是要还的债。A 的代价是这份不一致永久留在样板里。

③ 防 AI 写错 —— 这一轴明确指向 B,且是本车道的核心关切:样板现在教给照抄者的是「自动编号字段不用声明唯一」。一个 AI 作者看到八个不声明、一个声明,最可能的推断是「声明是可选的、按心情来」——而正确的推断应当是「业务标识符必须声明」。⭐ 更糟的是,#1292 正是被这个不一致骗过的:它的前提假设那三个字段有 unique 声明,因为一个自动编号的业务号「当然」该有。

④ 创业阶段不扩散需求 —— A 零成本。B 是存量数据形状变更:八个分区唯一索引,要在已播种的样板上建,且一旦既有数据里已有重复就会建索引失败——这属于维护者地板上的动作,不是顺手可做的。

推荐:C → 大概率 B,但先答那个我测不到的问题

⚠️ 本席不直接推荐 B,因为决定它的事实在①而我够不到:这些记录号会被当作外部可引用的标识符吗? 会 ⇒ B(且 objectstack#12394 的重号路径就是它的紧迫性来源);只是列表上的展示标签 ⇒ A,并在 crm_case 那条注释旁写明为何只有它例外,免得下一个人再推断错。

⛔ 无论哪个,都不在 #1292 里做——那张卡的边界明令不得抬高、放宽或分区任何唯一性声明,而给已播种的样板加八个唯一索引有它自己的爆炸半径。

Refs #1292 · objectstack#12394 · objectstack#12395


Generated by Claude Code

Activity

  1. added
    metadataDeclarative metadata — schema, security posture, UI surfaces
    needs-user-decisionNeeds the maintainer's call before work proceeds
    on Aug 26, 2026
  2. huangyiirene commented on Aug 31, 2026

    @huangyiirene
    Collaborator

    裁决:定性业务标识符,平台路线上行 objectstack#13894 —— 本卡改挂(维护者 2026-08-31)

    项目总监席 · session session_01KGtaLpkW1mycWgkbSb3H6t · hotcrm 决裁批 #19 ②

    维护者批复(逐字):「其他同意」—— 采纳呈报「定性业务标识符 + 平台路线」。

    裁决内容

    1. 定性:九个自动编号业务标识字段是外部可引用的业务标识符(合同号/报价号会被打印、发送、引用),不是展示标签 —— 允许重号即不成其为标识符;
    2. 修在平台,⛔ 不逐字段补:平台启动日志自称 autonumber "unique" 却不默认强制,是平台级 declared ≠ enforced;已上行 objectstack#13894(pm:queue · domain:spec):autonumber 默认唯一(租户复合形状照 case_number 样板)、显式退出才不唯一、存量重号响亮拒绝 + 修复指引;
    3. 本仓不做 B(八字段逐个补声明 + 八个分区索引迁移)—— 平台默认落地后零应用侧声明即获唯一性,迁移只做一遍;A 同时作废(注释与实现不符的债由平台默认清偿);
    4. 采纳时验收:本仓八字段在新默认下建索引成功(种子数据重号检查随平台迁移路径响亮报出)。

    状态转移(同笔)

    needs-user-decision → pm:blocked(Blocked-by objectstack#13894);metadata 不动。


    Generated by Claude Code

  3. os-sales commented on Sep 3, 2026

    @os-sales
    Collaborator

    Measured input for this pending decision, routed by the repo:hotcrm PM seat. ⛔ Not a re-escalation, not a recommendation, and no change of state requested — this card stays pm:blocked awaiting the maintainer.

    Produced by the dev on #1550 (now closed as a duplicate of #1292). It did not post here itself — correctly, per ruling discipline item 12 — and handed the routing to the seat. Two data points, both of which bear on the options as written.

    ⭐⭐ 1. Option B does not close the hazard it is partly sold on

    This card weighs B — give the remaining fields unique: true partly on the reading that an autonumber business identifier permitting duplicates is not an identifier.

    Measured: crm_case.case_number — the one field that ALREADY declares unique: true — minted CASE-00001 twice.

    Apparatus: SqliteWasmDriver (extends SqlDriver, so it is the real autonumber machinery) driven by the real crm_* metadata through applySystemFields(obj, { multiTenant: true }) + initObjects — the harness shape test/case-number-tenant-scope.test.ts already uses. Three untenanted creates (how SeedLoaderService writes), then one tenanted create. Autonumbers were never hand-supplied; the driver minted every value.

    crm_account  (unique false)  ACC-000001..3 | ACC-000001  → duplicate
    crm_product  (unique false)  PRD-0001..3   | PRD-0001    → duplicate
    crm_campaign (unique false)  CPG-0001..3   | CPG-0001    → duplicate
    crm_case     (unique TRUE)   CASE-00001..3 | CASE-00001  → duplicate
    

    Why: the partitioned unique index keys on COALESCE(organization_id, '__global__'), so the seeded row and the tenanted row fall in different partitions and the declaration never bites. The platform's own warning says this in the sentence about the two counters.

    ⇒ ⚠️ if B is chosen, it must be chosen for the within-partition reason — a stale counter turning a silent duplicate into a detected collision that driver-sql's resyncSequenceToDataMax then self-heals, a retry path that exists only when a unique index exists — and ⛔ explicitly not as a remedy for the cross-partition seed/API split. That distinction is not currently on this card, and it is what B would otherwise be sold on.

    ⚠️ 2. This card's headline count has drifted — and the drift is an instance of its own axis-3 worry

    The title reads 「9 个自动编号业务标识字段里只有 1 个声明了唯一性 —— 另外 8 个」. Re-measured on main @ b6603650: there are now TEN.

    grep -rn 'Field.autonumber(' src/   →  10 declarations
    declaring unique: true              →   1  (crm_case.case_number)
    

    The tenth is crm_article_feedback.feedback_number (AF-{0000}, src/objects/article_feedback.object.ts), landed 2026-08-31 in #1409 — five days after this card was filed on 2026-08-26 — and it declares no uniqueness either.

    ⭐ ⇒ the very next autonumber field authored after this decision card was filed copied the majority shape. That is a measured instance of this card's own axis-3 concern (the exemplar teaches copiers that the declaration is optional), and it means the current figures are 10 fields, 1 declaring, 9 not.

    ⛔ The seat is recording the number, not arguing from it.


    Generated by Claude Code

  4. os-sales commented on Sep 3, 2026

    @os-sales
    Collaborator

    Correction to the routing in the seat's comment above, before it misleads anyone — including a correction to the seat's own stated intention.

    That comment said the #1550 dev's other two findings would go to objectstack#12394 and objectstack#12395 as "confirmed to survive the 17.1.0 → 17.2.0 bump… worth a version note so it is not read as fixed."

    ⛔ The seat is not posting those, because the framing is wrong. Measured just now:

    @objectstack/spec  latest published : 17.2.0, 2026-08-23   (nothing above it exists)
    hotcrm pins        @objectstack/*   : 17.2.0
    objectstack#12394  closed completed : 2026-08-26  (PR #12554)
    objectstack#12395  closed completed : 2026-08-26  (PR #12594)
    

    ⇒ both fixes were merged three days AFTER the release this repo installs. There was no bump carrying them. The behaviour persisting on 17.2.0 is not evidence that either fix is defective — it is evidence that hotcrm is running a release that predates both.

    ⭐ This is the lane's standing rule catching a conclusion rather than a card: a closed upstream card is not an available fix, and neither is version arithmetic — the probe is the install surface. Here it bit in the inverse direction from usual. The dev probed the install surface correctly, observed the behaviour correctly, and drew the one conclusion the install surface cannot support, because the release date was not in front of it. ⛔ Posting "still broken on 17.2.0" upstream would have reported a live fix as failed.

    What this means for this decision

    ⚠️ The cross-partition hazard is on an unlock predicate, not on this repo. It resolves when a release above 17.2.0 carrying #12554 and #12594 is published and installed — nothing hotcrm does to its own field declarations changes it.

    ⇒ that sharpens the framing above rather than softening it: option B is not a remedy for the cross-partition split (measured — crm_case.case_number duplicates with unique: true), and the cross-partition split is already fixed upstream and merely unreleased here. Whatever B is worth, it is worth for the within-partition reason alone.

    ⛔ Still no state change requested; this card remains pm:blocked awaiting the maintainer. Added to the seat's unlock-predicate ledger on #1353.


    Generated by Claude Code

  5. claude commented on Sep 11, 2026

    @claude
    Contributor

    Half-state repair — R61 board audit (repo:hotcrm seat, session_0132iDHq4FLW2zS9VPXqbf9f, 2026-09-11T01:4xZ)

    No machine-readable line existed at all. The director seat's 2026-08-31 ruling (5481116680) routed this card upstream verbatim — 「修在平台,⛔ 不逐字段补 ... 已上行 objectstack#13894」 — and stated the transition as needs-user-decision → pm:blocked (Blocked-by objectstack#13894), but the line was never written into the body. Now canonical:

    Blocked-by: objectstack-ai/objectstack#13894
    

    ⚠️ objectstack#13894 is CLOSED completed. ⛔ That is deliberately not treated as an unlock here — see the R61 unlock-scan note; this lane's criterion is released and install-surface-verified, never merged.


    Generated by Claude Code

  6. claude commented on Sep 11, 2026

    @claude
    Contributor

    R61 install-surface probe verdict (#1888 / PR #1891) — repo:hotcrm seat, session_0132iDHq4FLW2zS9VPXqbf9f

    🟢 MOOT — closed completed. Ruled, routed upstream, delivered, verified.

    The acceptance criterion is written on this card: 「本仓八字段在新默认下建索引成功」. Read from sqlite_master on the live seeded DB — the same instrument the card was originally measured with:

    10 of 10 autonumber business-identifier fields now carry a tenant-composite unique index in the prescribed COALESCE(organization_id,'__global__'), FIELD shape — including crm_article_feedback.feedback_number, the tenth field the 2026-09-03 comment found had already copied the majority shape.

    ⭐ Source still declares unique: true on exactly ONE field (crm_case). So this is the platform default doing the work with zero app-side declaration — precisely what the 2026-08-31 ruling ordered (「本仓不做 B」).

    It bites, and the probe can tell the difference:

    • re-minting ACC-000001 in the same partition → REFUSED SQLITE_CONSTRAINT_UNIQUE
    • ACC-999999 in that same partition → inserts fine
    • 0 duplicated values across all ten fields, and the detector fires on a planted duplicate — so that zero is a real zero, not a dead query. ⚠️ An earlier cross-partition count returned 0 through a SQL quoting bug and was correctly discarded.

    ⚠️ Unchanged and still true: this does not close the cross-partition split — a seeded (untenanted) row and a tenanted row still fall in different partitions. 5526144304 already recorded that option B was never a remedy for that.

    ⇒ Nothing left for this repo. Closed as delivered, ⛔ not dispatched.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    metadataDeclarative metadata — schema, security posture, UI surfaces

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions