Skip to content

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

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-yaj4v5
Nov 6, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-yaj4v5

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

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,Setup and tools: say which organization features need the enterprise edition, and remove fields that do nothing #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.
Ablations (one per changed behaviour)
  • 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.

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: feat(objectql,plugin-sharing): an organization authors its own assignment rules as sys_assignment_rule records #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.ai/code/session_016WV4cyCnzVPr9GuD38DdGt

…en; sys_organization hides group structure on one organization

Claude-Session: https://claude.ai/code/session_016WV4cyCnzVPr9GuD38DdGt
Co-authored-by: Claude <noreply@anthropic.com>
…; book it in the feature registry

Claude-Session: https://claude.ai/code/session_016WV4cyCnzVPr9GuD38DdGt
Co-authored-by: Claude <noreply@anthropic.com>
…dle; regenerate spec references; changeset

Claude-Session: https://claude.ai/code/session_016WV4cyCnzVPr9GuD38DdGt
Co-authored-by: Claude <noreply@anthropic.com>
…it on the object form, regenerate api-surface and export-origins

Claude-Session: https://claude.ai/code/session_016WV4cyCnzVPr9GuD38DdGt
Co-authored-by: Claude <noreply@anthropic.com>
…unt the new requiresFeature row

Claude-Session: https://claude.ai/code/session_016WV4cyCnzVPr9GuD38DdGt
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation protocol:data tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/lint, @objectstack/platform-objects, @objectstack/spec, touching 18 documentable anchor(s). ⚠️ 5 changed file(s) yielded no anchor (packages/spec/api-surface/kernel.json, packages/spec/authorable-surface/data.json, packages/spec/export-origins/kernel.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

30 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 2038c56ec99a60e2bf3cb6ee56ef70a5c368ed32.

⛔ 16 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 5 changed file(s) yielded no anchor (packages/spec/api-surface/kernel.json, packages/spec/authorable-surface/data.json, packages/spec/export-origins/kernel.json, …) — pages documenting those are invisible to this run
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 63 of 221 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 170 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 2038c56ec99a60e2bf3cb6ee56ef70a5c368ed32 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9e414459fa4556f71c88a815fc6e87c16caafbdd — the merge of head 559cf50443f54565dd625a39bc3aff0e6fb1a759 into base 2038c56ec99a60e2bf3cb6ee56ef70a5c368ed32, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9e414459fa4556f71c88a815fc6e87c16caafbdd && git checkout 9e414459fa4556f71c88a815fc6e87c16caafbdd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2038c56ec99a60e2bf3cb6ee56ef70a5c368ed32 559cf50443f54565dd625a39bc3aff0e6fb1a759 && git checkout -B drift-repro 2038c56ec99a60e2bf3cb6ee56ef70a5c368ed32 && git merge --no-ff 559cf50443f54565dd625a39bc3aff0e6fb1a759

node scripts/docs-audit/affected-docs.mjs --json 2038c56ec99a60e2bf3cb6ee56ef70a5c368ed32

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 2038c56ec99a60e2bf3cb6ee56ef70a5c368ed32 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 24375ec421c30b753b543f58a6e1727eca34dd2b
Local-runs: none
Scope: packages/spec/src/data/object.zod.ts, packages/spec/src/data/object.form.ts, packages/spec/src/kernel/public-auth-features.ts + packages/lint/src/validate-expressions.ts (the gate that enforces the rule, read as one change), packages/platform-objects/src/identity/sys-organization.object.ts, packages/platform-objects/src/feature-gate-guard.test.ts, packages/spec/src/ui/action.zod.ts and packages/spec/src/ui/action.form.ts (the precedent), packages/spec/src/data/field-group-layout.ts, packages/formula/src/cel-engine.ts (SCOPE_ROOTS), ui/packages/plugin-detail/src/renderers/record-details.tsx, ui/packages/components/src/renderers/form/form.tsx, ui/packages/react/src/hooks/useExpression.ts, ui/packages/app-shell/src/providers/ExpressionProvider.tsx, ui/packages/app-shell/src/console/AppContent.tsx, packages/plugins/plugin-auth/src/auth-manager.ts (the multiOrgEnabled producer), and the new pins in packages/spec, packages/lint and packages/platform-objects.

Inputs: card #30692 (body, triage 6671686007, claims 6675467698 / 6675514573 / 6675727155, dev report 6677017944), ruling 6670372892 on #30210 (row 4 A), PR #30810 body and file list (28 files, +533/-33), the net diff against merge-base 2d04cd2, and the check-runs on the head. Nothing was built, run or re-run locally.

① Derived judgments

  • New field-group key fieldGroups[].requiresFeature, z.enum(PUBLIC_AUTH_FEATURE_NAMES).optional() on ObjectFieldGroupSchema, the same vocabulary ActionSchema / ActionParamSchema use — RIGHT. A flag outside the registry is refused at parse with invalid_value at path requiresFeature (pinned in field-group-requires-feature.test.ts).
  • Lowered output: the .transform on strictObject({...}) that calls lowerRequiresFeature(group, ctx, 'visibleWhen') yields visibleWhen: { dialect: 'cel', source: 'features.multiOrgEnabled != false' } for a default-on flag and == true for an opt-in one (FEATURE_GATE_OPERATORS), and the key is stripped (Omit on the output type; 'requiresFeature' in out pinned false, also inside ObjectSchema.create) — RIGHT. The sugar is on the input/authorable side only: authorable-surface/data.json gains data/ObjectFieldGroup:requiresFeature, the docs table and the Studio form offer it, and no parsed output, z.infer type or consumer ever sees it. deriveFieldGroupLayout / readGroup read visibleWhen only. Idempotent: the parsed group carries no key, and its visibleWhen envelope re-parses unchanged, so a second parse (kernel registry, stored-row replay) cannot double-lower.
  • AND-combination with an authored visibleWhen, author first: EvaluatedExpressionInputSchema normalises a bare string to the { dialect: 'cel', source } envelope BEFORE the transform runs, so the string and the envelope arms reach the same existing.source and both produce (AUTHORED) && GATE — RIGHT (both pinned equal). The false literal arm of the helper cannot be reached on this slot (the field-group visibleWhen has no boolean arm), the non-CEL and blank-source arms are reachable through an envelope and now name visibleWhen in their messages — RIGHT.
  • lowerRequiresFeature third parameter slot: RequiresFeatureSlot = 'visible': both action call sites (action.zod.ts L617 param, L2523 action) pass no slot, so actions and params are byte-unchanged — RIGHT. RequiresFeatureSlot is a new exported type: api-surface/kernel.json and export-origins/kernel.json each gain the one row, naming the file — RIGHT.
  • PUBLIC_AUTH_FEATURES.multiOrgEnabled.gatedInputs gains sys_organization.fieldGroups.group_structure; the registry test's grammar admits OBJECT.fieldGroups.KEY; the platform-objects guard resolves that path to the group's visibleWhen and its reverse walk now books every features.* read in any group predicate — RIGHT, both directions.
  • Lint accept-set: a field group's root verdict is now judged on withoutLoweredFeatureGate(raw), which returns undefined when the whole source is exactly one registry flag's canonical gate, or the authored remainder when the source is ( + authored + ) && GATE and that remainder parses on its own; everything else is passed through untouched — RIGHT. features is a SCOPE_ROOTS member, so the follow-up check(where, raw, ...) on the WHOLE predicate does not bare-reference-refuse it. The finding keeps quoting the whole predicate (celSourceOf(raw) ?? verdict.source) and the features-root prescription on this slot now names requiresFeature: 'FLAG' — RIGHT.
  • Liveness object/fieldGroups.requiresFeature live with in-repo evidence, state counts 77 to 78 — RIGHT. object.form.ts adds one row on the fieldGroups repeater; the four *.metadata-forms.generated.ts bundles carry its label and help, and the two echo-decision population pins move 122 to 124 and 732 to 733 — RIGHT.
  • sys_organization: four groups declared in field order (identity, branding, configuration, group_structure), field group values retargeted from the former undeclared labels to the declared snake_case keys, group_structure carries requiresFeature: 'multiOrgEnabled'; the System trio stays undeclared and lands in the trailing ungrouped bucket (deriveFieldGroupLayout L176-208) — RIGHT, and the four _sections label blocks in en / zh-CN / ja-JP / es-ES match the keys.
  • Nothing in the diff reads ObjectFieldGroupSchema.shape or unwraps it, so turning it into a pipe breaks no reader; data/object.zod.ts importing kernel/public-auth-features follows the action file's own cycle-free pattern (the module is import-free).

② Semver level

.changeset/30692-field-group-requires-feature.md: @objectstack/spec minor, @objectstack/lint minor, @objectstack/platform-objects minor — MATCHES the diff. Spec publishes a new optional key, a new optional helper parameter and a new exported type (additive, no removal, no tightening of an existing accept-set). Lint widens an accept-set (behaviour change, additive). Platform-objects changes the organization form and record page layout (section headers appear, two fields hide where the flag is off), additive. The changeset's Clause-②: yes / Metadata-protocol: added lines are true of the diff: one new key on a published metadata schema. The Skills-docs: line is true: skills/objectstack-data/references/examples-objects.md has a Field Groups section that teaches fieldGroups and Field.group and names neither visibleWhen nor requiresFeature; keeping it out of this diff keeps the PR off the Tier H skills/** row. The PR body carries the same three lines.

③ Boundary flags

The dev report lists no open questions. Deviations, each judged:

  • public-auth-features.ts + tests widened into the surface — ACCEPTED. Reusing the one helper with a slot argument is the right shape (one lowering, not a twin), and the seat's re-posted claim 6675727155 names it.
  • feature-gate-guard.test.ts — ACCEPTED. A gatedInputs row that the guard cannot resolve would red the guard; the resolver and the reverse walk are the enforcement half of the new row.
  • object.form.ts — ACCEPTED. A key authors can write must be offered on the Studio form, and the authoring-surface pins would otherwise drift (the echo-decision counts are the proof).
  • Liveness ledger, generated api-surface / export-origins / references — ACCEPTED, these are the packages/spec regeneration AGENTS.md requires before pushing.
  • Translation bundles *.objects.generated.ts / *.metadata-forms.generated.ts / source hashes (not feat(objectql,plugin-sharing): an organization authors its own assignment rules as sys_assignment_rule records #30760's {locale}.ts) — ACCEPTED; section labels are keyed on the new group keys, and the disjoint file set keeps the serial constraint with feat(objectql,plugin-sharing): an organization authors its own assignment rules as sys_assignment_rule records #30760.
  • Two echo-decision population pins — ACCEPTED; count moves that follow one new authored row in all three locales.
  • Lint accepts ONLY the trailing gate the lowering emits; any other features read keeps its root verdict — HOLDS, and I tried to fool the peel: the removed text is always exactly one registry flag's canonical gate (the one read of features the renderers bind); the remainder is parsed on its own and judged in full by the same root verdict; the whole raw predicate still goes through check(). GATE && record.x (gate first), features.X == true on a default-on flag (wrong operator), (current_user.x) && GATE, (a) && (b) && GATE (remainder a) && (b does not parse so the raw is judged), ((a) && features.sso == true) && GATE (remainder reads features), (a || features.sso == true) && GATE, features.notAFlag, and features.ssoEnforced versus the sso tail (different suffixes) are all still refused. A hand-written predicate byte-identical to the lowered form is accepted, as the acceptance note says, because it IS the gate. The peel cannot be used to smuggle an unbound root past the verdict.
  • No ui/** change, per the ruling — HOLDS. record-details.tsx sectionPredicateVisible (L252) and the form's sectionGateVisible (form.tsx L110, call sites L2075 / L2169 / L3489) both evaluate a group's visibleWhen through evalFieldPredicate with usePredicateScope(), and ExpressionProvider builds that scope from its features prop, which AppContent.tsx (L1552) and RecordFormPage.tsx (L370) feed from /api/v1/auth/config. multiOrgEnabled is published on every deployment from the tenancy posture (auth-manager.ts L4655), so a single-organization deployment sees false and the group hides.
  • Skills follow-up "noted, not filed" — see should-fix 3.

④ Design judgment

packages/spec/src/data/object.zod.ts — HOLDS against the ruling (row 4 A: a field-group-level requiresFeature: 'multiOrgEnabled' lowered to the group's visibleWhen), against the precedent it cites (action.zod.ts L2523 / action.form.ts L182: the same helper, the same strip), and against its first consumers. Tried to break it: (a) a feature that is on at parse but off at render — the lowering is textual, nothing is evaluated at parse, the predicate is evaluated per render with the host's live features; correct. (b) Re-parse of a parsed object — no key survives, the envelope is stable; idempotent. (c) The strip leaving the key in an emitted surface — only input-side artifacts (authorable surface, docs table, Studio form) carry it, which is what authorable means; the parsed output type omits it. (d) A group whose fields are also placed elsewhere — membership is declared on the field and a field has one group, so the two organization fields cannot be in a second group; a custom form or page section that names parent_organization_id by field name rather than { group } is outside the group gate, as every group visibleWhen already is, and sys_organization declares no such layout; presentation only, both fields required: false, so a hidden group never blocks a save. (e) The System trio stays undeclared: field-group-undeclared is a warning and the layout buckets them last, so nothing is refused. No must-fix.

packages/spec/src/data/object.form.ts — HOLDS. One row on the fieldGroups repeater, help text that says what the parse does (lowered, both-hold composition, API unaffected), mirrored into the four locale bundles with the echo pins moved. Round-trip note, consistent with the action precedent: a stored object carries only the lowered visibleWhen, so a Studio re-open shows the gate under Visible When and an empty Requires Feature; the action form documents the same trait at L182, and the help text here says the key is lowered. Not a finding.

packages/spec/src/kernel/public-auth-features.ts — HOLDS. The slot argument defaults to visible, every refusal is re-worded with the slot, the registry row and the grammar extension are enforced by the guard in both directions. Two should-fixes below, neither a contract break.

Findings:

  • should-fix 1 (public-auth-features.ts): lowerRequiresFeature's slot is not tied to T — T extends WithRequiresFeature still only knows visible, and the read is (rest as WithRequiresFeature & { visibleWhen?: ... })[slot], so a future caller passing 'visibleWhen' on a shape that has only visible compiles and writes the wrong slot silently. Make the slot part of the generic constraint (a shape keyed on the slot) so the compiler refuses the mismatch.
  • should-fix 2 (object.zod.ts docblock / public-auth-features.ts semantics note): the default-on comment promises that a missing flag "keeps the input visible". That holds on an action's visible (fail-soft) and not on this slot: a group predicate is fail-closed, and the console's features is {} until /api/v1/auth/config resolves (AppContent.tsx L24-L30), so a multi-organization deployment hides Group Structure for the pre-fetch window and any host that publishes no features hides it for good. The PR's acceptance note names the no-features host, not the pre-fetch window. State the fault direction of this slot beside the semantics table, and pin it, so the next flag gated on a group is chosen knowing it.
  • should-fix 3 (process): the Skills-docs: follow-up is "noted, not filed" in the dev report. Prime Directive chore: version packages #10 lets an acceptance note carry it, but an owed skills line with no card is the kind that evaporates; the seat files the skills card before this lands or carries it on Edit organization: hide Parent Organization and Sort Order on a single-organization deployment with a field-group requiresFeature #30692 at close.

No must-fix. The hits files hold against the cited code, the ruling they implement and their first consumers.

Check-runs on the head at the time of this read: no gate-carrying run red; Lint · ESLint, Lint · scoped · tooling self-tests, Lint · PM dispatch-gates, Check Changeset, Check PR Size, PR-body guards, Spec property liveness, Governed Surface Queue Guard, Check Documentation Links, Flag docs affected, Dogfood Regression Gate (aggregate) are success; the Test Core shards, Test UI shards, Build Core, Build UI Deps, Live E2E UI, Type Check (workspace / consumer gates / UI / debt ledger / source gates), and the remaining Lint jobs were still in_progress. This record judges the diff; the seat's ACCEPT reads the finished conclusions.

Implemented-by: claude/project-thread-yaj4v5
Reviewed-by: session_016WV4cyvF7SP65frVFAYLv7

VERDICT: PASS

…s; state and pin a field group's fail-closed fault direction

The slot argument is now a type parameter, and the input's constraint is
keyed on it: a shape may carry that slot and must not carry the other one.
Lowering an action into 'visibleWhen', or a field group into the default
'visible', fails to compile, and the cast on the slot read is gone. The
action call sites are unchanged.

A field group's visibleWhen is fail-closed, where an action's visible is
fail-soft. A features scope that does not carry the flag yet makes the
gate fault (CEL "No such key"), so the group stays hidden until the host
publishes the flag, even for a default-on flag. The semantics table, the
gate docs and the object comment say so, and the sys_organization pin
holds the empty-scope reading.

Claude-Session: https://claude.ai/code/session_016WV4cyCnzVPr9GuD38DdGt
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 559cf50443f54565dd625a39bc3aff0e6fb1a759
Local-runs: none
Scope: packages/spec/src/data/object.zod.ts, packages/spec/src/data/object.form.ts, packages/spec/src/kernel/public-auth-features.ts + packages/lint/src/validate-expressions.ts, packages/platform-objects/src/identity/sys-organization.object.ts, packages/spec/src/ui/action.zod.ts (both lowerRequiresFeature call sites and the action shape's keys), packages/spec/tsconfig.test.json and packages/spec/package.json (the program that compiles the new compile-time pins), ui/packages/app-shell/src/console/AppContent.tsx (the features scope's lifecycle), and the two amended pins in packages/spec and packages/platform-objects.

Delta record: one commit 559cf50 on top of 24375ec (6 files, +105/-23), judged against the tree. The PASS of record 6677172123 on 24375ec is carried forward for everything this commit did not touch: object.form.ts, validate-expressions.ts and its tests, the feature-gate guard, the generated surfaces, the translation bundles and the echo pins are byte-identical between the two heads. Nothing was built, run or re-run locally.

① Derived judgments

  • lowerRequiresFeature is now generic on the slot: T extends WithRequiresFeature(S), S extends RequiresFeatureSlot = 'visible', where WithRequiresFeature(S) is { requiresFeature? } intersected with { [K in S]?: SlotPredicate } and { [K in Exclude(RequiresFeatureSlot, S)]?: never } — RIGHT. An action or param (which carries visible) lowered into 'visibleWhen', or a field group (which carries visibleWhen) lowered into the default 'visible', no longer type-checks; the two @ts-expect-error pins in field-group-requires-feature.test.ts hold exactly those two cases, and they are compiled: packages/spec typecheck runs check:test-typecheck --project tsconfig.test.json, which reaches src/**/*.test.ts (the build tsconfig excludes tests; the sibling exists for this), so the pins are not phantom checks.
  • The read is a widening assignment, const slotted: { [K in S]?: SlotPredicate } = input, then slotted[slot] — no cast, as claimed. The value default slot: S = 'visible' as S does carry an assertion; it is sound in the only shape that reaches it, because S is 'visible' whenever the argument is omitted and no type argument is written. A caller spelling the type argument by hand and omitting the value would lower into visible — a TypeScript limitation, not reachable from either schema, noted and not a finding.
  • The two action call sites (action.zod.ts L617 param, L2523 action) are byte-unchanged and still satisfy the 'visible' instantiation: neither the param nor the action output carries a top-level visibleWhen key — visibleWhen appears in that file only as a per-option key inside a param's options (L418) and as an alias row that resolves to visible at parse (L1067), so the never half of the constraint is not met by any real key. The field-group output has visibleWhen and no visible — RIGHT on both sides.
  • RequiresFeatureSlot keeps its name and union; its public surface rows in api-surface / export-origins are unchanged — RIGHT. SlotPredicate and WithRequiresFeature(S) stay module-private.
  • Fault direction stated where it is decided: the PublicAuthFeatureSemantics table now says "keeps the input visible" is the slot's fault direction, not the gate's; featureGatePredicate's docblock scopes "fails open" to a fail-soft slot; RequiresFeatureSlot names both directions; the field-group requiresFeature docblock, the sys_organization comment and the changeset each say the group is hidden while features does not carry the flag and appears once it is published — RIGHT, and consistent with the slot's standing contract (ADR-0085 §5 fail-closed; FIELD_RULE_SLOT_CONSEQUENCE in lint says the same).
  • The platform-objects pin features not yet published: the gate faults and the group is hidden evaluates the canonical gate with extra: { features: {} }, expects ok === false, and expects the layout without the two fields — RIGHT; it is the empty-scope reading the console's useState({}) produces (AppContent.tsx L24-L30), so the pin measures the real shape.
  • The changeset text and the PR body's Skills-docs: line now read follow-up #30812; skills(objectstack-data): teach a field group's visibleWhen and requiresFeature in examples-objects.md #30812 exists, open, titled for examples-objects.md's Field Groups section, filed by the seat with the Skills-docs line as its gate — RIGHT.

② Semver level

Unchanged: @objectstack/spec minor, @objectstack/lint minor, @objectstack/platform-objects minor. The delta narrows a generic constraint on an exported helper (lowerRequiresFeature): a hypothetical external caller passing a shape with both slots, or the wrong slot, would now fail to compile — that is the refusal the first record asked for, on a helper whose only callers are the two spec schemas, and it stays inside minor. Clause-②: yes / Metadata-protocol: added remain true; Skills-docs: follow-up #30812 is true of the board.

③ Boundary flags

  • Dev claim (1), the slot keyed on the shape — HOLDS against the tree (see ①). The pins are real; the call sites are byte-unchanged.
  • Dev claim (2), fail-closed on an empty features scope — HOLDS and is the question put to this record: is "hidden for the session if /auth/config fails" acceptable under ruling 6670372892, or a must-fix? ACCEPTABLE, not a must-fix. The ruling chose the field-group visibleWhen as the mechanism, and that slot's fault direction is a standing contract (ADR-0085 §5: "FALSE or a faulting predicate — fail-closed — hides the WHOLE group") that this PR did not set and may not quietly reverse; the blast radius is two presentation-only, required: false fields on one form and one record page, readable and writable through the API, on a session whose auth config fetch has already failed (a state in which the console's login-surface flags and create_organization gating are equally unknown); the window on a healthy session is the pre-fetch gap, after which the group appears; and the direction is now stated at the semantics table, the predicate, the slot type, the key, the object and the changeset, and pinned. Seeding the console's features with the registry's defaults so a default-on flag resolves before the fetch would close the gap for every surface at once — that is ui/**, which the claim fences off and the ruling says needs no change; it is a candidate card for the ui lane, not a finding on this PR.
  • No new files, no governed path, no ui/** touched — HOLDS.

④ Design judgment

packages/spec/src/kernel/public-auth-features.ts — HOLDS. The constraint refuses the mismatch at compile time instead of writing into a key the schema never reads; the read is cast-free; the semantics prose now tells the truth about both slots. Tried to break it: an action shape with the per-option visibleWhen (L418) — that key lives on an option object inside options, not on the param, so the never half is not met; the alias row (L1067) is consumed by strictObject before the transform, so no visibleWhen reaches the output. No finding.

packages/spec/src/data/object.zod.ts — HOLDS. Docblock only; the key, the transform and the output shape are unchanged from 24375ec. No finding.

packages/spec/src/data/object.form.ts — untouched by the delta; PASS carried forward.

No must-fix, no should-fix on this delta. Record 6677172123's should-fix 1 and 2 are resolved by this commit; should-fix 3 is carried by #30812.

Check-runs on head 559cf50 at the time of this read: none red; Governed Surface Queue Guard, Check Changeset, Check PR Size, PR-body guards, Spec property liveness, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, Check Documentation Links, Flag docs affected, Dogfood Regression Gate (aggregate) are success; Build Core, Build UI Deps, Build Docs, Live E2E UI, Plan Test Core shards, the Type Check jobs (workspace / consumer gates / source gates / debt ledger) and the remaining Lint jobs were still in_progress, and the Test Core / Test UI shards had not yet been planned. This record judges the diff; the seat's ACCEPT reads the finished conclusions.

Implemented-by: claude/project-thread-yaj4v5
Reviewed-by: session_016WV4cyvF7SP65frVFAYLv7

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cyvF7SP65frVFAYLv7

席位核对 · ACCEPT

PR #30810 · head 559cf504 · 2026-11-06T05:51Z · card #30692 under ruling 6670372892 on #30210 (row 4 A) · claim re-posted 6675727155 (widened surface) · os-dev report 6677017944 (head 24375ec4) and the fix-round note on 559cf504 · contract review PASS 6677172123 (24375ec4) and delta PASS 6678016867 (559cf504)

维护者速读(席位校正)

The body's six lines stand as written. One consequence is added, because the delta made it explicit: the Group Structure group is fail-closed. On a multi-organization deployment it stays hidden in the console until /api/v1/auth/config has answered, and for the whole session if that request fails. Actions keep their fail-soft gate. The two fields stay readable and writable over the API either way.

席位核对

  • Ruling fit: a field-group requiresFeature from the action key's PUBLIC_AUTH_FEATURES vocabulary, lowered at parse into the group's visibleWhen and stripped from the output; packages/lint accepts exactly the lowered trailing gate, and any other features read in a group predicate keeps its root verdict; sys_organization declares four fieldGroups in field order, with group_structure (Parent Organization, Sort Order) on requiresFeature: 'multiOrgEnabled'. No ui change: both renderers already evaluate group predicates through usePredicateScope (the reviewer verified it). Row 4 C's help text (PR fix(platform-objects): Parent Organization and Sort Order say they matter only with several organizations #30686) is kept and still shows wherever multiOrgEnabled is on.
  • Contract review: CONTRACT_REVIEW_TIER, by an agent other than the drafter. PASS with three should-fixes on 24375ec4; the seat adopted 1 (the slot keyed on the shape's type) and 2 (the group slot's fault direction, documented and pinned) into this PR; 3 is skills(objectstack-data): teach a field group's visibleWhen and requiresFeature in examples-objects.md #30812. The delta PASS on 559cf504 has no finding.
  • Accepted deviations: the surface widened with the seat's re-posted claim: public-auth-features.ts and its tests, feature-gate-guard.test.ts, object.form.ts, the spec's regenerated ledgers and references, the platform-objects *.objects.generated.ts / *.metadata-forms.generated.ts bundles (not feat(objectql,plugin-sharing): an organization authors its own assignment rules as sys_assignment_rule records #30760's {locale}.ts files), and two echo-decision count pins.
  • Not covered, named for later: seeding the console's features with the registry defaults would close the pre-fetch gap on every gated surface. That is ui/**, outside this ruling, and a candidate card for the ui lane.
  • Declarations: Fixes #30692, Clause-②: yes, Metadata-protocol: added, Skills-docs: follow-up #30812; minor changesets on @objectstack/spec, @objectstack/lint and @objectstack/platform-objects, consistent with an added key.
  • Gates (report, 559cf504): 105 derived, 105 exit 0, --ran 105 / 0 NOT-MEASURED / 0 UNRUN; pnpm lint, check:cli-command-ids, check:adr-symbol-anchors, check-i18n and spec check:generated 0. Suites: spec 908 files, lint 195, platform-objects 117. Two ablations (the spec lowering through dist, the lint peel) went red and were restored.
  • Landing: check-governed-merges --pr 30810: NOT governed, +630/-48. The seat lands with pr_ready + automerge_enable once every check on 559cf504 is green or expected-skip.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 6, 2026 06:10
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 6, 2026 06:10
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 6, 2026
Merged via the queue into main with commit 677d542 Nov 6, 2026
70 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-yaj4v5 branch November 6, 2026 06:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/l tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Edit organization: hide Parent Organization and Sort Order on a single-organization deployment with a field-group requiresFeature

1 participant