Uh oh!
There was an error while loading. Please reload this page.
fix(plugin-form): declare object-master-detail-form formType as a closed, measured vocabulary - #6177
Merged
Conversation
…sed vocabulary `object-master-detail-form` declared `formType` as a bare `string` while the sibling `object-form` declared the same key as an `enum` of six, and both funnel into the renderer that switches on those six names. A value outside the vocabulary matched no branch and fell through to the flat field list with no diagnostic. The declared set is measured, not copied from the sibling: `simple | tabbed`. The other four are reachable branches that break the master-detail composition. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019b5UBNMtTzKbVtZZGvFuxe
Contributor
✅ 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
|
yinlianghui-tw
marked this pull request as ready for review
August 24, 2026 22:58
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #5939
Part of, notFixes. The card asks two questions and this PR answers one: the declaration is tightened, and the renderer fall-through remedy is reported for a ruling rather than implemented (§4). Merging this should not close the card until that ruling lands.object-master-detail-formdeclaredformTypeas a barestringwhile the siblingobject-formdeclared the same key as anenumof six. Both funnel intoObjectForm, which switches on those six names, so a value outside them matched no branch and fell through to the flat field list with no diagnostic.The declared set here is
simple | tabbed— measured against the master-detail composition, not copied from the sibling.Verified on
e8f171366(final commit; every gate reading below is from that tree).1. The per-value measurement (step 1 of the dispatch order)
Mounted
object-master-detail-formwith two parent sections (Sec One→ref,Sec Two→memo) and one detail collection, then drove the master-detail's own bottom Save bar. Counts are calls on one stubdataSource.simpleObjectForm.tsx:1134→SimpleObjectFormsections pathbatchTransaction×1,create×0tabbedObjectForm.tsx:236→TabbedFormbatchTransaction×0,create×1wizardObjectForm.tsx:260→WizardFormmemoabsent)Step 2 of 2; nothing persistedsplitObjectForm.tsx:287→SplitFormbatchTransaction×0,create×1drawerObjectForm.tsx:316→DrawerFormmodalObjectForm.tsx:346→ModalForm'wizzard'(out-of-vocabulary control)Two independent statements already in the repo agree with the measured two, which is what the enum is derived against rather than a second guess:
MasterDetailFormSchema.formType?: 'simple' | 'tabbed'(MasterDetailForm.tsx:93) — the type contract for the same key.formType === 'tabbed' ? 'tabbed' : 'simple'(ObjectForm.tsx:215) — the coercionObjectFormapplies when it routes asubformsschema into this block. Declaring fewer than two would have made the renderer itself emit an out-of-vocabulary value.Declaring all six would have minted a declared-but-inert vocabulary — choices an authoring UI offers and this composition cannot honour. The table is pinned as regression coverage in
packages/plugin-form/src/masterDetailFormTypeVocabulary.test.tsx, so a value that starts or stops being honoured turns it red instead of drifting.2. ⭐ The green-because-it-never-ran result, reproduced — then turned into a real reading
This is the card's whole argument, so it is measured in both directions rather than asserted.
Ablation —
apps/console/src/__tests__/public-block-binding-reach.test.tsxrestored to its pre-#3840 generic sampling by deleting the two by-name samples (sections,formType), so the probe once again handsobject-master-detail-formthe generic'x'for astringinput. The mutation was confirmed on disk, per anchor, before either leg ran:No rebuild leg is required and none was run: the root
vitest.config.mtsaliases every@object-ui/*specifier topackages/*/src, so the mutation reaches the module under test directly — there is nodist/in the resolution path to go stale. The declaration leg was moved withgit checkoutagainst committed state in both directions, and the whole script carriedtrap restore EXIT INT TERM.LEG A — before the fix (declaration reverted to
type: 'string'; on-disk proof0 enum / 1 string):object-master-detail-formpassed — green, while carrying the identical latent crash. It never reached the section loop.LEG B — after the fix (declaration
enum: ['simple','tabbed']; on-disk proof1 enum / 0 string):and the two failures are now the same reading. Quoted with the block name in backticks where the runner prints it wrapped in angle brackets, so the body sanitizer cannot eat the identifier:
The declaration is what decided whether the probe exercised the block or routed around it. Restoration was verified afterwards: probe anchors back to
1/1, declaration back to1 enum / 0 string,git status --porcelainempty.3.⚠️ What catches an out-of-vocabulary value now — and what still does not
Caught.
manifestFromConfigsserializesinputsinto the manifest the JSX-page compiler and the save gate validate against, andsdui-parser'scheckTyperaisesinvalid-enumat severityerrorwhen a prop's only declared arm is anenum(packages/sdui-parser/src/validate.ts:198). While the key was a barestring, every string passed. Pinned inapps/console/src/__tests__/masterDetailFormTypeManifest.test.tsfor'wizzard','x','wizard'and'drawer', with'simple'/'tabbed'asserted clean and a reachability guard ahead of both (that guard earned its place: the manifest key isplugin-form:object-master-detail-form, and the first draft asserted against the bare name, which would have made every "no diagnostic" claim vacuous).Not caught, stated plainly. Per #5155's standing maintainer ruling, rejection lives at the zod/publish boundary, and that boundary is
@objectstack/spec's, not this repo's:That is asserted as a test in the same file rather than left as prose, so this PR cannot be read as "the hole is closed". Filed as the cross-repo half with the full measurement in it: objectstack#11873. The spec is not touched here.
4. The renderer fall-through — reported, not implemented
The card's second remedy (reject/report an unknown
formTypeinstead of falling through) is not in this PR, per the dispatch order. The census it asked for, over every tracked file (git ls-files, sonode_modulesand build output are out by construction), excluding this PR's own four files:wizardmodaltabbedsimpledrawersplit98 authored literal values, every one of them inside the six-name vocabulary. Nothing in the repo — fixtures, examples, catalog entries, tests — authors an out-of-vocabulary
formTypetoday. So the blast radius of a reject-or-report change is zero in this repo; it is not zero for already-published apps, which is the part only the PM can rule on.One measurement that qualifies the vocabulary, reported rather than quietly shipped:
submitHandler— the seamMasterDetailFormhands the parent form so the save routes throughbatchTransaction— is read in exactly one place,ObjectForm.tsx:820insideSimpleObjectForm.TabbedFormandSplitFormcalldataSource.createdirectly instead. Sotabbed, which this PR declares, is presentationally honoured but escapes the atomic batch: the parent commits on its own and a failing child leg orphans it. Droppingtabbedfrom the enum was rejected as the wrong lever — it would contradict both in-repo statements of the vocabulary and makeObjectForm.tsx:215emit an undeclared value. Filed as #6176 instead, with the measurement.5. Gates
Each quotes its own verdict line; the exit code was captured before any pipe (
cmd > log 2>&1; EXIT=$?), never off atail. Root vitest only, never package-scoped (#3378). Every heavy run went through the container's shared verify lock.0Test Files 9 passed (9)·Tests 179 passed (179)packages/plugin-form/(whole package path)0Test Files 60 passed (60)·Tests 613 passed (613)turbo run type-check --filter=@object-ui/plugin-form --filter=@object-ui/console0Tasks: 37 successful, 37 totalturbo run lint(same two packages)0Tasks: 3 successful, 3 total—0 errorsin both (637/203pre-existingno-explicit-anywarnings)check:control-bytes0check-control-bytes: OK (scanned 5102 tracked text file(s); skipped 85 binary).check:phantom-deps0Every in-scope import is declared by the package that publishes it.check:self-import0No package names itself inside its own src/.check:i18n-keys0Every in-scope call-site key resolves against the en pack (2820 keys)…lint:coverage0lint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).type-check:coverage0type-check coverage: 45/46 via type-check, 0 via their own build, 0 known-brokenchangeset:check0All workspace packages are in the changeset fixed group.·No changeset declares a major bump.check-changeset-presence01 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/master-detail-formtype-vocabulary.md.Declared narrowing.
lintwas run per-package (plugin-form,console) rather than as a whole-repoeslint ., and the narrowing is a measurement rather than a skip: the population is the repo's own package boundary (not a hand-picked file list), both packages that this diff touches ran in full, andeslint.config.jsenables no type-aware linting — noproject, noprojectService, norecommendedTypeChecked— so this diff cannot move the verdict on any file it does not contain. CI runs the whole farm regardless.Control bytes: the four changed files were also scanned outside the gate with
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'— no matches.Files
packages/plugin-form/src/index.tsx— the declaration,type: 'string'→type: 'enum', enum: ['simple','tabbed'], with the exclusions recorded next to it.packages/plugin-form/src/masterDetailFormTypeVocabulary.test.tsx— the vocabulary and the measurement behind it.apps/console/src/__tests__/masterDetailFormTypeManifest.test.ts— what the tightening buys at the manifest/save-gate layer, and the publish-boundary gap it does not close..changeset/master-detail-formtype-vocabulary.mdRelated: #3840 (where this surfaced, and the probe that read green) · #3838 · #5155 (why the enum is not by itself a rejection) · #6158 (the declared-but-inert trap this measurement exists to avoid) · #6176 (filed here) · objectstack#11873 (the spec half).
Generated by Claude Code
Generated by Claude Code