Skip to content

finding: FormViewSchema.groups is declared as "alias to sections" but nothing folds it — every consumer reads only sections #6926

Description

@os-project-manager

Found while implementing #6601 (which narrows the public-form schema to the fields sections declares — that made me check what else can declare fields on a form). Filed, not fixed: the fix belongs at the producer (packages/spec), which is outside #6601's lane, and #6601's dispatch explicitly forbade touching packages/spec.

Graded observation-class (finding, no pm:queue): no shipped app authors groups today, so nothing a user hits right now. The hazard is authoring-time, and specifically AI-authoring-time.

What was measured

packages/spec/src/ui/view.zod.ts:1673-1674 declares both keys on FormViewSchema:

sections: z.array(FormSectionSchema).optional(), // For simple layout
groups: z.array(FormSectionSchema).optional(),   // Legacy support -> alias to sections

The comment promises an alias. Nothing performs the fold — FormViewSchema's .superRefine at :1821 iterates both arrays for the section.pane check (so the key is validated), and there is no .transform() that moves groups onto sections. Grepping the framework for a consumer of form.groups:

  • packages/rest/src/rest-server.ts — all three public-form routes read match.form?.sections only (schema narrowing at :7126, submit whitelist at :7236, lookup fieldCfg scan at :7333).
  • packages/runtime, packages/metadata-protocol — no .groups read on a form at all (the .groups hits there are navigation trees and book.groups, different surfaces).
  • packages/spec/src/system/i18n-resolver.ts:1802 is the one place that handles it, and only for translation (next.groups = form.groups.map(translateSection)), which is what makes the key look alive from a grep.

So a form authored with groups: parses clean, translates, and is then read by nobody.

Why it matters

This is the "accepted but ignored" shape this repo rejects on principle — the same argument that retired form.aria in the #3896 audit close-out ("an ACCESSIBILITY claim that is merely accepted is false compliance"). It is worse than a plain inert key because the inline comment actively tells an author the key works: an agent authoring metadata reads // Legacy support -> alias to sections, writes groups, gets a green parse, and ships a form that renders nothing.

On the public-form surface specifically it degrades into three separate silent failures at once, which is how I found it. For a groups-authored public form:

Suggested disposition

Contract-first: fix at the producer, not by teaching each consumer a second key to read. Two options, in the ADR-0049 enforce-or-remove frame:

  • Enforce — fold groups onto sections in a .transform() on FormViewSchema so the declared alias becomes true for every consumer at once (this is what aliases: already does for scalar renames elsewhere in the file; a whole-array alias just needs the transform). Cheapest way to make the comment honest.
  • Remove — retire groups via the ADR-0087 registry with a retiredKey message pointing at sections, the way form.aria went. Defensible if the liveness ledger says no authored metadata uses it, which is what my grep suggests.

I have no evidence of business pull for groups as an authoring spelling, so removal looks like the smaller surface — but the ledger check and the ADR-0087 conversion are the retirement playbook's, not mine to assert from a grep.

⛔ Whichever is chosen, do NOT fix this by adding ?? match.form?.groups in rest-server.ts. A lenient consumer is where AI-authored metadata errors hide, and it would leave the other consumers still blind.

Related: #6601 (found during), #6920 (the submit-side degeneration this compounds), #3896 (the form.aria retirement precedent), ADR-0049, ADR-0087.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions