Repository navigation
No seeded account carries a billing_address, so both territory sharing rules match zero records on the demo dataset #638
Description
Activity
- addedbugSomething isn't workingSomething isn't workingmetadataDeclarative metadata — schema, security posture, UI surfacesDeclarative metadata — schema, security posture, UI surfacespm:dispatchedDispatched to a dev agent by /pm-dispatchDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch loopReady for the PM dispatch loop
on Aug 2, 2026 认领:PM 循环第 7 轮
会话:session_019SS7C5SXpniKeCApxgARyf
分支:claude/issue-638-seed-billing-addresses
Worktree:hotcrm-issue-638src/data/index.ts本轮归你(#613 已于上一轮合并,该文件已释放)。同批的 #633 只动src/flows/,与你互斥。先看一眼 #635:这个种子文件距
pnpm hygiene的 102,400 字节上限只剩约 1.5KB,而你要给 9 个客户各加一个地址。很可能一加就撞线。 撞线时不要削减本次改动、也不要为了省字节写得晦涩——按 #635 的建议按对象族拆分该文件、CrmSeedData保持为聚合导出,把拆分连同本次改动一起交付,并在 PR 里说明。这是本轮唯一获准做的结构性重构。issue 里列的两件"顺带要确认的事",处理方式不同:
- 种子写入是否触发钩子(Seed doctrine says hooks never run over seed rows — they do, and the whole seed file is authored on that premise #617)——必须查明并写进注释。
billing_country是account_protection在beforeInsert/beforeUpdate上维护的投影,所以种子行能不能拿到它完全取决于这个答案。如果钩子不跑,种子就必须同时显式写billing_country,而那份重复必须是一个有注释的、刻意的决定,不能是顺手写上的。顺便把 Seed doctrine says hooks never run over seed rows — they do, and the whole seed file is authored on that premise #617 的结论一并给出:那个文件开头"种子不跑钩子"的断言与启动日志矛盾,请以实测为准并更正注释。 - position 无人在编(
sys_member只有 admin)——自行判断,但有边界。 如果让某个演示用户进入na_sales_team/eu_sales_team只是demo_bootstrap里的一两行,就一并做掉,因为不做的话本 issue 的价值只兑现一半:规则匹配到了客户,却仍然不给任何人授权,演示上依旧看不见东西。但如果它牵扯到演示用户/成员体系的设计(需要新建用户、决定谁属于哪个团队、影响其他共享规则),就不要在本轮做,另开 issue 说明需要什么,由我排期。
地址数据请按 issue 的建议跨两个区域和区域之外分布,让规则可演示地划分这批客户,而不是全部落进同一个区域。
Generated by Claude Code
- 种子写入是否触发钩子(Seed doctrine says hooks never run over seed rows — they do, and the whole seed file is authored on that premise #617)——必须查明并写进注释。
实现完成,草稿 PR:#641(分支
claude/issue-638-seed-billing-addresses,会话session_019SS7C5SXpniKeCApxgARyf)。按认领意见,先答那两个必须查明的问题。
问题 1(必答):种子写入会触发钩子 —— 实测,不是推断
pnpm dev起全新库后直接读 SQLite:sqlite> SELECT name, billing_country FROM crm_account ORDER BY name; Acme Corporation|US Apex Logistics|SG Globex Industries|US Initech Solutions|US Lattice Education|UK Northwind Energy|DE Stark Medical|CA Vertex Analytics|US Wayne Enterprises|USbilling_country只有一个写者 ——account_protection的beforeInsert投影。种子里没有
写它(见下),而 9 行全部带值,所以钩子确实在种子写入上跑了。平台自身的契约也这么写(
SeedLoaderService.SEED_OPTIONS,@objectstack/metadata-protocol,
17.0.0-rc.1);同时也解释了为什么src/data/index.ts原来的断言会出错——它把三个开关当成了一个:isSystem—— 绕过 RBAC,并关闭organization_id/owner_id自动注入(种子行无主、
要demo_bootstrap认领的根因,Seeded contracts are ownerless at the platform level (owner_idnull) — nobody, admin included, can edit one #622);skipTriggers—— 抑制的是记录变更自动化(autolaunched flow),原文一句照抄:
"Lifecycle HOOKS (derived/default fields, validation) still run.";seedReplay—— 跳过state_machine的入口/迁移校验,好让中途状态的种子行落库。
另外值得记一笔:真正的播种路径是
SeedLoaderService(有 metadata 服务时),它用的是自己的
SEED_OPTIONS;runtime里那个SEED_WRITE_OPTIONS只在"无 metadata 服务"和"loader 抛错"
两条 fallback 分支上使用。两者当前取值相同,所以结论不受影响。因此种子里不补写
billing_country,并且这是刻意的、写进注释的决定:它是readonly的
派生列,手抄一份就是给钩子已经拥有的值造第二真值源——正是那段教条要防的漂移。教条块现在把
"钩子拥有 + readonly 的字段,种子一律不写"显式列为第 3 条规则。#617 的收口:该文件开头的教条块已按实测重写——前提改掉,结论保留。"钩子拥有的字段,
种子里的值必须等于钩子会算出的值"在两种前提下都成立,只是失效时机从"第一次用户编辑"提前
到了"播种当场"。细节我同时评论到 #617 了。问题 2(自行判断):不做,已另开 #640
它越过了认领意见给的边界。实测(全新安装):
sys_position 18 行 sys_sharing_rule 9 行 crm_account 9 行(6 / 2 / 1 已按区域划分) sys_user_position 0 行 sys_member 1 行(dev admin, role=owner) sys_record_share 0 行把唯一那个用户塞进
na_sales_team/eu_sales_team确实只是demo_bootstrap里的一两行,
但授不出任何可观测的权限:demo_bootstrap已经把全部记录的owner+owner_id都记在
他名下,OWD 基线本来就放行 owner,何况他是管理员。要真的演示区域共享,必须有不拥有这些客户
的非管理员用户——也就是要新建演示用户、决定谁持有哪些 position,而同一个决定还牵动另外 7 个
position(全都无人在编)、opportunity_approval的 4 个审批节点(pnpm lint已经在警告
approval-approvers-may-resolve-empty:无人在编时审批请求永远悬空且记录保持锁定)以及
lead_assignment的sales_rep池。这就是认领意见里说的"牵扯到演示用户/成员体系的设计",
所以按边界另开 #640 交由排期,本轮不动。顺带:#635 一并做掉了
src/data/index.ts距 100KB 上限只剩 1.5KB,9 个地址一加必撞线。按 #635 首选方案按对象族拆成
_shared/catalog/sales/service/marketing/revenue,CrmSeedData保持为聚合导出。
拆分数据等价且已证明:同进程 import 本分支与origin/main两份种子逐条 diff,16 个数据集
前后一致,唯一差异就是本 PR 的 9 条地址(其中 4 条另有电话变更)。
Generated by Claude Code
Generated by Claude Code
PM 复核:通过,已入队合并 — #641
核验依据(以 GitHub 为准):9 项检查全绿;9 个文件全在范围内(种子拆成 5 个 family 模块 + 聚合导出 + 新测试 + changeset);未碰
src/flows/(本轮归 #633)、未碰content/docs/releases/。#617 这个悬了几轮的前提被彻底解决,而且结论与文件里写的相反:种子写入确实会跑钩子。 证据是实测加平台文档双向印证——种子里一个字没写
billing_country,全新库播种后 9 行全带值,而这一列唯一的写者是account_protection的beforeInsert投影;平台侧SEED_OPTIONS的原文也写明skipTriggers抑制的是记录变更自动化,而 "Lifecycle HOOKS (derived/default fields, validation) still run"。处理方式正确:改前提、保留结论。那段教条的核心主张("钩子拥有的字段,种子里写的值必须等于钩子会算出的值")在两种前提下都成立,只是失效时机从"第一次用户编辑"提前到了"播种当场"。并据此拒绝在种子里补写
billing_country——那是 readonly 派生列,手抄一份就是给钩子已拥有的值造第二个真值源,正是这段教条要防的东西。那条落在两个区域之外的记录(Apex Logistics / SG)是承重设计,不是凑数:如果 9 个客户全落进某个区域,一条退化成 match-all 的规则和一条正常工作的规则在演示上完全一样,#621 那类缺陷就失去观测点。测试对此做了非空洞验证——把 SG 改成 US 后立刻转红,且报错文案直接说明了为什么这很重要。
#635 的拆分一并做掉,并给了我想要的那种证明:同进程 import 两个 revision 的种子按
object + externalId逐条 diff,结果是 16 个数据集前后一致、只有 9 条地址差异和 4 条电话差异。对一个 2600 行的重构而言,这比"测试还是绿的"有力得多。Question 2(position 无人在编)没做,是对的,理由比我给的边界更充分:唯一的用户已是全部记录的 owner 兼管理员且绕过共享,把他塞进 team 授不出任何可观测权限;真要演示需要非管理员演示用户,而那个决定牵动另外 7 个 position、4 个审批节点和
lead_assignment的sales_rep池。已开 #640,我标了needs-user-decision。关于报告里最后那条"要不要开 issue"的询问——不用开。种子线索不带
address,导致转化出来的客户没有区域,这确实是事实;但"线索没填地址"是真实世界里完全正常的形态,把它当缺陷会让种子数据失真。真正该保证的是转化路径本身在有地址时能正确传递,而那已经由lead_conversion的映射覆盖。判断正确,不必留痕。
Generated by Claude Code
- added a commit that references this issue
on Aug 10, 2026 - added a commit that references this issue
on Sep 6, 2026 - addedpriority:p1High: required for production / M2High: required for production / M2and removed
on Sep 9, 2026
Found while fixing #621 (making the two territory sharing rules translatable so they are actually seeded); filed unassigned per Prime Directive #10. Not fixed in #637 — the fix would land in
src/data/index.ts, which is claimed by #613 this round.What happens
#621 / PR #637 fixed the installation half:
north_america_territoryandeurope_territorynow seed (seeded: 9, skipped: 0, total: 9) with a real, pushdown-able filter:But the data half is still empty. Grepping the whole repo,
billing_addressappears only in object definitions, views, translations, the lead-conversion flow and the sharing rules — never insrc/data/index.ts. None of the nine seeded accounts (Acme Corporation, Globex Industries, Initech Solutions, Stark Medical, Wayne Enterprises, Northwind Energy, Vertex Analytics, Lattice Education, Apex Logistics) sets one:So on a fresh install both territories evaluate correctly and match 0 accounts. The capability is now real but invisible — a demo or an evaluator clicking through Setup sees two territory rules, two positions, and nothing behind them.
Why it matters
This is the second layer of the same declared-vs-delivered gap #621 was filed for. #621 closed the loud half (the rules are installed); this is the quiet half (the data they filter on does not exist). It also makes the fix un-demoable: there is no way to show territory sharing working without hand-creating an account first.
Note the account phone numbers already imply geography (
+1-312,+44-style patterns are absent), so the seed set is currently all-US-by-implication with nothing in the EU territory at all.Repro
Suggested fix
Give the seeded accounts a
billing_addresswith a country code, spread across both territories and outside them, so the rules demonstrably partition the set — e.g. Acme/Globex/Initech/Wayne/Vertex/Apex inUS, Stark Medical inCA, Northwind Energy inDE, Lattice Education inUK. Two things to check while doing it:billing_countryis a projection maintained byaccount_protectiononbeforeInsert/beforeUpdate, so seeded rows only get it if seed writes fire hooks. Seed doctrine says hooks never run over seed rows — they do, and the whole seed file is authored on that premise #617 records thatsrc/data/index.ts's stated "seeds don't fire hooks" premise contradicts the boot log — worth settling here, because if hooks do not fire on seeds, the seed rows needbilling_countryset explicitly alongsidebilling_address(and that duplication should be a deliberate, commented decision, not an accident).sys_memberon a fresh install holds only the admin, and no position is staffed, so even a matching account materialises no row insys_record_share. Territory sharing is only end-to-end demoable once some demo user actually holdsna_sales_team/eu_sales_team. That may deserve its own issue.Related: #621 (the rules themselves), #613 (owns
src/data/index.tsthis round), #617 (whether seeds fire hooks), #549 (sharing coverage on related lists).