Repository navigation
fix(plugin-detail,app-shell): a field group's visibleWhen gates its record:details section through the spec's { group } reference (objectui#11630) - #11641
Conversation
…ils section (objectui#11630)
deriveFieldGroupDetailSections now carries each group's visibleWhen verbatim
and unevaluated, and record:details evaluates it per record with the entry
form's own evaluator (resolveFieldRuleState + usePredicateScope), dropping a
section whose verdict is FALSE. An authored { group } reference inherits the
predicate the same way.
Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL
Co-authored-by: Claude <noreply@anthropic.com>
…he form's verdicts (objectui#11630) A parity matrix renders each predicate case through record:details and through the entry form's own section gate on the same row, and the synthesizer's carriage is pinned on both spellings. Adds the changeset. Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
…tail-group-visible-when Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
… (objectui#11630) Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…'s { group } reference (objectui#11630)
Patch round 1, route B (seat ruling on objectui#11630). The synthesized
record:details body writes each declared field group as the spec's own
{ group: KEY, columns } reference and the ungrouped bucket as bare field
names, so the node parses as RecordDetailsProps. The console's runtime
default page writes { group: KEY, showBorder: true }. record:details resolves
the reference and gates it with the group's visibleWhen, which now travels
beside the resolved section (deriveFieldGroupDetailEntries, package-internal)
rather than on any section; visibleWhen on an enumerated section is no longer
read. deriveFieldGroupDetailSections returns exactly what it returned before
this card.
Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL
Co-authored-by: Claude <noreply@anthropic.com>
…tail-group-visible-when Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
ℹ️ Console Performance Budget — not measuredThis run did not produce a console bundle to measure, so there is no pass/fail verdict for the performance budget. This is not a budget violation. Nothing was measured — the numbers a real violation would carry are simply absent.
See the workflow run for details. No package size report: it is only generated from a complete package build, so a partial one is never shown. |
…bjectui#11630) Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…d records (objectui#11630) Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Isolated review of PR objectui#11641 for card objectui#11630, read-only: the card body and its six comments (triage Check-runs on the head: 43 runs, 40 ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
|
The record's non-blocking items are carried by the seat:
Generated by Claude Code |
Fixes #11630
Clause-②: yes
What was wrong
An object's
fieldGroups[].visibleWhengated the group on the entry form but not on the record detail page.deriveFieldGroupDetailSectionsrebuilt each group as an enumerated section and dropped the predicate that the shared derivation (deriveFieldGroupLayout,@objectstack/spec) passes through, andrecord:detailsread none. So the "Pro details" section of the card's reproduction rendered, header included, on abasicrow.Route (patch round 1: route B, seat ruling on the card)
@objectstack/spec17.6.0RecordDetailsProps.sections[]is strict. It declaresgroup, the ADR-0085 §5 reference form that inherits a group's members and presentation,visibleWhenincluded. It declaresfieldsas bare names, and it refusesvisibleWhenon an enumerated section. So the fix writes pages in that vocabulary:packages/plugin-detail/src/synth/buildDefaultPageSchema.ts):resolveDetailSections, and through itbuildDefaultDetails/buildDefaultTabs/buildDefaultPageSchema, now emit each declared group as{ group: KEY, columns }. The trailing ungrouped section lists bare field names. The derivation sits behind one package-internal function,deriveFieldGroupDetailEntries. It returns each resolved section with the group'svisibleWhencarried BESIDE it, never on it. The publicderiveFieldGroupDetailSectionsreturns exactly what it returned before this card: resolved sections, no predicate.packages/plugin-detail/src/renderers/record-details.tsx):record:detailsresolves a{ group }reference against the object'sfieldGroupsand evaluates the group'svisibleWhenagainst the bound record. It drops the section, heading and members, when the verdict is FALSE. It does not readvisibleWhenoff an enumerated section, a key the spec refuses there.packages/app-shell/src/views/RecordDetailView.tsx): the console's default record page builds its own section list, so it now writes{ group: KEY, showBorder: true }for each declared group. Without that change the card's reproduction path stays ungated, because an enumerated copy cannot carry the predicate. Its ungrouped primary and "More details" sections are unchanged.record:detailsregistration'ssectionsinput description now says a{ group }reference inherits the group'svisibleWhen, and that one written on a section itself is not read.One evaluator, the form's
The form renderer gates a
section-dividerrow withresolveFieldRuleState(@object-ui/core) underusePredicateScope()(@object-ui/react).record:detailsmakes the same call with the same three inputs. Both modules are existing peer dependencies of@object-ui/plugin-detail: noplugin-formdependency and no copy of the evaluator.record: every declared field seeded tonull, then the defined values of the bound record, as the form'sruleRecordis built. A relation is collapsed to its stored id (toPredicateRecord), because the edit form reads its record without$expandand this page's record may arrive expanded.previous: the same row. An edit form bindspreviousto the persisted row, and the row this page shows is the persisted row.scope: the host predicate scope (current_user,features, ...).A predicate that cannot be evaluated SHOWS the section and warns once. That is the form's direction (
VISIBLE_WHEN_FAULTED, ADR-0137 D3), taken from the shared evaluator rather than restated.Consumers of the synthesized sections, and what each does now
RecordDetailViewbuildssynthParts.sectionsfromderiveFieldGroupDetailSections(objectDef)and passes them intobuildDefaultPageSchema{ group: KEY, showBorder: true }per group;RecordContextProvidergetsobjectSchema={objectDef}, so the reference resolvesRecordDetailView's earlybuildDefaultPageSchema(objectDef, slots)metadata-admin/anchors.tscreateSeedstoresbuildDefaultPageSchema(objectDef).regions{ group }references and parses asRecordDetailsPropsmetadata-admin/previews/PagePreview.tsxcallsbuildDefaultPageSchema(recordSchema, { slots })RecordContextProvidergetsobjectSchema={recordSchema}, so the references resolve{ group }referencerecord:detailsresolves it throughderiveFieldGroupDetailEntriesvisibleWhenderiveFieldGroupDetailSectionscallersPins that cover them:
RecordDetailView.sectionHeadingsRenderPath-6190(mountsRecordDetailView),anchors.page-seed,PagePreview,defaultFieldGroupsPage.sectionHeadings(real synthesized page through the real registry), and the two new files below.Published surface (Clause-②)
The emitted shape changes for grouped objects: BREAKING for code that reads the synthesized sections (the changeset is
minor, per AGENTS.md §9).buildDefaultPageSchema,buildDefaultTabs,buildDefaultDetailsandresolveDetailSectionsused to emit each group as an enumerated copy:name,label,icon,description,collapsible,defaultCollapsed,columns, andfieldsas rich{ name, label, type, ... }descriptors. They now emit{ group: KEY, columns }. The ungrouped section'sfieldsare bare names. An explicitsectionsoption is still returned unchanged.record:detailsgates a{ group }reference with the group'svisibleWhen.RecordDetailView(@object-ui/app-shell) writes{ group: KEY, showBorder: true }per group.inputsentry, prop or i18n key.deriveFieldGroupDetailEntriesis exported from its module for the renderer and is not re-exported from the package entry.Migration (also in the changeset): read a group's heading or members via
deriveFieldGroupDetailSections(def)instead of off a synthesized section. Replace a stored section copied from an older seed ({ name: 'pro', label: ..., fields: [...] }) with{ group: 'pro' }; only the reference form inherits the group'svisibleWhen.Spec legality, measured (installed
@objectstack/spec17.6.0,RecordDetailsProps.safeParse):buildDefaultDetails(def).propertiesfor a grouped objectrecord:detailsnode insidebuildDefaultPageSchema(def)visibleWhenunrecognized_keys @ sections.0 [visibleWhen]invalid_type @ sections.0.fields.0This also retires the round-0 finding that the synthesized grouped sections were refused at every field object.
Tests
Gates on
75c794c(route B, after mergingorigin/mainat0abd4f9), all under the shared verify lock, from the repo root:buildDefaultPageSchema.test.ts,defaultFieldGroupsPage.sectionHeadings,recordDetailsInputs.spec-parity,buildDefaultPageSchema.strictPayload):Test Files 6 passed (6),Tests 147 passed (147).pnpm exec vitest run packages/plugin-detail/:Test Files 238 passed | 1 skipped (239),Tests 2382 passed | 8 skipped (2390).pnpm exec vitest run packages/app-shell/:Test Files 1001 passed | 1 skipped (1002),Tests 9951 passed | 9 skipped (9960).turbo run build --filter='@object-ui/app-shell^...': exit 0.pnpm --filter @object-ui/plugin-detail type-check: exit 0.lint: exit 0 for both packages, 0 errors.pnpm --filter @object-ui/app-shell type-checkwas red on75c794c(TS2345:flatMapinferred its element type from the first return). Fixed in594b154andaeddcd7by annotating the element type as unknown-valued records, a type-only change.Rerun on final HEAD
aeddcd7under the lock: app-shell type-check exit 0; app-shell lint exit 0, 0 errors, with the warning count equal to75c794c; the three app-shell consumer pins (RecordDetailView.sectionHeadingsRenderPath-6190,anchors.page-seed,PagePreview):Tests 15 passed (15); the two new files:Tests 18 passed (18). The full plugin-detail and app-shell suites ran on75c794c, whose runtime codeaeddcd7does not change. Unlocked root checks on594b154, each exit 0:check:new-line-citations(0 new),check:control-bytes,check:changeset-claims,check:pending-changeset-literals,check:test-path-roots,check:phantom-deps,check:installed-pin-claims,check:esm-specifiers,check:spec-symbols,scripts/check-changeset-presence.mjs. Onaeddcd7,check:new-line-citations,check:control-bytes,check:changeset-claims,check:test-path-roots,check:phantom-depsand the changeset presence check were rerun, exit 0 each.renderers/__tests__/record-details.groupVisibleWhen-11630.test.tsxrenders every case twice on the same row: throughrecord:details, fed bybuildDefaultDetails(now{ group }references), and through the entry form renderer (@object-ui/componentsform), given the section-divider rowprojectSectionDivideremits for a group, as an edit form of that row. Each leg asserts the form's verdict, the detail page's verdict and the expected verdict on[basic, pro]:record.kind == 'pro'(the card)kind == 'pro'(unbound, so it faults)record.tier == 'gold', a declared field no row carriesrecord.owner == 'u2', a lookup that arrives expanded hereprevious.kind == 'pro''sales_manager' in current_user.positions, denied then allowedFurther legs:
{ group: 'pro' }reference: hidden, shown, on both spellings.{ group, showBorder }shape: hidden, shown.visibleWhen: shown, shown, because it is not read.Every render reads an ungated anchor first and throws
HARNESS DEADbefore any verdict.synth/__tests__/buildDefaultPageSchema.groupVisibleWhen-11630.test.tspins the emitted references, the absence ofvisibleWhenand field objects on every section the page writes, the two-control legality reading above, and the predicate travelling beside the resolved section.Ablation on route B (one-shot, on
75c794c)Each mutation went through
ablation-replace.mjs: an anchor that must hit once, a blob change verified on disk, and a restore proven by blob equal to HEAD with an emptygit diff HEAD. Afterwards the diff against HEAD was 0 bytes and the two new files passed again (18/18). The red direction was predicted before running.resolveSectionGroup{ group }buildDefaultDetails, redvisibleWhenoff an enumerated sectionpreviousto nothingpreviousleg rednullseedNo
HARNESS DEADor partial-group error appeared in any ablation run.NOT MEASURED: live before/after on a running backend. Reason: the shared objectstack checkout has no built CLI (
packages/cli/distis absent). A backend needs a fresh objectstack worktree plus a full app-showcase closure build, and that build goes through the shared verify lock, which stayed saturated this session. The console path is covered in two halves, not by a live run. TheRecordDetailViewmount pin shows that its references resolve and the group headings render. The runtime-default-page leg shows that the same{ group, showBorder }shape is gated.Acceptance notes
RecordDetailView's ungrouped primary and "More details" sections still carry rich field descriptors. That page is synthesized at runtime and never stored or parsed, and route B does not need them changed.deriveFieldGroupLayoutTSDoc calls the section gate fail-closed. The form, and now the detail page, fail open on a faulted predicate. This PR keeps parity with the form; the seat carries that question to the maintainer.Attribution: implemented in session
https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL.Generated by Claude Code