Repository navigation
Commit 677d542
feat(spec): a field group's requiresFeature hides the group where a deployment feature is off; Edit organization hides Parent Organization and Sort Order on one organization (#30810)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DKdMdBgsLj6ZtbKmEBFgfcw)_
Fixes #30692
Clause-②: yes
Metadata-protocol: added
Skills-docs: follow-up #30812 —
skills/objectstack-data/references/examples-objects.md (its Field Groups
section teaches `fieldGroups` and should name `visibleWhen` and
`requiresFeature` beside `collapse`); kept out of this diff so it does
not become a governed `skills/**` change
## 维护者速读
- 改了什么:字段分组新增 `requiresFeature`(与动作上的同名键同一套特性名),解析时降成该分组的 `visibleWhen`
并从输出中去掉;组织对象声明了四个字段分组,「上级组织」「排序」所在的「集团结构」组要求 `multiOrgEnabled`。
- 为什么改:单组织部署的「编辑组织」页一直显示这两个毫无作用的字段(裁定 6670372892,#30210 第 4 行
A)。现在只有多组织部署才看得到。
- 风险与回滚:表单和详情页现在按分组显示(标题:基本信息 / 品牌 / 配置 / 集团结构),字段顺序不变;隐藏只是界面层,API
照常读写这两个字段。回滚即撤本 PR。
- 先看:`packages/spec/src/data/object.zod.ts`(新键与降级
transform);`packages/lint/src/validate-expressions.ts` 的
`withoutLoweredFeatureGate`(lint
只放行降级出来的那一个门);`packages/platform-objects/src/identity/sys-organization.object.ts`(四个分组)。
- 两分钟验证:`pnpm --filter @objectstack/platform-objects exec vitest run
src/identity/sys-organization-group-structure-gate.test.ts`,预期 4
条全绿:`multiOrgEnabled` 关时两字段不出现,开时出现。
- 需要你做的:无,席位落地。
## What changes
- **`@objectstack/spec`: `fieldGroups[].requiresFeature`.** The
field-group twin of the action-level `requiresFeature`, from the same
`PUBLIC_AUTH_FEATURES` vocabulary, so an unknown flag is refused at
parse (`invalid_value` at `requiresFeature`). It is lowered at parse
into the group's `visibleWhen` by the same helper the action key uses,
`lowerRequiresFeature`, which now takes an optional third argument
naming the slot (`'visible'` by default, `'visibleWhen'` here); its
refusals name that slot. The slot is a type parameter and the input's
constraint is keyed on it, so lowering an action into `'visibleWhen'`,
or a field group into the default `'visible'`, fails to compile; the
action call sites are unchanged. The key is stripped from the output.
- **How the two combine** when an author writes both: AND, the author's
predicate first and the gate last, exactly as for actions: `visibleWhen:
"record.kind == 'child'"` + `requiresFeature: 'multiOrgEnabled'` parses
to `(record.kind == 'child') && features.multiOrgEnabled != false`. A
non-CEL `visibleWhen` refuses the pair, as for actions.
- `PUBLIC_AUTH_FEATURES.multiOrgEnabled.gatedInputs` books
`sys_organization.fieldGroups.group_structure`; the `gatedInputs` path
grammar admits `OBJECT.fieldGroups.KEY`, and the platform-objects
completeness guard walks field groups in both directions.
- `object.form.ts` offers the key on the `fieldGroups` repeater (label
and help in en / zh-CN / ja-JP / es-ES). Liveness:
`object/fieldGroups.requiresFeature` is `live`.
- **`@objectstack/lint`: the lowered gate is accepted.** The field-group
`visibleWhen` root verdict now judges the predicate with the trailing
gate a `requiresFeature` lowers to taken off
(`withoutLoweredFeatureGate`): the gate alone, or `(AUTHORED) && GATE`
where the authored part parses on its own. Any other `features` read in
a group predicate keeps its root verdict, and its message now points at
`requiresFeature`. The finding still quotes the whole predicate.
- **`@objectstack/platform-objects`: `sys_organization` declares
`fieldGroups`.** Identity, Branding, Configuration, Group Structure, in
the fields' own order, so no field moves; `id` / `created_at` /
`updated_at` keep `group: 'System'`, which is not declared, so they
render last in the ungrouped bucket as before. Group Structure
(`parent_organization_id`, `sort_order`) carries `requiresFeature:
'multiOrgEnabled'`. Section labels are translated in zh-CN / ja-JP /
es-ES.
The ui is unchanged, as the ruling says: the form's section gate and the
record page's `record:details`
(`ui/packages/plugin-detail/src/renderers/record-details.tsx`
`sectionPredicateVisible`) both evaluate a group's `visibleWhen` with
`usePredicateScope()`, which carries the host's `features`.
## Tests
- Reproduced on `main` (`1371b9d8`, the object before this PR, tests
from this PR): `sys-organization-group-structure-gate.test.ts` 3 failed
/ 1 passed; with `multiOrgEnabled: false` the flat layout still renders
`parent_organization_id`.
- New pins:
`packages/spec/src/data/field-group-requires-feature.test.ts` (lowered
and stripped; an opt-in flag gets its own gate; unknown flag refused at
`requiresFeature`; both-present AND-composition from a string and from
an envelope; unchanged without the key; inside `ObjectSchema.create`;
and two `@ts-expect-error` compile pins: an action lowered into
`'visibleWhen'` and a field group lowered into the default slot),
`packages/platform-objects/src/identity/sys-organization-group-structure-gate.test.ts`
(off hides, on shows, empty `features` faults and hides, only that group
gated, declaration order kept), and a `#30692` describe in the lint
harness `runtime-gate.object-field-group-visibility-writes.test.ts`
(three lowered shapes publish clean at the door and the build; the
unlowered key is clean; five hand-written `features` shapes keep the
verdict; the prescription names `requiresFeature`).
- Full suites on the pre-merge head `ab3d3a94`: `@objectstack/spec`
818/818 files, 25833 tests; `@objectstack/lint` 195/195, 8226;
`@objectstack/platform-objects` 117/117, 1585; the three typechecks exit
0; `check:generated` exit 0.
<details><summary>Ablations (one per changed behaviour)</summary>
- Spec lowering, through `dist/`: `scripts/ablation-replace.mjs`
replaced the transform with one that keeps the group and plants
`ABLATION_30692` (anchor 1 → 0, blob `47ddc173` → `c0bfefc3`); spec
rebuilt; `ablation-dist-preflight.mjs` found the marker in 10 built
files. Red: spec pin 4 failed / 2 passed; platform-objects 4 failed (the
three group-structure cells and the guard's forward row
`sys_organization.fieldGroups.group_structure is gated on
multiOrgEnabled`). Restored to the HEAD blob with `git diff HEAD` empty;
spec rebuilt; preflight `--absent` clean over 592 files and the tree;
the same two files green, 118/118.
- Lint peel (source-imported, no dist leg): the verdict call fed `raw`
instead of `withoutLoweredFeatureGate(raw)` (anchor 1 → 0, blob
`7b9a6ec1` → `85289f1e`). Red: the three lowered-shape CONTROL cells;
restored to the HEAD blob, `git diff HEAD` empty.
</details>
## Acceptance notes
- The `features` root is not opened on group predicates generally
(ruling row 4 B was not chosen): only the exact trailing gate the
lowering emits is accepted. A hand-written predicate byte-identical to
that lowered form is accepted too, because it is the same gate.
- **Fault direction.** A group's `visibleWhen` is fail-closed, where an
action's `visible` is fail-soft. Reading a key that `features` does not
carry is a CEL fault (`No such key`), not `null`, so with `features =
{}` the default-on gate faults and the group is hidden. The console
holds `features = {}` until `/auth/config` resolves (`AppContent.tsx`),
and keeps it if that fetch fails. So on a multi-organization deployment
Group Structure is hidden for the pre-fetch window and appears when the
config arrives; if the fetch fails it stays hidden for that session.
`getPublicConfig()` in plugin-auth always publishes `multiOrgEnabled`. A
host that never publishes `features` would hide the group everywhere.
The semantics table in `public-auth-features.ts`, the field-group
docblock and the changeset say so, and
`sys-organization-group-structure-gate.test.ts` pins the empty-scope
reading.
- Row 4 C's help text ("Meaningful only where the deployment runs
several organizations.") stays, and stays reachable: the fields still
show wherever `multiOrgEnabled` is on.
- Declaring the groups adds section headers to sys_organization's form
and record page; the field order is unchanged.
- `id`, `created_at` and `updated_at` keep their undeclared `group:
'System'` (the `field-group-undeclared` advisory fires for those three,
where before it fired for all twelve fields); declaring a System group
would surface `created_at` / `updated_at` on the record page, a change
the card did not ask for.
## Verification
Head `559cf504` (review follow-up on `24375ec4`; merges cleanly with
`origin/main` `2038c56e`), with spec and platform-objects rebuilt:
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 105 families on `559cf504`; all 105
exit 0, each code captured to a file before any pipe. `--ran`: 105
derived, 105 run, 0 NOT-MEASURED, 0 UNRUN. Its STALE TREE note names
only `scripts/pm/**` fleet tooling that moved on main, none on this
diff's surface.
- `pnpm lint`, `pnpm check:cli-command-ids`, `pnpm
check:adr-symbol-anchors`, `node scripts/check-i18n-bundles.mjs`: exit
0. `packages/spec` `check:generated` (incl. api-surface and
test-typecheck): exit 0.
- Suites (`vitest run` in each package): `@objectstack/spec` 908/908
files, 27374 passed + 1 todo; `@objectstack/lint` 195/195, 8235;
`@objectstack/platform-objects` 117/117, 1590.
- Ablation of the slot key: the other-slot exclusion in
`WithRequiresFeature` replaced with an inert key; `tsc -p
tsconfig.test.json` then reports both `@ts-expect-error` pins as unused
(TS2578); restored from the saved copy, marker count 0.
- Dogfood battery: n/a (no `!` in the title, no narrowing arm).
- Open PRs on the surface: #30760 edits
`packages/platform-objects/src/apps/translations/{en,es-ES,ja-JP,zh-CN}.ts`;
this PR touches the `*.objects.generated.ts` and
`*.metadata-forms.generated.ts` bundles beside them, not those files.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_016WV4cyCnzVPr9GuD38DdGt
<!-- signed: domain:server#5 · os-elon-musk ·
session_016WV4cyvF7SP65frVFAYLv7 -->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent ae82d05 commit 677d542
28 files changed
Lines changed: 630 additions & 48 deletions
File tree
- .changeset
- content/docs/references/data
- packages
- lint/src
- platform-objects/src
- apps/translations
- identity
- spec
- api-surface
- authorable-surface
- export-origins
- liveness
- state-counts
- src
- data
- kernel
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
320 | 320 | | |
321 | 321 | | |
322 | 322 | | |
| 323 | + | |
323 | 324 | | |
324 | 325 | | |
325 | 326 | | |
| |||
724 | 725 | | |
725 | 726 | | |
726 | 727 | | |
| 728 | + | |
727 | 729 | | |
728 | 730 | | |
729 | 731 | | |
730 | 732 | | |
731 | 733 | | |
| 734 | + | |
| 735 | + | |
| 736 | + | |
| 737 | + | |
| 738 | + | |
| 739 | + | |
| 740 | + | |
| 741 | + | |
| 742 | + | |
| 743 | + | |
| 744 | + | |
| 745 | + | |
| 746 | + | |
| 747 | + | |
| 748 | + | |
| 749 | + | |
732 | 750 | | |
733 | 751 | | |
734 | 752 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
30 | 30 | | |
31 | 31 | | |
32 | 32 | | |
| 33 | + | |
33 | 34 | | |
34 | 35 | | |
35 | 36 | | |
| |||
165 | 166 | | |
166 | 167 | | |
167 | 168 | | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4480 | 4480 | | |
4481 | 4481 | | |
4482 | 4482 | | |
| 4483 | + | |
| 4484 | + | |
| 4485 | + | |
| 4486 | + | |
| 4487 | + | |
4483 | 4488 | | |
4484 | 4489 | | |
4485 | 4490 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
135 | 135 | | |
136 | 136 | | |
137 | 137 | | |
138 | | - | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
139 | 144 | | |
140 | 145 | | |
141 | 146 | | |
| |||
1023 | 1028 | | |
1024 | 1029 | | |
1025 | 1030 | | |
| 1031 | + | |
| 1032 | + | |
| 1033 | + | |
| 1034 | + | |
| 1035 | + | |
| 1036 | + | |
| 1037 | + | |
| 1038 | + | |
| 1039 | + | |
| 1040 | + | |
| 1041 | + | |
| 1042 | + | |
| 1043 | + | |
| 1044 | + | |
| 1045 | + | |
| 1046 | + | |
| 1047 | + | |
| 1048 | + | |
| 1049 | + | |
| 1050 | + | |
| 1051 | + | |
| 1052 | + | |
| 1053 | + | |
| 1054 | + | |
| 1055 | + | |
| 1056 | + | |
| 1057 | + | |
| 1058 | + | |
| 1059 | + | |
| 1060 | + | |
| 1061 | + | |
| 1062 | + | |
| 1063 | + | |
| 1064 | + | |
| 1065 | + | |
| 1066 | + | |
| 1067 | + | |
1026 | 1068 | | |
1027 | 1069 | | |
1028 | 1070 | | |
| |||
1120 | 1162 | | |
1121 | 1163 | | |
1122 | 1164 | | |
1123 | | - | |
| 1165 | + | |
| 1166 | + | |
| 1167 | + | |
| 1168 | + | |
| 1169 | + | |
| 1170 | + | |
| 1171 | + | |
| 1172 | + | |
| 1173 | + | |
1124 | 1174 | | |
1125 | 1175 | | |
1126 | 1176 | | |
| |||
3830 | 3880 | | |
3831 | 3881 | | |
3832 | 3882 | | |
3833 | | - | |
| 3883 | + | |
| 3884 | + | |
| 3885 | + | |
| 3886 | + | |
3834 | 3887 | | |
3835 | 3888 | | |
3836 | | - | |
| 3889 | + | |
3837 | 3890 | | |
3838 | 3891 | | |
3839 | 3892 | | |
| |||
0 commit comments