fix(data): give the demo accounts billing addresses so the territory rules match records (#638) - #641
Merged
Conversation
…rules match records (#638) #621 fixed the installation half of territory sharing: `north_america_territory` and `europe_territory` now compile and seed against the flat `crm_account.billing_country` column. The data half was still empty. Grepping the repo, `billing_address` appeared in object definitions, views, translations, the lead-conversion flow and the sharing rules — never in the seeds. So both rules evaluated correctly over nine accounts and covered ZERO of them: Setup listed two territories, two positions, and nothing behind either. All nine accounts now carry a billing address, distributed across all THREE outcomes the rules can produce rather than bulk-loaded into one territory: north_america_territory ["US","CA","MX"] 6 (5 × US, Stark Medical CA) europe_territory ["UK","DE","FR","IT","ES"] 2 (Northwind DE, Lattice UK) neither territory 1 (Apex Logistics SG) The account outside both is load-bearing, not filler: with every account inside a territory, a rule that degraded to match-all would look identical to a working one. `country` carries the two-letter code the rules name (`UK`, not the ISO `GB`) because the projection trims and upper-cases but never translates. The four relocated accounts' phone numbers moved with them. `billing_country` is deliberately NOT authored in the seeds. Measured on 17.0.0-rc.1 against a fresh install, hooks DO run over seed writes — all nine rows come out of `pnpm dev` carrying the projection that only `account_protection` writes: sqlite> SELECT name, billing_country FROM crm_account; Acme Corporation|US Stark Medical|CA Northwind Energy|DE Apex Logistics|SG Lattice Education|UK … That settles #617: the seed doctrine's premise ("the loader uses skipTriggers, hooks never run over seed rows") was wrong, and the platform says so itself — `SeedLoaderService.SEED_OPTIONS` documents `skipTriggers` as suppressing record-change AUTOMATION for seed writes while "Lifecycle HOOKS (derived/default fields, validation) still run". The doctrine block is rewritten around the corrected premise; its conclusion (a seeded value of a hook-owned field must equal what the hook computes) holds either way and is kept. A readonly, hook-derived column is now explicitly out of bounds for a seed: a hand-copied `billing_country` would be a second source of truth for a value the hook already owns. `test/territory-seed-coverage.test.ts` refuses to let this go quiet again. It walks the whole chain with the platform's own pieces — seed record → the real `account_protection` handler → `compileCelToFilter` (the call plugin-sharing makes) → which accounts each territory covers — and fails when any of the three buckets empties out. Verified non-vacuous: moving the SG account into the US turns it red. Seed fixtures are split by object family (#635). `src/data/index.ts` was 1.5KB under the 100KB source-hygiene cap, so nine addresses did not fit, and the cheapest way under the cap would have been deleting the comments the cap exists to protect. The file is now the aggregating `CrmSeedData` export over `_shared.ts` (doctrine + cross-family helpers), `catalog.seed.ts`, `sales.seed.ts`, `service.seed.ts`, `marketing.seed.ts` and `revenue.seed.ts`. The split is provably data-identical: importing both revisions in one process and diffing the aggregated datasets reports 16 datasets before and after with the only record differences being the nine addresses and four phone numbers. A second test fails if a family module's dataset is ever left out of `CrmSeedData`, which is the one failure mode the split introduces. Not fixed here: no user holds `na_sales_team` / `eu_sales_team` on a fresh install (`sys_user_position` is empty, so `sys_record_share` stays empty), which needs demo-user design rather than seed data. Filed separately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019SS7C5SXpniKeCApxgARyf
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
os-zhuang
marked this pull request as ready for review
August 2, 2026 16:13
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #638
问题
#621 修好了"安装"那一半:两条区域规则现在能编译、能播种(
seeded: 9, skipped: 0)。"数据"那一半仍然是空的——9 个种子客户没有一个带
billing_address,于是两条规则在9 条记录上正确求值,匹配 0 条。Setup 里能看到两个区域、两个 position,后面什么都没有。
改了什么:9 个客户的地址,跨三种结果分布
north_america_territorynorth_america_territoryeurope_territoryeurope_territory那条落在区域外的记录是承重的,不是凑数:如果 9 个客户全都落进某个区域,一条退化成
match-all 的规则和一条正常工作的规则在演示上完全一样,#621 那类缺陷就没有任何观测点。
按 issue 的建议分布,规则是可演示地划分这批客户:6 / 2 / 1。
country写的是规则匹配的两字母代码(UK而不是 ISO 的GB)——投影只做 trim +大写,不做翻译,所以 'United States' 会静默地掉出所有区域。四个换了国家的客户的电话
号码也跟着换了(慕尼黑的客户挂着丹佛的号码,是评估者在看到功能之前就会注意到的不一致)。
种子写入到底跑不跑钩子(#617 必须先答的那一问)
跑。 这不是推断,是实测。
pnpm dev起一个全新库,直接读 SQLite:billing_country全程只有一个写者:account_protection的beforeInsert投影。种子里一个字都没写它,而 9 行全都带上了值 —— 钩子确实在种子写入上跑了。
平台自己的文档也是这么说的(
SeedLoaderService.SEED_OPTIONS,@objectstack/metadata-protocol):isSystem—— 绕过 RBAC,并关闭organization_id/owner_id的自动注入(这正是种子行无主、需要
demo_bootstrap认领的原因);skipTriggers—— 抑制的是记录变更自动化(autolaunched flow),原文:"Lifecycle HOOKS (derived/default fields, validation) still run";
seedReplay—— 跳过state_machine的入口/迁移校验,好让中途状态的种子行(closed_won)能落库。所以
src/data/_shared.ts里的种子教条块已按实测重写:前提改掉,结论保留——"钩子拥有的字段,种子里写的值必须等于钩子会算出的值"在两种前提下都成立,只是失效时机从"第一次用户编辑"
提前到了"播种当场"。#617 可以据此收口。
为什么不在种子里补写
billing_country因为钩子会跑,所以不需要;更重要的是不应该。
billing_country是readonly的派生列,种子里手抄一份就等于给一个钩子已经拥有的值造了第二个真值源——正是那段教条要防的漂移。教条块现在
把这一条显式写成第 3 条规则。测试从反方向钉住这个依赖:每个种子地址都必须带一个投影能变成
区域归属的国家码。
顺带做掉的结构性拆分(#635)
src/data/index.ts距 100KB 上限只剩 1.5KB,9 个地址塞不进去;而在时间压力下最省字节的出路是删注释,恰好是这个上限想保护的东西。按 #635 的首选方案拆:
拆分是数据等价的,而且是证明过的:在同一个进程里同时 import 本分支和
origin/main两份种子,按
object + externalId配对逐条 diff:除这 9 条外没有任何记录变化,也没有数据集丢失或新增。
测试
新增
test/territory-seed-coverage.test.ts(8 个用例)。它不复刻任何逻辑,而是用平台自己的零件把整条链走一遍:
断言:每个客户都有两字母国家码;种子不得书写
billing_country;钩子跑完每行都有投影;NA / EU / 两者皆非 三个桶都非空;两个区域互斥;NA 桶不能退化成只有 US 一个国家
(否则
$in列表其余部分等于没测)。非空洞验证:把 SG 那条改成 US 后测试立刻转红,报的是有信息量的失败:
另有一组用例专治拆分自身唯一的新失败模式:某个 family 模块加了数据集却没接进
CrmSeedData(编译通过、源码里有记录、就是永远不播种)。
验证
真机(
pnpm dev,空库)播种后的实际划分:播种出来的规则行确实拿着这两个过滤器:
{"name":"north_america_territory","criteria_json":"{\"billing_country\":{\"$in\":[\"US\",\"CA\",\"MX\"]}}"} {"name":"europe_territory","criteria_json":"{\"billing_country\":{\"$in\":[\"UK\",\"DE\",\"FR\",\"IT\",\"ES\"]}}"}需要维护者知道的三件事
sys_user_position在全新安装上是 0 行,所以匹配到的客户依旧不给任何人授权(
sys_record_share= 0)。按认领意见的边界,这不是demo_bootstrap里的一两行:唯一的用户已经是全部记录的 owner + 管理员,把他塞进两个 team 授不出任何可观测的
权限;要真的演示区域共享,需要非管理员的演示用户、每人持哪些 position 的决定,而同一个
决定还牵动另外 7 个 position、4 个审批节点和
lead_assignment的sales_rep池。Nobody holds any position on a fresh install, so every position-based sharing rule and both approval flows resolve to an empty recipient set #640 把实测数据和待定的三个问题都写清楚了,交由排期。
content/docs/一个字没动:管理员文档里关于区域规则的描述本来就是准确的(它讲的是机制,不是演示数据),本 PR 不给它加演示数据细节,以免在三个语言版本里制造漂移。
content/docs/releases/未触碰;本次的发布输入是 changeset。🤖 Generated with Claude Code
https://claude.ai/code/session_019SS7C5SXpniKeCApxgARyf
Generated by Claude Code