Repository navigation
finding: FormViewSchema.groups is declared as "alias to sections" but nothing folds it — every consumer reads only sections #6926
Description
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 sectionsThe 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 readmatch.form?.sectionsonly (schema narrowing at :7126, submit whitelist at :7236, lookupfieldCfgscan at :7333).packages/runtime,packages/metadata-protocol— no.groupsread on a form at all (the.groupshits there are navigation trees andbook.groups, different surfaces).packages/spec/src/system/i18n-resolver.ts:1802is 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:
GET /forms/:slugpublishes an empty schema after finding: a section-less public form publishes EVERY field of its target object to anonymous callers #6601 (before finding: a section-less public form publishes EVERY field of its target object to anonymous callers #6601 it published the whole object, i.e. it hit exactly the bug finding: a section-less public form publishes EVERY field of its target object to anonymous callers #6601 fixes);POST /forms/:slug/submithas an emptyallowedFieldsand therefore accepts every non-anchor key (see finding: a section-less public form accepts ANY non-anchor field from an anonymous visitor (the write-side twin of #6601) #6920);GET /forms/:slug/lookup/:fieldfinds nofieldCfgand answers403 LOOKUP_NOT_PUBLICfor every field.
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
groupsontosectionsin a.transform()onFormViewSchemaso the declared alias becomes true for every consumer at once (this is whataliases: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
groupsvia the ADR-0087 registry with aretiredKeymessage pointing atsections, the wayform.ariawent. 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.
Activity
claude commentedon Aug 9, 2026
Findings round: route only + hold —
domain:specadded,findinggrade unchanged, no ownership taken.groupsontosectionsvia a transform, or retiregroupsvia ADR-0087) lands inpackages/spec/src/ui/view.zod.tsand CHANGES what accepted metadata means (a transform alters parsed output; a retirement alters acceptance). Behavior-changing ⇒ red line ⇒domain:spec, not spec-surface (meta: bind Zod schemas for webhook / connector / sharing_rule WITHOUT registering the kinds — close the unvalidatedPUT /metawrite hole (#2657 audit, option A) #6245/feat(spec): action param 的 options[] 声明逐选项 visibleWhen (#5016) #6235 precedent).origin/main@ee58392:view.zod.tsstill declares both keys with the "Legacy support" alias comment ongroups, no transform folds it, and all three public-form reads inpackages/rest/src/rest-server.ts(:7110/:7237/:7339) walksectionsonly.groups(the card's own reachability analysis) — dormant, authoring-time hazard only. Restart conditions: (a) any authoredgroupsusage or a first user report of a blank form, or (b) the next spec-seat batch touchingFormViewSchema— fold the ruling in then, with the liveness-ledger check the retirement playbook requires.sections) and finding: a section-less public form accepts ANY non-anchor field from an anonymous visitor (the write-side twin of #6601) #6920 (submit-side degeneration) are adjacent consumers, not overlaps;POST /data/sharing/rules绕过SharingRuleSchema:criteria缺失或拼错静默变成"共享该对象全部记录",与 ADR-0049 "never seeded as a permissive match-all" 直接冲突 #3896 is theform.ariaretirement precedent the card cites for the remove route.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-project-manager commentedon Aug 9, 2026
Triage (maintainer-authorized one-off pass, 2026-08-09, registered on #6015): promoted
finding→pm:queue(domain:speckept), with the standing-policy direction: retire the alias per ADR-0049 —groupsis self-labelled "Legacy support", has zero in-repo producers and zero consumers, and a declared-but-unfolded alias is precisely the AI-authoring trap the filing names (validated, then silently ignored). The protocol-17 conversion should foldgroupsintosectionsrather than strip, so any in-the-wild author migrates losslessly; tombstone prescription per the #6856 ruling. If implementation discovers real externalgroupsauthors beyond what the filing measured, STOP and escalate instead of shipping the removal.Generated by Claude Code
os-zhuang commentedon Aug 9, 2026
CLAIM — PM seat
domain:spec(sessionsession_01PiRUoQkTSBBmpyXBY3cVn2), dispatching to an os-dev subagent.claude/issue-6926-retire-form-groups-alias../objectstack-issue-6926(per-task, per Prime Directive Migrate documentation site to Fumadocs with monorepo structure and shared content #11)groupsalias per ADR-0049, protocol-17 conversion foldsgroupsintosections(lossless), tombstone per the [finding][spec] Tombstones over STRIP-only conversions sayos migrate meta --from 16will "rewrite it" — the corpus is split between two verbs for one behaviour #6856 house sentence, and STOP-and-escalate if real externalgroupsauthors are discovered beyond what the filing measured. That ruling is quoted verbatim in the dispatch brief as 裁决.packages/spec/src/ui/view.zod.ts(FormViewSchema,:1673anchor — re-locate on current post-fix(spec,rest,cli): validation diagnostics reach the real defect — named view-union branches +invalid_key/invalid_elementdescent (#7025) #7042 main),packages/spec/src/conversions/registry.ts(+fixtures), retirement registries (RETIRED_KEYS_BY_MAJOR[17],DEFAULT_CHANGES_BY_MAJOR[17]inpackages/spec/scripts/lib/default-changes.ts, ADR-0087 D3),packages/spec/src/system/i18n-resolver.tsgroupstranslate branch, regenerated artifacts.view.zod.ts(ViewFilterRuleSchema, ~:398 area). Textual overlap is unlikely; expect a merge lap at landing, and generated artifacts resolve via the serial-relay os-regen protocol — do not pre-resolve them.Generated by Claude Code
os-zhuang commentedon Aug 9, 2026
STOP-and-escalate: the tripwire fired —
groupshas real consumers, and the fold already exists (inobjectui)Dev seat for the retirement dispatch. No PR opened. The ruling's stop condition is met, so I am reporting instead of shipping.
The promotion ruling authorized retirement on this factual basis (quoted verbatim):
The "zero consumers" half is disproven. The filing's grep covered
packages/rest,packages/runtime,packages/metadata-protocol— framework only. It never sweptobjectui, which is where the fold lives.Measured on current
origin/main(60be96377) +objectui@7b3e048The retirement playbook's required liveness-ledger check fails immediately — the framework's own ledger already records this key as live, with named evidence:
packages/spec/liveness/view.json→.props.form.children.groups:{ "status": "live", "note": "objectui: legacy alias of sections, normalized at ObjectForm.tsx:90 and spec-bridge form-view.ts:169 (spec.sections ?? spec.groups). Audit L24 drift resolved by alias-at-one-boundary." }Verified independently against real objectui source — five consumer sites, two of which perform the actual fold:
packages/react/src/spec-bridge/bridges/form-view.ts:169const sections = (spec.sections ?? spec.groups ?? []).map(mapSection);— the foldpackages/plugin-form/src/ObjectForm.tsx:129-142title→label,defaultCollapsed→collapsedpackages/plugin-view/src/ObjectView.tsx:875groups: schema.form?.groups— forwarded deliberatelyapps/console/src/components/FormPage.tsx:143const sections = form.sections ?? form.groups ?? [];apps/console/src/pages/developer/PublicFormsPage.tsx:112spec?.groupsused as a form-detection signalSite 1's own comment states the contract, and site 2's cites the fix that established it:
Decisive detail: objectui's
FormViewSpecinterface (form-view.ts:54-80) is a key-for-key structural mirror ofFormViewSchemaand declaresThe renderer read the spec's inline comment as a contract and implemented it. The comment is not a lie; it is honored one boundary downstream.
What this means
groups:parses clean, translates, and is then read by nobody" — is false. Agroups-authored form renders correctly through the primary ObjectUI path.groupswould break those five sites and silently break in-the-wild forms that render correctly today. The conversion would migrate sources, but nothing migrates a stored row already rendering fine.The real defect, restated (narrower, and still worth fixing)
The filing did find something true — just not "dead key". The alias is honored at one boundary and not the other:
groups→ renders correctly.packages/rest/src/rest-server.ts:7298 / :7348 / :7426 / :7570 readsectionsonly (anchors re-measured; the filing's :7110/:7237/:7339 moved post-fix(spec,rest,cli): validation diagnostics reach the real defect — named view-union branches +invalid_key/invalid_elementdescent (#7025) #7042).So a
groups-authored public form renders in the console but degrades on/forms/:slug,/forms/:slug/submitand/forms/:slug/lookup/:field— the three degradations the filing describes. That is an inconsistency between two consumers, not an inert key. Confirmed: the framework performs no groups→sections fold anywhere (grep forsections ?? …groupsreturns nothing outside objectui).cloud: swept, zero form-groupshits. In-repogroupsauthors: onlypackages/spec/src/ui/view.test.ts:526,640, both pinning current behavior.Recommendation (not mine to decide — direction was ruled, and the ruling's basis has changed)
Re-triage toward Enforce, i.e. the filing's option 1: add the
.transform()toFormViewSchemaso the fold happens at the producer, making the declared alias true for every consumer at once — REST included — and letting objectui's five boundary normalizations retire naturally as dead code later. That is contract-first, fixes the REST degradation at its root, and keeps the AI-authoring trap closed without breaking working renders. It does not need the ADR-0087 retirement kit.Retirement remains defensible as a later step, but only after the alias is honored uniformly and the objectui reads are removed — and it would need a coordinated cross-repo lap, not this one-PR dispatch.
⛔ Restating the filing's own guardrail, which still binds either way: do not fix this with
?? match.form?.groupsinrest-server.ts.Generated by Claude Code
os-zhuang commentedon Aug 9, 2026
PM status: STOP-and-escalate fired —
pm:dispatched→needs-user-decision. No code was shipped; the dispatched dev tore down cleanly.The retirement ruling's tripwire ("If implementation discovers real external
groupsauthors … STOP and escalate") triggered on its factual basis, not on scope: the "zero consumers" premise is disproven by measurement — see the dev's evidence comment above (five objectui consumer sites, two performing the actual fold:spec-bridge/bridges/form-view.ts:169,plugin-form/ObjectForm.tsx:129-142;packages/spec/liveness/view.jsonalready records the keylivewith objectui evidence; objectui#2545 shipped the fold precisely because a groups-only spec once rendered nothing). Retiring would break working renders and in-the-wild stored forms.The surviving real defect is narrower: the alias is honored at the objectui boundary but NOT on the framework REST public-form path (
rest-server.tsreadssectionsonly at :7298/:7348/:7426/:7570 on current main) — an inconsistency between consumers, not an inert key.Awaiting maintainer re-ruling. Options on the table (dev's report, attached above):
.transform()onFormViewSchemafoldinggroups→sections, making the declared alias true for every consumer at once (REST included); objectui's boundary folds become dead code and retire in a later lap. Dev recommends A; the PM seat concurs.Also flagged either way: the liveness-ledger note for
groupsshould be refreshed (anchor driftObjectForm.tsx:90→:129; the note omits that the framework REST path does not fold — the omission that let the filing read a live key as dead).Generated by Claude Code
os-zhuang commentedon Aug 9, 2026
MAINTAINER RE-RULING (2026-08-09, chat) — recorded by the
domain:specPM seat, verbatim and untranslated:Option A as escalated (the ruling's referent, from the STOP-and-escalate report above): Enforce — add a
.transform()onFormViewSchemafoldinggroupsontosectionsat the PRODUCER, so the declared alias becomes true for every consumer at once, REST included. objectui's five boundary normalizations become dead code and retire naturally in a later, separate lap. No ADR-0087 retirement kit needed.This supersedes the 2026-08-09 triage direction (retire per ADR-0049), which was correct on the facts it had; the facts changed (five measured objectui consumers, ledger
live).needs-user-decision→pm:dispatched.RE-CLAIM — PM seat (session
session_01PiRUoQkTSBBmpyXBY3cVn2), dispatching to a fresh os-dev subagent.claude/issue-6926-fold-groups-into-sections(the earlier retirement-direction branch was never pushed — the prior dev tore down with zero code)../objectstack-issue-6926(per-task, PD Migrate documentation site to Fumadocs with monorepo structure and shared content #11)spec.sections ?? spec.groupsprecedence is the reference) + measured verdict on whether the REST public-form path actually runs the schema parse (if it reads stored rows un-parsed, the fold's placement must follow the measurement — report, don't improvise) + liveness-ledger note refresh (open question 2 of the escalation, rec A) +i18n-resolvergroupsbranch disposition by measurement.view.zod.ts; serial landing, merge lap expected, generated artifacts via os-regen relay.Generated by Claude Code
os-zhuang commentedon Aug 9, 2026
PM bookkeeping:⚠️ Unlike the sibling cancellations, this one left a partial branch:
pm:dispatched→pm:queue. The A-direction os-dev was stopped externally (mass session-level cancellation, 15:21Z patrol census).claude/issue-6926-fold-groups-into-sections@3a69119(pushed ~13:45Z, one lap). The next dev should inspect that branch first — reuse it if it holds real fold work, or discard and restart from main; do NOT assume it is complete. The maintainer's A-direction re-ruling (producer.transform()fold) stands unchanged. Claim released; re-dispatch awaits the maintainer's word.Generated by Claude Code
os-zhuang commentedon Aug 9, 2026
RE-CLAIM — PM seat (session
session_01PiRUoQkTSBBmpyXBY3cVn2), fresh os-dev dispatched under the maintainer's re-acceleration (2026-08-09 chat). Branchclaude/issue-6926-fold-groups-into-sections(existing partial3a69119— inspect first, reuse or reset per the 15:26Z bookkeeping note), worktree../objectstack-issue-6926. The A-direction re-ruling and the 13:11Z re-claim scope remain binding.Generated by Claude Code
fix(spec): fold FormViewSchema.groups into sections at the producer (o…
chore(spec): re-classify the field.conditionalRequired liveness entry…
test(spec): the last ui/ file group's test titles state each cited de…