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

Draft
objectstack-fleet[bot] wants to merge 6 commits into
mainfrom
claude/project-thread-yaj4v5
Draft

objectstack-fleet[bot] wants to merge 6 commits into
mainfrom
claude/project-thread-yaj4v5

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Fixes #30692

Clause-②: yes
Metadata-protocol: added
Skills-docs: follow-up owed — 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 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), packages/platform-objects/src/identity/sys-organization-group-structure-gate.test.ts (off hides, on shows, 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.
  • The group is hidden fail-closed wherever a host does not publish features (any section predicate that faults hides its group). The console publishes it on both surfaces; a host without it would hide Group Structure on a multi-organization deployment as well.
  • 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 24375ec4 (branch merged with origin/main 2d04cd2b after #30753 landed; clean merge, no conflict), after a whole-tree turbo run build (exit 0):

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 105 families on 24375ec4; all 105 exit 0. --ran: 105 derived, 105 run, 0 NOT-MEASURED, 0 UNRUN.
  • pnpm lint, pnpm check:cli-command-ids, pnpm check:adr-symbol-anchors: exit 0. Local-only check:dev-prereqs, check:skill-frame-freshness: exit 0. node scripts/check-i18n-bundles.mjs: in sync.
  • Re-run on the merged head: @objectstack/spec 818/818 files, 25834 tests; @objectstack/lint 195/195, 8235; @objectstack/platform-objects 117/117, 1589.
  • 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 15 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 100303c0430eb644506970270a0134db304b7cbb.

⛔ 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
  • 2 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 100303c0430eb644506970270a0134db304b7cbb → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2766081a635df0940d339bfa43378180379935fd — the merge of head 24375ec421c30b753b543f58a6e1727eca34dd2b into base 100303c0430eb644506970270a0134db304b7cbb, 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 2766081a635df0940d339bfa43378180379935fd && git checkout 2766081a635df0940d339bfa43378180379935fd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 100303c0430eb644506970270a0134db304b7cbb 24375ec421c30b753b543f58a6e1727eca34dd2b && git checkout -B drift-repro 100303c0430eb644506970270a0134db304b7cbb && git merge --no-ff 24375ec421c30b753b543f58a6e1727eca34dd2b

node scripts/docs-audit/affected-docs.mjs --json 100303c0430eb644506970270a0134db304b7cbb

⚠️ 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 100303c0430eb644506970270a0134db304b7cbb → 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

This branch has not been deployed

No deployments
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