Found while implementing objectui#7051 (the form.sections[].group reference form on @object-ui/plugin-form). Filed rather than fixed there: it is a different package, a different renderer, and a different code path, so it is out of that card's scope.
What was measured
@objectstack/spec 17.3.0 declares group on FormSectionSchema — a section may point at one of the object's declared fieldGroups instead of enumerating fields (objectstack#13855, ADR-0085 §5). Measured against the installed package on origin/main:
FormSectionSchema.safeParse({ group: 'contact_info' })
-> ACCEPT { collapsible: false, collapsed: false, columns: 1, group: 'contact_info' }
apps/console/src/components/FormPage.tsx has its own section builder, independent of @object-ui/plugin-form:
export function buildSections(form: FormViewSpec, objectSchema: ObjectSchemaPayload | null): RenderableSection[] {
const sections = form.sections ?? form.groups ?? [];
...
for (const entry of sec.fields ?? []) { ... } // a { group } section has no `fields`
...
return { label: sec.label, ..., fields }; // a { group } section has no `label` either
}
So a spec-legal { group: 'contact_info' } section reaches this builder and produces a section with no label and no fields — it renders nothing, reports nothing, and the members the group declares are simply absent from the console's form page. This is the same silent-drop class objectui#7051 fixed on the plugin-form chain, on the third consumer.
⚠️ How this was established, stated honestly: by reading the source (sec.fields ?? [] cannot produce rows for a section that carries no fields), not by a DOM measurement of the console page. Whoever takes this should reproduce it first — that is cheap here and the reproduction is the acceptance evidence.
Why it is not the same fix
FormPage is not a plugin-form container: it does not go through ObjectForm's dispatcher (where objectui#7051 resolves the reference, once, above the routing fork), and the resolver packages/plugin-form/src/sectionGroups.ts is package-internal — it is not exported from @object-ui/plugin-form's public surface. So the options are a deliberate choice, not a mechanical copy:
- A. Export the resolver from
@object-ui/plugin-form and call it from buildSections — one assembler, one behaviour, at the cost of a new published export (and check-readme-exports follows an export).
- B. Have
buildSections call deriveFieldGroupLayout (ADR-0085 §5) itself. Still the single upstream assembler, no new published surface, but a second adapter onto a section shape.
- C. Leave it, and record the divergence deliberately —
FormPage is a reference-app renderer, not the published one.
⛔ Whatever is chosen, no assembly rule (declared order, empty-group drop, ungrouped trailing bucket, collapse/visibleWhen passthrough) may be re-implemented on the objectui side — that is objectui#7051's and objectstack#13855's standing constraint.
Non-regression axis for whoever takes it
⭐ Not "the form renders" and not "the section is non-empty" — both pass on a builder that drops every group-referenced section, which is today's behaviour. The discriminating assertion is that a group-referenced section renders the members that group declares, asserted against concrete field identifiers, with a second section referencing a different group so a constant-resolution caricature cannot satisfy both.
Refs
Generated by Claude Code
Found while implementing objectui#7051 (the
form.sections[].groupreference form on@object-ui/plugin-form). Filed rather than fixed there: it is a different package, a different renderer, and a different code path, so it is out of that card's scope.What was measured
@objectstack/spec17.3.0 declaresgrouponFormSectionSchema— a section may point at one of the object's declaredfieldGroupsinstead of enumeratingfields(objectstack#13855, ADR-0085 §5). Measured against the installed package onorigin/main:apps/console/src/components/FormPage.tsxhas its own section builder, independent of@object-ui/plugin-form:So a spec-legal
{ group: 'contact_info' }section reaches this builder and produces a section with no label and no fields — it renders nothing, reports nothing, and the members the group declares are simply absent from the console's form page. This is the same silent-drop class objectui#7051 fixed on theplugin-formchain, on the third consumer.sec.fields ?? []cannot produce rows for a section that carries nofields), not by a DOM measurement of the console page. Whoever takes this should reproduce it first — that is cheap here and the reproduction is the acceptance evidence.Why it is not the same fix
FormPageis not aplugin-formcontainer: it does not go throughObjectForm's dispatcher (where objectui#7051 resolves the reference, once, above the routing fork), and the resolverpackages/plugin-form/src/sectionGroups.tsis package-internal — it is not exported from@object-ui/plugin-form's public surface. So the options are a deliberate choice, not a mechanical copy:@object-ui/plugin-formand call it frombuildSections— one assembler, one behaviour, at the cost of a new published export (andcheck-readme-exportsfollows an export).buildSectionscallderiveFieldGroupLayout(ADR-0085 §5) itself. Still the single upstream assembler, no new published surface, but a second adapter onto a section shape.FormPageis a reference-app renderer, not the published one.⛔ Whatever is chosen, no assembly rule (declared order, empty-group drop, ungrouped trailing bucket, collapse/
visibleWhenpassthrough) may be re-implemented on the objectui side — that is objectui#7051's and objectstack#13855's standing constraint.Non-regression axis for whoever takes it
⭐ Not "the form renders" and not "the section is non-empty" — both pass on a builder that drops every group-referenced section, which is today's behaviour. The discriminating assertion is that a group-referenced section renders the members that group declares, asserted against concrete field identifiers, with a second section referencing a different group so a constant-resolution caricature cannot satisfy both.
Refs
plugin-formhalf (this finding's parent measurement)sections[].group, bound a section's blast radius, and resolve enumerated field labels through the declaredlabel#8584 — therecord:detailshalfGenerated by Claude Code