Skip to content

Commit 778aedf

Browse files
fix(platform-objects, lint): the Business Unit page and the readScope hint say what needs the enterprise edition (#30622)
<!-- claude-projects-attribution: {"github_login":"os-bill"} --> _Requested by **Bill** · [project thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgQCCAGJ1VCZe1N15s2gYhCQ)_ domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE (dev: card thread session_01Utp6CYwoPbRRkiyph2ufCw) ## 维护者速读 - 改了什么:业务单元页面说明改成「开源版里共享规则可以按业务单元共享、成员离开即失去;按业务单元读取全部记录需要企业版」;业务单元时区帮助不再声称日期按它计算;`os validate` 的 `security-private-no-readscope` 提示在项目没声明 `hierarchy-security` 时只建议 `org`,并写明另外三个范围需要企业包。 - 为什么改:管理员照页面和提示去配,结果开源版编译直接拒绝(#30210 第 1、2 行);时区帮助说的事运行时根本没人读。 - 风险与回滚:只改说明文字和一条 info 级提示的 hint,编译拒绝、规则触发条件与级别都不变;回滚即 revert 本 PR。 - 先看:`packages/lint/src/validate-security-posture.ts`(`hierarchyScopesCompile` 那段)、`packages/platform-objects/src/identity/sys-business-unit.object.ts`(description 与 timezone)、`zh-CN.objects.generated.ts` 两处中文。 - 两分钟验证:`pnpm --filter @objectstack/lint exec vitest run src/validate-security-posture.test.ts -t hierarchy-security`,预期 2 条通过;无 `requires` 时 hint 只建议 `'org'` 并点名 `@objectstack/security-enterprise`。 - 需要你做的:无,席位落地。 Refs #30210 (rows 1, 2) Clause-②: no Metadata-protocol: none Skills-docs: none (`git grep` of `skills/**` for business unit, readScope and security-private-no-readscope: 6 hits in `skills/objectstack-data` — SKILL.md lines 73 and 388, rules/security.md lines 178 and 252, one eval; none states a sentence this diff makes false) ## What changes - **Row 1 — Business Unit page.** `sys_business_unit.description` (the page text) said units are "used to scope data and reports". It now says what a unit does in the open edition (a sharing rule can share records with a unit, or a unit and the units under it; members get that access and lose it when they leave — measured on the card) and that letting a permission set read every record of a unit (read scope `unit`) needs the enterprise edition. The compile refusal (`validateHierarchyScopeCapability` in `stack.zod.ts`) is unchanged, as the card asks. zh-CN value hand-written; `en` regenerated by `pnpm i18n:extract`. - **Seat amendment 6662593558 — Business unit Timezone help.** It said date boundaries are computed in this zone; nothing reads `sys_business_unit.timezone` (same reading as `sys_organization.timezone`, corrected in #25347). The help now says it changes no date calculation and names Settings → Localization → Default timezone, mirroring the organization's help. en + zh-CN. No field change. - **Row 2 — `security-private-no-readscope` hint** (`packages/lint`, run by `os validate` / `os lint`). The rule now reads the stack's `requires` — the same declaration `defineStack` reads before refusing a hierarchy scope. With `hierarchy-security` declared, the hint is unchanged. Without it, the hint offers `readScope: 'org'` or widening `sharingModel`, and says `'own_and_reports'`, `'unit'` and `'unit_and_below'` need `requires: ['hierarchy-security']`, which `@objectstack/security-enterprise` provides. A stack with no `requires` (the runtime metadata door hands none) gets the requirement named too. Same grants fire, same `info` severity. Not built here: **row 3** (Timezone on Edit organization) is measured only, per the order — removing a `sys_organization` field narrows an accept set and is the owning lane's ADR-0049 removal. **Row 4** (hide Parent Organization / Sort Order on one organization) stopped as `needs_decision`: field and field-group `visibleWhen` bind only `record` / `previous` / `parent` at authoring (`FIELD_RULE_BOUND_ROOTS`, `packages/lint/src/validate-expressions.ts`), and fields carry no `requiresFeature`, so hiding needs a new metadata capability. Both readings and the row 4 options are in the os-dev-report on the card. ## Tests - `packages/platform-objects/src/identity/sys-business-unit-edition-copy.test.ts` (new): the description names sharing rules and the enterprise edition and drops the scoping claim; the zh-CN description says 共享规则 / 企业版; the Timezone help (en, zh-CN) says it changes no date calculation and names Localization's Default timezone. - `packages/lint/src/validate-security-posture.test.ts`: two cases in the existing `security-private-no-readscope` block — without `hierarchy-security`, the hint offers `'org'` before any hierarchy scope and names the requirement in the same sentence as all three; with it, every scope is offered and the enterprise package is not named. The #5017 key-surface meta-test now lists `requires` among the keys read off `stack` (declared by `ObjectStackSchema`). <details><summary>Red on main 2ffdb64, green on the fix</summary> ```text red (main 2ffdb64, tests only): lint × without hierarchy-security, offers org and names the requirement beside the hierarchy scopes Tests 1 failed | 1 passed platform-objects × all 4 cases Tests 4 failed (4) green (fe3d2a1): @objectstack/lint test Test Files 195 passed (195) Tests 8105 passed (8105) @objectstack/platform-objects test Test Files 114 passed (114) Tests 1567 passed (1567) both typechecks exit 0 ``` </details> <details><summary>Ablation (once): the requires read replaced by true</summary> ```text ablation-replace: ok mutation landed: anchor 1 -> 0, blob d877152 -> 6eded5d4c050 × without hierarchy-security, offers org and names the requirement beside the hierarchy scopes × every key read off 'stack' is declared by 'ObjectStackSchema' restored: git hash-object == HEAD blob d877152, git diff HEAD empty ``` </details> ## Acceptance notes - The order's file surface named `packages/cli/src/**` for the advisory; it lives in `packages/lint`. The seat's amendment 6662593558 added `packages/lint/src/validate-security-posture.ts` and its tests. - `pnpm i18n:extract` rewrote only `en`; ja-JP / es-ES kept their existing values (their source-hash ledgers do not cover these keys), and `check:i18n` / `check:i18n-stale-fill` are green. - `sys_business_unit`'s docblock lists a `bu:` approver prefix in the approval engine; `plugin-approvals` has no such prefix on `main`. A code comment, not admin-facing — left alone. ## Verification All local readings on head fe3d2a1 (after merging origin/main bdbaf80, which carries #30551; the delta vs main is exactly the 7 files of this PR). `pnpm i18n:extract` after the merge wrote no change. <details><summary>dispatch-gates: 59 derived families, 59 run, 0 NOT-MEASURED; plus both packages' test and typecheck and pnpm lint — 64/64 exit 0</summary> ```text node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-adoption.mjs :: exit 0 node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-dts-emitted.mjs --self-test :: exit 0 node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 node scripts/check-empty-changeset.mjs --self-test :: exit 0 node scripts/check-issue-citations.mjs :: exit 0 node scripts/check-plugin-teardown-shape.mjs :: exit 0 node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0 node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0 node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0 node scripts/docs-audit/check-affected-docs.mjs :: exit 0 node scripts/docs-audit/check-drift-comment.mjs :: exit 0 node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 node scripts/release-pending-publish.mjs --self-test :: exit 0 pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 pnpm check:changeset-gate-self-tests :: exit 0 pnpm check:ci-filter-parity :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:doc-authoring :: exit 0 pnpm check:docs-transcript-drift :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:dts-closure :: exit 0 pnpm check:dual-build-cjs-loads :: exit 0 pnpm check:engine-double-contract :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:i18n :: exit 0 pnpm check:i18n-stale-fill :: exit 0 pnpm check:issue-citations :: exit 0 pnpm check:keyed-text-bounds :: exit 0 pnpm check:lean-entry-closure :: exit 0 pnpm check:logger-receiver-detach :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:objectql-double-limit :: exit 0 pnpm check:org-identifier :: exit 0 pnpm check:page-declaration-shape :: exit 0 pnpm check:platform-object-tenancy-census :: exit 0 pnpm check:published-files :: exit 0 pnpm check:query-options-erasure :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:registry-log-declared :: exit 0 pnpm check:rest-log-spy-declared :: exit 0 pnpm check:slot-lookup :: exit 0 pnpm check:sourcemap-no-sources-content :: exit 0 pnpm check:system-context-census :: exit 0 pnpm check:test-source-alias :: exit 0 pnpm check:tier-file-adoption :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 0 pnpm check:undeclared-dep-imports :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:where-matcher :: exit 0 pnpm --filter @objectstack/lint test :: exit 0 pnpm --filter @objectstack/lint typecheck :: exit 0 pnpm --filter @objectstack/platform-objects test :: exit 0 pnpm --filter @objectstack/platform-objects typecheck :: exit 0 pnpm lint :: exit 0 dispatch-gates --ran: 59 derived famil(ies) accounted for — 59 run, 0 NOT-MEASURED ``` </details> CI families are read from this PR's check-runs, not re-run here. --- _Generated by [Claude Code](https://claude.ai/code/session_01Utp6CYwoPbRRkiyph2ufCw)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 740edc5 commit 778aedf

7 files changed

Lines changed: 139 additions & 11 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
'@objectstack/lint': patch
4+
---
5+
6+
fix: the Business Unit page and the `security-private-no-readscope` hint say what needs the enterprise edition
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
- **Business Unit page (`sys_business_unit` description, en and zh-CN).** It used to say units are "used to scope data and reports". It now says what a unit does in the open edition: sharing rules can share records with a unit, or a unit and the units under it, and members get that access and lose it when they leave. It also says that letting a permission set read every record of a unit (read scope `unit`) needs the enterprise edition. That matches the compile refusal, which is unchanged.
12+
- **Business unit Timezone help (en and zh-CN).** It used to say date boundaries are computed in this zone. Nothing reads the column, so the help now says it changes no date calculation and that Settings → Localization → Default timezone is the one in effect, as the organization's Timezone help already does.
13+
- **`security-private-no-readscope` hint (`os validate`, `os lint`).** The hint now offers only the scopes the stack can compile. When `requires` lists `hierarchy-security`, nothing changes: it offers `own_and_reports`, `unit`, `unit_and_below` and `org`. Without it, the hint offers `org` (or widening the object's `sharingModel`), and says that the three hierarchy scopes need `requires: ['hierarchy-security']`, which `@objectstack/security-enterprise` provides. The rule still fires on the same grants at the same severity.

‎packages/lint/src/validate-security-posture.test.ts‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,42 @@ describe('validateSecurityPosture (ADR-0090 D7)', () => {
792792
expect(findings[0]).toMatchObject({ severity: 'info', rule: SECURITY_PRIVATE_NO_READSCOPE });
793793
});
794794

795+
// The hint offers only the scopes this stack can compile: `defineStack`
796+
// refuses `own_and_reports` / `unit` / `unit_and_below` unless `requires`
797+
// names `hierarchy-security`, so without that token the hint names the
798+
// requirement in the same sentence as those scopes instead of offering them
799+
// bare.
800+
const noReadscopeHint = (requires?: string[]): string =>
801+
validateSecurityPosture({
802+
...(requires ? { requires } : {}),
803+
objects: [{ name: 'expense', sharingModel: 'private' }],
804+
permissions: [{ name: 'finance_user', objects: { expense: { allowRead: true } } }],
805+
}).find((f) => f.rule === SECURITY_PRIVATE_NO_READSCOPE)?.hint ?? '';
806+
807+
it('without hierarchy-security, offers org and names the requirement beside the hierarchy scopes', () => {
808+
const hint = noReadscopeHint();
809+
const requirementAt = hint.indexOf("requires: ['hierarchy-security']");
810+
expect(requirementAt).toBeGreaterThan(-1);
811+
expect(hint).toContain('@objectstack/security-enterprise');
812+
const firstHierarchyScope = Math.min(
813+
...["'own_and_reports'", "'unit'", "'unit_and_below'"].map((s) => {
814+
const at = hint.indexOf(s);
815+
expect(at, `${s} is named`).toBeGreaterThan(-1);
816+
return at;
817+
}),
818+
);
819+
// What the hint offers before any hierarchy scope is `org` alone…
820+
expect(hint.slice(0, firstHierarchyScope)).toContain("readScope: 'org'");
821+
// …and no sentence ends between the first hierarchy scope and the requirement.
822+
expect(hint.slice(firstHierarchyScope, requirementAt)).not.toMatch(/\.\s/);
823+
});
824+
825+
it('with hierarchy-security declared, offers every scope and does not name the requirement', () => {
826+
const hint = noReadscopeHint(['hierarchy-security']);
827+
for (const scope of ["'own_and_reports'", "'unit'", "'unit_and_below'", "'org'"]) expect(hint).toContain(scope);
828+
expect(hint).not.toContain('security-enterprise');
829+
});
830+
795831
it('stays silent when depth or VAMA is declared, or the object is public', () => {
796832
expect(
797833
rulesOf({
@@ -1762,7 +1798,9 @@ const READ_SURFACES: Array<{ receiver: string; expected: string[]; declaredBy: s
17621798
// controlled-by-parent rules started judging an object on its MERGED
17631799
// field set: the extensions a stack (or each package body of an artifact)
17641800
// declares are folded onto the objects it declares.
1765-
expected: ['apps', 'books', 'capabilities', 'data', 'objectExtensions', 'objects', 'packages', 'permissions', 'positions'],
1801+
// [#30210] `requires` joined when the private-no-readscope hint started
1802+
// offering the hierarchy scopes only where `hierarchy-security` is declared.
1803+
expected: ['apps', 'books', 'capabilities', 'data', 'objectExtensions', 'objects', 'packages', 'permissions', 'positions', 'requires'],
17661804
declaredBy: 'ObjectStackSchema',
17671805
keys: () => Object.keys(ObjectStackSchema.shape),
17681806
},

‎packages/lint/src/validate-security-posture.ts‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1372,6 +1372,15 @@ export function validateSecurityPosture(stack: AnyRec, opts?: { nowMs?: number }
13721372
})
13731373
.map((o) => String(o.name ?? '')),
13741374
);
1375+
// The hint offers only the scopes this stack can compile. `defineStack`
1376+
// refuses a hierarchy scope (`own_and_reports` / `unit` / `unit_and_below`,
1377+
// `findHierarchyScopeGrants`) unless `requires` names `hierarchy-security`
1378+
// (ADR-0057), the enterprise capability — so read the same declaration here
1379+
// and, without it, name the requirement beside those scopes instead of
1380+
// offering them bare. A stack with no `requires` (the runtime metadata
1381+
// door hands none) gets the requirement named too.
1382+
const hierarchyScopesCompile =
1383+
Array.isArray(stack.requires) && (stack.requires as unknown[]).includes('hierarchy-security');
13751384
if (privateObjects.size > 0) {
13761385
for (let i = 0; i < permissionSets.length; i++) {
13771386
const ps = permissionSets[i];
@@ -1390,9 +1399,12 @@ export function validateSecurityPosture(stack: AnyRec, opts?: { nowMs?: number }
13901399
message:
13911400
`"${objName}" is private (OWD) and this set grants allowRead without a readScope — ` +
13921401
`holders see ONLY records they own (plus explicit shares).`,
1393-
hint:
1394-
`If that is intended (personal data), ignore this. Otherwise add readScope: ` +
1395-
`'own_and_reports' | 'unit' | 'unit_and_below' | 'org', or widen the object's sharingModel.`,
1402+
hint: hierarchyScopesCompile
1403+
? `If that is intended (personal data), ignore this. Otherwise add readScope: ` +
1404+
`'own_and_reports' | 'unit' | 'unit_and_below' | 'org', or widen the object's sharingModel.`
1405+
: `If that is intended (personal data), ignore this. Otherwise add readScope: 'org', ` +
1406+
`or widen the object's sharingModel. Scopes 'own_and_reports', 'unit' and 'unit_and_below' ` +
1407+
`need requires: ['hierarchy-security'], which @objectstack/security-enterprise provides.`,
13961408
});
13971409
}
13981410
}

‎packages/platform-objects/src/apps/translations/en.objects.generated.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -984,7 +984,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
984984
sys_business_unit: {
985985
label: "Business Unit",
986986
pluralLabel: "Business Units",
987-
description: "Your company structure — divisions, departments, regions and offices — used to scope data and reports.",
987+
description: "Your company structure — divisions, departments, regions and offices. Sharing rules can share records with a unit, or a unit and the units under it: members get that access, and lose it when they leave. Letting a permission set read every record of a unit (read scope 'unit') needs the enterprise edition.",
988988
fields: {
989989
name: {
990990
label: "Name"
@@ -1010,7 +1010,7 @@ export const enObjects: NonNullable<TranslationData['objects']> = {
10101010
},
10111011
timezone: {
10121012
label: "Timezone",
1013-
help: "IANA time zone (e.g. Asia/Shanghai) date boundaries are computed in for this unit. Leave unset to inherit the parent unit's zone, then the organization's, then UTC."
1013+
help: "IANA time zone (e.g. Asia/Shanghai) kept for this unit; leave it empty to take the parent unit's, then the organization's. It does not change any date calculation today: formulas, reports and displayed dates use Settings → Localization → Default timezone."
10141014
},
10151015
organization_id: {
10161016
label: "Organization",

‎packages/platform-objects/src/apps/translations/zh-CN.objects.generated.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -984,7 +984,7 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
984984
sys_business_unit: {
985985
label: "业务单元",
986986
pluralLabel: "业务单元",
987-
description: "公司的组织结构——事业部、部门、区域和办公地点——用于划定数据和报表的范围。",
987+
description: "公司的组织结构——事业部、部门、区域和办公地点。共享规则可以把记录共享给某个业务单元,或某个业务单元及其下级单元:成员获得这些访问权限,离开后即失去。让权限集按业务单元读取全部记录(读取范围「unit」)需要企业版。",
988988
fields: {
989989
name: {
990990
label: "名称"
@@ -1010,7 +1010,7 @@ export const zhCNObjects: NonNullable<TranslationData['objects']> = {
10101010
},
10111011
timezone: {
10121012
label: "时区",
1013-
help: "该业务单元计算日期边界所用的 IANA 时区(如 Asia/Shanghai)。留空则依次继承上级业务单元的时区、组织的时区,最后为 UTC。"
1013+
help: "为该业务单元保存的 IANA 时区(如 Asia/Shanghai);留空则取上级业务单元的,再取组织的。它目前不会改变任何日期计算:公式、报表和显示的日期使用 设置 → 本地化 → 默认时区。"
10141014
},
10151015
organization_id: {
10161016
label: "组织",
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// The Business Unit page says what a unit does in the open edition, and what
4+
// needs the enterprise edition (#30210 row 1). It used to say units are "used
5+
// to scope data and reports", while scoping a permission set's read access by
6+
// unit (`readScope: 'unit'` and the other hierarchy scopes) is refused at
7+
// compile without `requires: ['hierarchy-security']`, which only
8+
// `@objectstack/security-enterprise` provides. What does work in the open
9+
// edition is a sharing rule naming a unit (or a unit and the units under it),
10+
// whose access follows membership in and out.
11+
//
12+
// The unit's `timezone` help used to say date boundaries are computed in it.
13+
// Nothing reads the column (the same reading as `sys_organization.timezone`,
14+
// pinned in `sys-organization-name-timezone-help.test.ts`), so the help says
15+
// which setting is in effect instead.
16+
import { describe, expect, it } from 'vitest';
17+
import { SysBusinessUnit } from './sys-business-unit.object.js';
18+
import { zhCNObjects } from '../apps/translations/zh-CN.objects.generated.js';
19+
20+
const zhUnit = () =>
21+
(zhCNObjects as unknown as Record<string, { description?: string; fields?: Record<string, { help?: string }> }>)
22+
.sys_business_unit;
23+
24+
describe('sys_business_unit: the page says what the open edition gives and what needs enterprise (#30210)', () => {
25+
it('names sharing rules as what a unit drives, and the enterprise requirement for unit-scoped read access', () => {
26+
const d = SysBusinessUnit.description ?? '';
27+
expect(d).toMatch(/[Ss]haring rules/);
28+
expect(d).toMatch(/enterprise edition/);
29+
expect(d).not.toMatch(/used to scope data and reports/);
30+
});
31+
32+
it('the zh-CN page text says the same, not the old scoping claim', () => {
33+
const d = zhUnit()?.description ?? '';
34+
expect(d).toMatch(/共享规则/);
35+
expect(d).toMatch(/企业版/);
36+
expect(d).not.toMatch(/划定数据和报表的范围/);
37+
});
38+
39+
it('the zh-CN timezone help no longer says date boundaries are computed in it', () => {
40+
const help = zhUnit()?.fields?.timezone?.help ?? '';
41+
expect(help).toMatch(/不会改变任何日期计算/);
42+
expect(help).not.toMatch(/计算日期边界所用/);
43+
});
44+
45+
it("the timezone help says it changes no date calculation and that Localization's Default timezone wins", () => {
46+
const help = (SysBusinessUnit.fields as Record<string, { description?: string }>).timezone.description ?? '';
47+
expect(help).toMatch(/does not change any date calculation/);
48+
expect(help).toMatch(/Settings → Localization → Default timezone/);
49+
expect(help).not.toMatch(/are computed in/);
50+
});
51+
});

‎packages/platform-objects/src/identity/sys-business-unit.object.ts‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,17 @@ export const SysBusinessUnit = ObjectSchema.create({
3030
managedBy: 'platform',
3131
// Admin-facing copy: what the records are and what an admin does here. The
3232
// design reference (canonical org/data-partition tree, ADR-0057 D2) lives in
33-
// the docblock above, never in this string.
34-
description: 'Your company structure — divisions, departments, regions and offices — used to scope data and reports.',
33+
// the docblock above, never in this string. It says what a unit does in
34+
// the open edition (sharing rules naming a unit, whose access follows
35+
// membership in and out) and that scoping a permission set's read access by
36+
// unit (`readScope: 'unit'` and the other ADR-0057 hierarchy scopes) needs
37+
// the enterprise edition: `defineStack` refuses those scopes without
38+
// `requires: ['hierarchy-security']`, which only
39+
// @objectstack/security-enterprise provides.
40+
description:
41+
'Your company structure — divisions, departments, regions and offices. Sharing rules can share records with a unit, ' +
42+
'or a unit and the units under it: members get that access, and lose it when they leave. ' +
43+
"Letting a permission set read every record of a unit (read scope 'unit') needs the enterprise edition.",
3544
displayNameField: 'name',
3645
nameField: 'name', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField)
3746
highlightFields: ['name', 'kind', 'parent_business_unit_id', 'manager_user_id'],
@@ -189,13 +198,18 @@ export const SysBusinessUnit = ObjectSchema.create({
189198
// bound unconverged). No `defaultValue`, deliberately: an explicit
190199
// default here would mean "stop inheriting", which is the opposite of what
191200
// an unset unit means.
201+
//
202+
// [#30210] Because nothing reads the column, the help says so in the
203+
// reader's terms and names the setting in effect — the same correction
204+
// `sys_organization.timezone` took (#25347). The old help said date
205+
// boundaries ARE computed in this zone, a claim no reader backed.
192206
timezone: Field.text({
193207
label: 'Timezone',
194208
required: false,
195209
maxLength: 64,
196210
valueDomain: 'iana_time_zone',
197211
description:
198-
'IANA time zone (e.g. Asia/Shanghai) date boundaries are computed in for this unit. Leave unset to inherit the parent unit\'s zone, then the organization\'s, then UTC.',
212+
'IANA time zone (e.g. Asia/Shanghai) kept for this unit; leave it empty to take the parent unit\'s, then the organization\'s. It does not change any date calculation today: formulas, reports and displayed dates use Settings → Localization → Default timezone.',
199213
group: 'Hierarchy',
200214
}),
201215

0 commit comments

Comments
 (0)