Skip to content

test(app-shell): measure whether a { group } form section reaches SchemaForm (#8725) - #8739

Merged
os-justin merged 2 commits into
mainfrom
claude/issue-8725-formsectionspec-group-reachability
Sep 9, 2026
Merged

test(app-shell): measure whether a { group } form section reaches SchemaForm (#8725)#8739
os-justin merged 2 commits into
mainfrom
claude/issue-8725-formsectionspec-group-reachability

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Part of #8725 — deliberately not Fixes. This PR delivers the card's first deliverable, the reachability measurement, and stops there. Neither half of the fix is here: the half that remains is the type widening plus a disposition for the three read sites, which is a behaviour ruling on a renderer. See "What this PR does not do".

Leg 1 — reachability: YES, and the card's guess about the inputs is false

The card was filed with reachability explicitly unmeasured and named that as its own gap. It guessed the inputs were "this repo's own *.form.ts create/edit schemas, which do not author group" — which would have made the defect latent.

Sweep. Population: 4,231 tracked .ts / .tsx files under apps/ and packages/ (git ls-files -- 'apps/**/*.ts' 'apps/**/*.tsx' 'packages/**/*.ts' 'packages/**/*.tsx' | wc -l). Lit control on the same command shape over the same population: 1,904 files match git ls-files -- '*.ts', and git grep -l "from 'react'" over packages/ apps/ examples/ lights 1,875 files. Against that non-empty population, git ls-files matches 0 files named *.form.ts and 0 files containing .form. at all. The premise names a file class this repository does not contain.

What the inputs actually are. A git grep for the JSX opening tag of SchemaForm over the whole tracked tree returns 13 production call sites; seven pass a form prop, and they take it from exactly two channels:

  1. A server document, untyped and uncast-checked. RichMetadataTypeEntry.form (useMetadata.ts:93) is a Record of string to unknown — the /meta/types registry response, deserialised in useMetadataTypes with no validation and no normalisation — and ResourceEditPage.tsx:2798 / :2822 hand it over as form={createMode && config.createSchema ? undefined : (entry?.form as any)}. EmbeddedItemEditor.tsx:76 does the same with subEntry?.form as any.
  2. Spec-bundled formsgetPageForm / getViewForm / getReportForm / getDashboardForm read pageForm and friends straight out of @objectstack/spec/ui, an upstream versioned document this repo does not author either.

So the REQUIRED fields on FormSectionSpec stands between a TypeScript author and this renderer, and between nothing else. On the channel that actually reaches ResourceEditPage, there is no compile-time barrier at all.

Premise re-measured against the installed @objectstack/spec 17.3.0 (root export is thin, so via the /ui subpath):

FormSectionSchema.safeParse({ group: 'contact_info' })  -> ACCEPT
   { collapsible: false, collapsed: false, columns: 1, group: 'contact_info' }
CONTROL FormSectionSchema.safeParse({ notAKey: 1 })     -> REJECT  ["unrecognized_keys"]

The control fires, so the accept is a real reading of a strict schema. Also measured on 17.3.0: none of pageForm / viewForm / reportForm / dashboardForm declares a group section or a section missing fields today — a property of one spec release, not a barrier.

The DOM reproduction, and one word the card has wrong

Reproduced in happy-dom through the same untyped channel. The render throws out of the component body; there is no partial tree and no per-section boundary could contain it. Container innerHTML is the empty string — the card's parent measurement (#8641) found a visible empty bordered card for its defect; this one leaves no DOM at all. A well-formed sibling section in the same document dies with it, and the tabbed arm dies identically.

The card predicts Cannot read properties of undefined (reading 'map'). That is not what fires. The observed throw is:

TypeError: s.fields is not iterable

SchemaForm.tsx:786 is for (const f of s.fields), inside SchemaFormBody's pre-flight "does this layout name any field the schema has" loop — upstream of SectionedSchemaForm, so the .map sites at :1030 and :1147 are never reached. Guarding only the two .map sites the card quotes leaves the crash exactly where it is. This is proved mechanically by ablation leg A below, not by reading.

What this PR does not do, and why

Nothing here widens FormSectionSpec.fields and nothing here touches a read site.

One thing the card could not know, measured here: the trap is real but loud, not silent. Widening fields to optional makes this package's own type-check report, immediately:

SchemaForm.tsx(786,23): error TS18048: 's.fields' is possibly 'undefined'.
SchemaForm.tsx(1030,20): error TS18048: 's.fields' is possibly 'undefined'.
SchemaForm.tsx(1147,9): error TS18048: 's.fields' is possibly 'undefined'.
form-spec.containers.test.tsx(117,60): error TS2537   (PIN D needs a NonNullable)
mergeServerFields.test.ts 69/82/128/160: error TS18048

So part 1 cannot ship without its author confronting every read. What strictNullChecks cannot do is stop them from typing ?? [] to silence it — which is precisely the ruling this card is holding open.

What landed

One new test file, packages/app-shell/src/views/metadata-admin/SchemaForm.groupSectionReachability-8725.test.tsx, plus an empty-frontmatter changeset. No source file, no type, no behaviour changes.

Its runtime pins assert what this renderer does today, which is to die. They are a measurement, not an endorsement: whoever rules on the disposition is expected to turn them into pins on the chosen behaviour, and their going red is the intended signal that the ruling landed.

pin kind asserts
T1 tsc the spec-legal { group } section is refused by FormSectionSpec (ts-expect-error, so it is two-way: widening fields reds this file and puts the runtime pins in front of the author)
T2 tsc the crashing shape is assignable to RichMetadataTypeEntry['form'] — the barrier guards one channel and not the reaching one
R1 vitest control / non-regression — a section that authors fields still renders them
R2 vitest the render throws a TypeError of the s.fields family and leaves no DOM
R3 vitest the read that fires first is the for…of, not a .map
R4 vitest one bad section destroys a well-formed sibling: the blast radius is the document
R5 vitest the tabbed arm dies at the same upstream read

T1 carries a positive control (group is a declared key, so T1's refusal is about the missing fields and not an unknown group).

Evidence

Every pin observed red. Five ablation legs, each run from a committed tree, each mutation line-preserving and proved on disk in both directions (anchor counts before and after, git hash-object differing from the HEAD blob, a line-total gate), each restored by state (git checkout HEAD -- ABSOLUTE_PATH, then git diff HEAD empty and the blob hash back to the HEAD blob), each under a trap ... EXIT INT TERM with absolute paths. Per-test classification read from vitest's JSON reporter, with file-level failureMessage (suite death) counted separately — it was 0 in every leg, so every result below is a failing assertion and not a suite death.

leg mutation result
A guard read site :786 only R3, R5 FAILED; R1, R2, R4 passed. Message becomes Cannot read properties of undefined (reading 'map') — the card's own quote, from :1030. Proves the ordering claim and that a two-site fix is a non-fix.
B guard all three read sites R2, R3, R4, R5 FAILED; R1 passed. The "fix landed" direction.
C caricature: every section resolves to no fields R1 FAILED, alone. The control is real.
D FormSectionSpec.fields widened to optional T1 FAILED (TS2578: Unused 'ts-expect-error' directive), plus the TS18048 inventory quoted above.
E RichMetadataTypeEntry.form retyped to FormViewSpec T2 FAILED (TS2322), and measured as the only error in the package — so T2 is a measurement, not a tautology.

Type-level pins are inside the checked program. tsc -p packages/app-shell/tsconfig.test.json --listFiles reads 4,474 files and the pin file is one of them (input #4,099). Legs D and E confirm behaviourally that the project judges it.

Non-regression, structurally. tsc -p packages/app-shell/tsconfig.json --noEmit --listFiles reads 3,697 files: the new file is not among them, and no *.test.tsx is (control: SchemaForm.tsx is). The emitting program's inputs are byte-identical to main, so nothing any form builds can have moved. R1 is the runtime half of the same claim.

Suites run (from the repo root with paths, per objectui#3378):

packages/app-shell/src/views/metadata-admin/   832 suites, 2559 tests, 0 failed, 0 suite deaths
packages/app-shell/src/views/studio-design/    168 suites,  323 tests, 0 failed, 0 suite deaths
pnpm --filter @object-ui/app-shell type-check  exit 0   (tsc --noEmit && tsc -p tsconfig.test.json)
eslint on the new file                          0 errors, 0 warnings
node scripts/check-control-bytes.mjs           exit 0   (6922 tracked text files)
node scripts/check-changeset-presence.mjs      exit 0   "Every one of them has an EMPTY frontmatter"
node scripts/check-changeset-no-major.mjs      exit 0

Declared narrowing. The full @object-ui/app-shell suite exceeds this container's foreground budget, so it was narrowed to the change's measured blast radius and the remainder is declared to CI. The radius is measured, not assumed: of 41 tracked files importing SchemaForm or form-spec, 39 are under views/metadata-admin/ and the other two are src/index.ts (a re-export) and views/studio-design/ObjectHooksPanel.tsx — both directories were run in full above.

Collisions

git ls-remote --heads origin 'gh-readonly-queue/*' at branch time listed PRs 8720, 8721, 8723, 8732, 8733. Diffing the queue tip against this branch's base gives 26 files; the intersection with this PR's two files is empty, and none of them is under views/metadata-admin/.

The decision this PR is asking for

{ group } can reach SchemaForm, so parts 1 and 2 have to land together. Part 2's disposition is the ruling:

  • A — ?? [] at the three read sites. Cheapest, no new dependency. It makes a group-referencing section render as an empty one: today's silent drop, no longer fatal. That is a behaviour ruling on a renderer, and it is what the compiler will tempt the next author into typing.
  • B — resolve the reference, importing resolveSectionGroupReferences from @object-ui/plugin-form (blocked on fix(console,plugin-form): render a form section that references a field group (#8641) #8727 landing). Correct, and the only route that does not re-implement an assembly rule here — but SchemaForm is not handed an object definition today, so it needs a new input threaded from ResourceEditPage / the inspectors.
  • C — put the barrier on the channel that actually reaches the renderer, by retyping RichMetadataTypeEntry.form (leg E measured this as the only error in the package) and dropping the as any casts. This does not fix the crash — a server document can still carry the shape at runtime — but it stops the type from claiming a guarantee it does not provide.

A and B are not exclusive with C.


Generated by Claude Code

…chemaForm (objectui#8725)

objectui#8725 was filed with reachability explicitly unmeasured. It is
reachable: the `form` document SchemaForm renders arrives from the
`/meta/types` registry as `RichMetadataTypeEntry.form`
(`Record<string, unknown>`, unvalidated) and is handed over as
`form={entry?.form as any}`, so the REQUIRED `fields` on `FormSectionSpec`
stands between a TypeScript author and this renderer and nothing else.

Pins the measurement without deciding either half of the fix: the render
throws a TypeError out of the component body and leaves no DOM, the whole
document dies with one bad section, and the read that fires first is
`SchemaFormBody`'s `for…of` pre-flight — not the `.map` sites the card
predicts, so guarding only those leaves the crash in place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
…nothing

Empty frontmatter — the explicit exemption for a test-only change under a
published package's `src/`, per scripts/check-changeset-presence.mjs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YBWFb5YgMU5dw8p2VKj16S
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3482.0 KB 3512.7 KB
Main entry chunk (gzip) 144.0 KB 350 KB
Entry file index-cErZLG2L.js
Status PASS

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 499.42KB 114.32KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.12KB 3.28KB
data-objectstack (index.js) 198.39KB 55.29KB
fields (index.js) 244.96KB 61.76KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.53KB 46.34KB
plugin-dashboard (index.js) 131.43KB 34.44KB
plugin-designer (index.js) 215.51KB 44.29KB
plugin-detail (index.js) 251.39KB 65.04KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 134.16KB 33.47KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 208.18KB 56.62KB
plugin-kanban (index.js) 55.44KB 15.73KB
plugin-list (index.js) 112.73KB 27.69KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.33KB 3.25KB
plugin-view (index.js) 84.54KB 20.84KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-justin
os-justin marked this pull request as ready for review September 9, 2026 02:27
@os-justin
os-justin enabled auto-merge September 9, 2026 02:27

Copy link
Copy Markdown
Collaborator Author

Contract review — accepted, flipped out of draft, auto-merge armed. Green on 30 of 33 with three shards still running; nothing failed.

This is the decision tree's prescribed output, not a half-finished fix: reachability came back YES, so the card's own tree routes to "report with the measurement and STOP", and what ships is seven pins and an empty-frontmatter changeset — no source, no type, no behaviour.

Two of the card's own premises were falsified, and the second one would have shipped a non-fix.

The card guessed the inputs were this repo's *.form.ts schemas; there are zero such files (0 of 1904 tracked .ts, 0 containing .form. at all) against lit controls on the same command shape. The real wide channel is a server documentRichMetadataTypeEntry.form, a Record<string, unknown> handed over as (entry?.form as any). So the required fields guards a TypeScript author and nothing else.

⭐ And the card predicts Cannot read properties of undefined (reading 'map'), while the observed throw is TypeError: s.fields is not iterable from SchemaForm.tsx:786's for-of pre-flight loop — upstream of both .map sites the card names. Leg A settles it by ablation rather than by reading: guard :786 alone and R3/R5 go red with the card's own quoted message then surfacing from :1030. A fix written to the card's text would have looked complete and left the crash in place.

Three things I checked rather than took on trust.

  1. The pin file is inside a checked program, and not merely listed. tsc -p packages/app-shell/tsconfig.test.json --listFiles reads 4474 files with the pin at fix(templates): move generator dependency ranges with the dependabot wave (#4098) #4099 — but that alone would be the "excluded tests" trap in reverse. Legs D and E prove it behaviourally: mutations produce TS2578 and TS2322 from that file.
  2. The non-regression argument is structural, which is stronger than a passing suite. tsc -p tsconfig.json --noEmit --listFiles reads 3697 files and the new test file is not among them, with SchemaForm.tsx as the control that is. The emitting program's inputs are byte-identical to main, so nothing any form builds can have moved — and R1 is the runtime half of the same claim. That is the right way to prove a test-only change is inert.
  3. Leg E's honesty. Retyping RichMetadataTypeEntry.form produced TS2322 as the only error in the package, which makes T2 a measurement rather than a tautology — and the comment in the file was corrected to say so instead of claiming the pin cannot fail. That correction is worth more than the pin.

The declared narrowing is acceptable because it is measured. The full app-shell suite hit the container's foreground budget (exit 124 — a budget stop, not a failure, and named as such), so the run was narrowed to the change's blast radius: of 41 tracked files importing SchemaForm or form-spec, 39 are under views/metadata-admin/ and the other two are a re-export and ObjectHooksPanel.tsx; both directories were run in full and the remainder declared to CI. Narrowed to a measured radius, not to a convenient one.

⚠️ Carried forward, and it is the thing most likely to be lost: these pins pin the crash. When the fix lands they go red and must be rewritten in the same commit, and what they protect — that this is a throw and not a silent drop — has to survive the rewrite. ⛔ Deleting them is not a resolution. Same shape as the #8071 pin on objectui#8730.

The ruling on the escalation is on objectui#8725: arm A fenced, arms B and C ruled in, parts 1 and 2 land together, and the unresolvable-group question is answered by consistency with plugin-form's resolver rather than by a second answer invented in SchemaForm. That card is blocked on #8727 (now in the merge queue) and gets re-dispatched as one unit when it lands.


Generated by Claude Code

@os-justin
os-justin added this pull request to the merge queue Sep 9, 2026
Merged via the queue into main with commit 32f1008 Sep 9, 2026
35 checks passed
@os-justin
os-justin deleted the claude/issue-8725-formsectionspec-group-reachability branch September 9, 2026 02:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants