test(app-shell): measure whether a { group } form section reaches SchemaForm (#8725) - #8739
Conversation
…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
✅ Console Performance Budget
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
Size Limits
|
|
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 ⭐ And the card predicts Three things I checked rather than took on trust.
The declared narrowing is acceptable because it is measured. The full
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- Generated by Claude Code |
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.tscreate/edit schemas, which do not authorgroup" — which would have made the defect latent.Sweep. Population: 4,231 tracked
.ts/.tsxfiles underapps/andpackages/(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 matchgit ls-files -- '*.ts', andgit grep -l "from 'react'"overpackages/ apps/ examples/lights 1,875 files. Against that non-empty population,git ls-filesmatches 0 files named*.form.tsand 0 files containing.form.at all. The premise names a file class this repository does not contain.What the inputs actually are. A
git grepfor the JSX opening tag ofSchemaFormover the whole tracked tree returns 13 production call sites; seven pass aformprop, and they take it from exactly two channels:RichMetadataTypeEntry.form(useMetadata.ts:93) is aRecordofstringtounknown— the/meta/typesregistry response, deserialised inuseMetadataTypeswith no validation and no normalisation — andResourceEditPage.tsx:2798/:2822hand it over asform={createMode && config.createSchema ? undefined : (entry?.form as any)}.EmbeddedItemEditor.tsx:76does the same withsubEntry?.form as any.getPageForm/getViewForm/getReportForm/getDashboardFormreadpageFormand friends straight out of@objectstack/spec/ui, an upstream versioned document this repo does not author either.So the REQUIRED
fieldsonFormSectionSpecstands between a TypeScript author and this renderer, and between nothing else. On the channel that actually reachesResourceEditPage, there is no compile-time barrier at all.Premise re-measured against the installed
@objectstack/spec17.3.0 (root export is thin, so via the/uisubpath):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/dashboardFormdeclares agroupsection or a section missingfieldstoday — a property of one spec release, not a barrier.The DOM reproduction, and one word the card has wrong
Reproduced in
happy-domthrough 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. ContainerinnerHTMLis 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:SchemaForm.tsx:786isfor (const f of s.fields), insideSchemaFormBody's pre-flight "does this layout name any field the schema has" loop — upstream ofSectionedSchemaForm, so the.mapsites at:1030and:1147are never reached. Guarding only the two.mapsites 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.fieldsand nothing here touches a read site.?? []at the read sites degrades a group-referencing section to an empty one — today's silent drop, made non-throwing. That is a behaviour ruling on a renderer.SchemaFormis not handed.@object-ui/plugin-formpublishesresolveSectionGroupReferencesfor exactly that (PR fix(console,plugin-form): render a form section that references a field group (#8641) #8727, still open and not merged at the time of writing), so that route is an import, never a new derivation — no assembly rule may be re-implemented on the objectui side (Consume the spec section group-reference form (objectstack#13855, ruled B) — pagerecord:detailssections and viewform.sectionsresolvegroup:keys viaderiveFieldGroupLayout#7051, objectstack#13855).One thing the card could not know, measured here: the trap is real but loud, not silent. Widening
fieldsto optional makes this package's owntype-checkreport, immediately:So part 1 cannot ship without its author confronting every read. What
strictNullCheckscannot 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.
tsc{ group }section is refused byFormSectionSpec(ts-expect-error, so it is two-way: wideningfieldsreds this file and puts the runtime pins in front of the author)tscRichMetadataTypeEntry['form']— the barrier guards one channel and not the reaching onefieldsstill renders themTypeErrorof thes.fieldsfamily and leaves no DOMfor…of, not a.mapT1 carries a positive control (
groupis a declared key, so T1's refusal is about the missingfieldsand not an unknowngroup).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-objectdiffering from the HEAD blob, a line-total gate), each restored by state (git checkout HEAD -- ABSOLUTE_PATH, thengit diff HEADempty and the blob hash back to the HEAD blob), each under atrap ... EXIT INT TERMwith absolute paths. Per-test classification read from vitest's JSON reporter, with file-levelfailureMessage(suite death) counted separately — it was 0 in every leg, so every result below is a failing assertion and not a suite death.:786onlyCannot 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.FormSectionSpec.fieldswidened to optionalTS2578: Unused 'ts-expect-error' directive), plus theTS18048inventory quoted above.RichMetadataTypeEntry.formretyped toFormViewSpecTS2322), 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 --listFilesreads 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 --listFilesreads 3,697 files: the new file is not among them, and no*.test.tsxis (control:SchemaForm.tsxis). The emitting program's inputs are byte-identical tomain, 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):
Declared narrowing. The full
@object-ui/app-shellsuite 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 importingSchemaFormorform-spec, 39 are underviews/metadata-admin/and the other two aresrc/index.ts(a re-export) andviews/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 underviews/metadata-admin/.The decision this PR is asking for
{ group }can reachSchemaForm, so parts 1 and 2 have to land together. Part 2's disposition is the ruling:?? []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.resolveSectionGroupReferencesfrom@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 — butSchemaFormis not handed an object definition today, so it needs a new input threaded fromResourceEditPage/ the inspectors.RichMetadataTypeEntry.form(leg E measured this as the only error in the package) and dropping theas anycasts. 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