Skip to content

GroupingConfigSchema.fields describes "(supports up to 3 levels)" but the gate is .min(1) with no upper bound — 50 levels parse green #7084

Description

@os-project-manager

Finding from the axis-① .describe() sweep (describe claims vs measured acceptance face — the #6762 class). Recorded unassigned; suggest domain:spec-surface for routing.

Anchor

packages/spec/src/ui/view.zod.ts, GroupingConfigSchema:

export const GroupingConfigSchema = lazySchema(() => strictObject({
  surface: 'this grouping configuration',
  history: VIEW_HISTORY,
}, {
  fields: z.array(GroupingFieldSchema).min(1).describe('Fields to group by (supports up to 3 levels)'),
}).describe('Record grouping configuration'));

Described claim

"Fields to group by (supports up to 3 levels)" — a fixed-count support envelope (house rule E17: any fixed count is a candidate by itself; counts drift).

Measured acceptance face

Probed on origin/main @ 2f3e79351 (tsx safeParse matrix, sources extracted via git archive; controls on both sides so a broken harness can't read as "no mismatch"):

control-reject 0 levels        : rejected [too_small]
control-reject unknown key     : rejected [unrecognized_keys]
 1 grouping levels -> ACCEPTED
 3 grouping levels -> ACCEPTED
 4 grouping levels -> ACCEPTED
 5 grouping levels -> ACCEPTED
10 grouping levels -> ACCEPTED
50 grouping levels -> ACCEPTED

The gate is .min(1) with no upper bound — the "3" exists only in the sentence.

Nothing downstream enforces it either, measured on the same checkout:

  • packages/lint/src has no grouping-level rule (the only grouping hits are validate-react-page-props.ts nested-key plumbing and org-axis prose).
  • The objectui consumer (objectui/packages/plugin-grid/src/useGroupedData.ts, buildLevel) recurses over all configured fields — if (depth >= fields.length) return [] is the only stop; no slice, no depth cap.

Why it matters for an authoring reader (ADR-0033)

The sentence is rendered verbatim in the published reference (content/docs/references/ui/view.mdx, the GroupingConfig.fields row). An authoring AI either trusts the cap (needlessly refusing a 4-level grouping a renderer demonstrably handles), or ignores it once it notices a 5-level config parses green — and then has no way to know whether level 4+ is contract or accident. Both readings are the #6762 shape: the describe states a support envelope the gate does not have.

The house already has the pattern for a real 3-cap one file over: ui/dataset.zod.ts include claims "max 3 hops" and enforces it.refine((p) => p.split('.').length <= 3, { message: 'include path exceeds the 3-hop limit (ADR-0071)' }). GroupingConfigSchema has the claim without the refine.

Suggested shape, if triage wants it fixed

Decide which side is true and make the other match:

  • if 3 is the intended contract, add .max(3) (an acceptance change — needs the usual pins), or
  • if multi-level is genuinely unbounded (what the renderer implements), drop the parenthetical or restate it as prose about typical use, and regenerate view.mdx.

Either way the fix is one describe string ± one bound, plus the regenerated reference page.

Refs

#6762 (class specimen: describe narrower than the measured gate), ADR-0071 (the enforced 3-hop precedent), ADR-0033 (AI-first authoring readers).

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions