Skip to content

No seeded account carries a billing_address, so both territory sharing rules match zero records on the demo dataset #638

Description

@os-zhuang

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_territory and europe_territory now seed (seeded: 9, skipped: 0, total: 9) with a real, pushdown-able filter:

"criteria_json": "{\"billing_country\":{\"$in\":[\"UK\",\"DE\",\"FR\",\"IT\",\"ES\"]}}"

But the data half is still empty. Grepping the whole repo, billing_address appears only in object definitions, views, translations, the lead-conversion flow and the sharing rules — never in src/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:

{
  name: 'Globex Industries',
  type: 'prospect',
  industry: 'manufacturing',
  annual_revenue: 12000000,
  number_of_employees: 800,
  phone: '+1-312-555-0200',      // <- a US phone number, and nothing else geographic
  website: 'https://globex.example.com',
  tier: 'enterprise',
  segment: 'net_new',
  last_activity_date: cel`daysAgo(8)`,
},

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

rm -rf .objectstack/data
pnpm exec objectstack dev --fresh -p 38617 --seed-admin
# then, authenticated:
curl -s -b cookies.txt 'http://localhost:38617/api/v1/data/crm_account' \
  | python3 -c "import sys,json;[print(r['name'], r.get('billing_country')) for r in json.load(sys.stdin)['records']]"
# every row prints: <name> None

Suggested fix

Give the seeded accounts a billing_address with 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 in US, Stark Medical in CA, Northwind Energy in DE, Lattice Education in UK. Two things to check while doing it:

  1. Do hooks run on seed writes? billing_country is a projection maintained by account_protection on beforeInsert/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 that src/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 need billing_country set explicitly alongside billing_address (and that duplication should be a deliberate, commented decision, not an accident).
  2. Staffing. sys_member on a fresh install holds only the admin, and no position is staffed, so even a matching account materialises no row in sys_record_share. Territory sharing is only end-to-end demoable once some demo user actually holds na_sales_team / eu_sales_team. That may deserve its own issue.

Related: #621 (the rules themselves), #613 (owns src/data/index.ts this round), #617 (whether seeds fire hooks), #549 (sharing coverage on related lists).

Activity

  1. added
    bugSomething isn't working
    metadataDeclarative metadata — schema, security posture, UI surfaces
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    pm:queueReady for the PM dispatch loop
    on Aug 2, 2026
  2. self-assigned this
    on Aug 2, 2026
  3. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    认领:PM 循环第 7 轮
    会话:session_019SS7C5SXpniKeCApxgARyf
    分支:claude/issue-638-seed-billing-addresses
    Worktree:hotcrm-issue-638

    src/data/index.ts 本轮归你(#613 已于上一轮合并,该文件已释放)。同批的 #633 只动 src/flows/,与你互斥。

    先看一眼 #635:这个种子文件距 pnpm hygiene 的 102,400 字节上限只剩约 1.5KB,而你要给 9 个客户各加一个地址。很可能一加就撞线。 撞线时不要削减本次改动、也不要为了省字节写得晦涩——按 #635 的建议按对象族拆分该文件、CrmSeedData 保持为聚合导出,把拆分连同本次改动一起交付,并在 PR 里说明。这是本轮唯一获准做的结构性重构。

    issue 里列的两件"顺带要确认的事",处理方式不同:

    1. 种子写入是否触发钩子(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 的结论一并给出:那个文件开头"种子不跑钩子"的断言与启动日志矛盾,请以实测为准并更正注释。
    2. position 无人在编(sys_member 只有 admin)——自行判断,但有边界。 如果让某个演示用户进入 na_sales_team / eu_sales_team 只是 demo_bootstrap 里的一两行,就一并做掉,因为不做的话本 issue 的价值只兑现一半:规则匹配到了客户,却仍然不给任何人授权,演示上依旧看不见东西。但如果它牵扯到演示用户/成员体系的设计(需要新建用户、决定谁属于哪个团队、影响其他共享规则),就不要在本轮做,另开 issue 说明需要什么,由我排期。

    地址数据请按 issue 的建议跨两个区域和区域之外分布,让规则可演示地划分这批客户,而不是全部落进同一个区域。


    Generated by Claude Code

  4. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    实现完成,草稿 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|US
    

    billing_country 只有一个写者 —— account_protection 的 beforeInsert 投影。种子里没有
    写它(见下),而 9 行全部带值,所以钩子确实在种子写入上跑了。

    平台自身的契约也这么写(SeedLoaderService.SEED_OPTIONS,@objectstack/metadata-protocol,
    17.0.0-rc.1);同时也解释了为什么 src/data/index.ts 原来的断言会出错——它把三个开关当成了一个:

    另外值得记一笔:真正的播种路径是 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

  5. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    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

  6. 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-dispatchpm:queueReady for the PM dispatch looppriority: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