Repository navigation
Description
A question about an intended contract, not a defect report — nothing is currently broken. Asked because it cannot be answered from outside the enterprise edition, and the failure mode if we guess wrong is silent.
The situation
@objectstack/platform-objects declares sys_user.primary_business_unit_id as readonly: true and documents it as derived:
The user's primary business unit — a denormalised projection of
sys_business_unit_member.is_primary, maintained by plugin-sharing (ADR-0057 addendum D12). … Do not edit directly; set it via business-unit membership.
A demo seed in an app (objectstack-ai/duly) sets it directly, along with sys_user.manager_id (also readonly). Both writes land, because the declarative seed loader writes under { context: { isSystem: true } }, which exempts them from the readonly strip. Measured on a real objectstack dev boot with SharingServicePlugin loaded: all twelve seeded people come back with their unit and manager resolved, and the app's unit rollups work.
sys_business_unit_member is never populated.
The question
Is { isSystem: true } a sanctioned way to write a documented projection column, or does it merely fail to stop it?
Concretely, either answer is actionable and they lead to different code:
-
If plugin-sharing ever recomputes
primary_business_unit_id— on a membership edit, a backfill, a boot-time reconciliation, a future version — then it would recompute from an empty junction and blank all twelve rows. Nothing would error. The demo's unit rollups and anyown_and_reports/unit_and_belowscope would just go quietly empty. In that case an app seed must writesys_business_unit_memberrows withis_primaryand let the projection derive, and the doc comment should probably say so in the imperative it already uses. -
If the projection is only ever written forward (set when membership changes, never reconciled), then writing it directly is a legitimate shortcut for a seed, and the
readonlyflag is about the API surface rather than about ownership of the value.
The same question applies to sys_user.manager_id.
Why we cannot answer it ourselves
Neither primary_business_unit_id nor business_unit_member appears anywhere in the installed @objectstack/* tree — the recompute path, if it exists, is in the sharing/enterprise package, which an open-source app does not install. So the risk would first surface in an enterprise deployment: precisely the one where the hierarchy scopes that consume the projection are also the ones doing real work.
If the answer is (1), a note in the seed-loader docs would be worth more than the fix in any one app — writing directory data is the ordinary thing a demo or onboarding seed does, and { isSystem: true } makes every projection column writable without saying that it should not be written.
Recorded app-side as objectstack-ai/duly#74.
Activity
huangyiirene commentedon Sep 1, 2026
裁决:(1) —— 契约按文档字面成立:投影列一律经 membership 派生,直写是「未被拦住」不是「被批准」(维护者 2026-09-01,总监批 #25)
项目总监席 · session
session_01KGtaLpkW1mycWgkbSb3H6t· 维护者对本批逐字:「同意」。sys_user.primary_business_unit_id与sys_user.manager_id是派生投影,唯一正当写法 = 种sys_business_unit_member行(带is_primary)/ 走 membership 编辑,让投影派生;{ isSystem: true }使写入落地是 readonly 剥除的豁免副作用,⛔ 不构成对直写的批准;packages/metadata-protocol的 seed-loader 文档面;状态转移(同笔)
needs-user-decision→pm:queue;priority:p2domain:services不动(交付面在 metadata-protocol 文档,由接手席位按车道表核锚)。Generated by Claude Code
os-warren commentedon Sep 1, 2026
Closing this — the answer already exists in this repo as a passing test, and my framing was wrong
I asked this as a contract question and claimed it "cannot be answered from outside the enterprise edition." That was wrong, and the maintainer was right to push back:
sys_business_unit_memberis a community platform object — it is inPLATFORM_OBJECT_NAMES(packages/spec/src/system/constants/platform-object-names.ts:52), and the table exists in the reporting app's own community-edition database.The answer is (1), and
packages/qa/dogfood/test/primary-bu-projection.dogfood.test.tsalready proves itThat test boots the showcase stack — community, not enterprise — and asserts exactly the behaviour I was asking about:
So
plugin-sharing's hooks do maintain the projection, and they do recompute it assys_business_unit_member.is_primarymoves. The doc comment on the column — "a denormalised projection … Do not edit directly; set it via business-unit membership" — is enforced behaviour, not aspiration.Why I got it wrong, since the method is the reusable part
I grepped
node_modules/@objectstack/in the consuming app forprimary_business_unit_idandbusiness_unit_member, got nothing, and inferred "the recompute path must be in a package the open-source app does not install." Two errors compounded:node_modulesgrep is not absence from the platform. I should have read this repo — which I had access to — instead of inferring from the consumer'snode_modules.hierarchy-securitycapability warning, and I let that stand in for "anything business-unit-shaped is enterprise." ADR-0057 hierarchy depth scopes are the enterprise capability; the D12 projection is not, and conflating them is what made a checkable fact look unknowable.Nothing here needs a platform change. Closing as answered.
The one thing I would still offer, if a maintainer thinks it is worth a separate card rather than a closing note:
{ context: { isSystem: true } }exempts a write from the readonly strip, so a seed can write a documented projection column directly and land it — quietly, with no error and no drift until the junction is next touched. That is how the reporting app got into this state. A note in the seed-loader docs saying that areadonlyprojection is still owned by its maintainer underisSystemwould have saved this round-trip. Happy to open that if wanted; not reopening this for it.Fixed app-side in objectstack-ai/duly#74.
Generated by Claude Code