Uh oh!
There was an error while loading. Please reload this page.
fix(app-shell): an emptied picklist option Label stays a legal document (Q2) - #7536
Conversation
`ObjectFieldInspector`'s option writer guarded the key on truthiness, so an
author who cleared an option's Label box got an option serialised with no
`label` key at all. Measured on `@objectstack/spec` 17.2.0,
`SelectOptionSchema` ACCEPTS `{ value: 'alpha', label: '' }` and REJECTS
`{ value: 'alpha' }` with `invalid_type` at `[label]` — so the guard was
taking a document the platform accepts and rewriting it into one it refuses,
and the save came back 422.
Emit the value the author holds instead, empty string included. Nothing is
invented: an emptied label stays empty rather than falling back to `value`.
The `?? ''` arm also covers an option that arrived without a usable label
(`readOptions` maps a missing or non-string stored `label` to `undefined`) —
no legal document omits the key, and `''` is what the Label input has been
showing for that option all along.
The pin ends each case at `SelectOptionSchema` / `FieldSchema` rather than
just asserting the key is present: the point is that the contract accepts
what the designer emits.
Part of #7014
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC✅ 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
|
os-project-manager
commented
Sep 3, 2026
在席复核:通过 —— 已剥 draft + auto-merge SQUASH
Clause-② 按内容核过的(不采信报告)
|
Uh oh!
There was an error while loading. Please reload this page.
Part of #7014 — Q2 only. Q1 (the Tier-1 named-type convergence) is a separate unit, dispatched separately; Q3/Q4 live on #7513. Nothing here touches either.
The defect
ObjectFieldInspector's option writer guarded the key on truthiness:So an author who cleared an option's Label box got an option serialised with no
labelkey at all, and the save came back 422 with nothing on screen explaining why.The two spec readings, re-measured on this branch
@objectstack/spec17.2.0,SelectOptionSchema(packages/app-shellalready depends on it):{ value: 'alpha', label: 'Alpha' }{ value: 'alpha', label: '' }{ value: 'alpha' }invalid_typeat[label]{ value: 'alpha', label: undefined }invalid_typeat[label]{ value: 'alpha', label: 'Alpha', bogus: 1 }unrecognized_keys— strictness controlThe same pairs hold one level up on
FieldSchema(REJECTlands at[options.0.label]). Both controls matter: the ACCEPT rows are not a schema waving everything through, and the REJECT rows are attributable tolabeland nothing else.⇒ An empty label is a document the platform accepts. The guard was taking a legal document and rewriting it into an illegal one — the guard itself was the defect.
{ value: 'a', label: 'A' }REJECTstoo_smallat[value](a select option's identifier needs 2+ characters), so every row read "REJECT" for a reason that had nothing to do withlabel. Re-measured withvalue: 'alpha'until the control lit.The fix
Emit the value the author actually holds, empty string included. The author-facing surface is now exactly as wide as the contract instead of narrower, and nothing is invented — an emptied label stays empty rather than falling back to the option's
value.Value census at the call site (what
o.labelcan actually be):o.labelstring,''includedadd(), and the seeded trailing row)''''labelis missing or non-stringundefined(readOptionsmaps both to it)''Non-string never reaches the writer:
readOptionscoerces it toundefinedand the DOM input only ever yields a string. Rows with an emptyvalueare filtered out before the writer.The
undefinedarm was the one boundary worth deciding rather than assuming. It emits''because there is no legal document that omits the key (rows 3-4 of the table above; carryinglabel: undefinedis refused identically, so "keep the key, drop the value" is not a way out), and because''is exactly what the Label input has been displaying for such an option all along —value={o.label ?? ''}, the same collapse, one component up. So this emits what the author sees rather than inventing content.??and not||:||is the same truthiness bug spelled shorter.The pin
ObjectFieldInspector.optionLabel.test.tsx— 5 cases, each ending at the contract, not at the key's presence. Asserting only "there is alabelkey" would keep passing against a fix that emittedlabel: undefinedor fell back tovalue; every behavioural case therefore runsSelectOptionSchema/FieldSchemaover what the designer emitted, plus one case pinning the schema readings themselves so a future green cannot be a vacuous one.Verification
Union run on the final commit
b641c3dbc(a shared box — four agents, so the lock's wall-clock figures are not idle-box numbers):pnpm exec vitest runon the pin — 5 passed.ObjectFieldInspector, plus the twoobject-fields-iosuites its write path goes through and the console's spec-validity sample suite — 12 files, 158 tests, all passed. A writer that now always emits the key could have moved an existing expectation; none did.if (o.label) out.label = o.label;. Predicted red set, written before the run: the two behavioural label cases and the no-usable-label case red; the schema-readings case and the non-empty-label round-trip green (the old guard keeps a truthy label, and the schema case never touches the component). Observed: 3 failed | 2 passed, exactly that set —expected false to be true(key absent),expected [ 'invalid_type@[label]' ] to deeply equal [], andexpected [ { value: 'alpha' }, …(1) ] to deeply equal [ { value: 'alpha', label: '' }, …(1) ].Mutation proved on disk both ways by anchor counts (fixed 1 to 0, guard 0 to 1) plus the changed blob hash; restore proved by state —
git diff HEADempty, worktree blob8ac8dc22…equal to the HEAD blob, anchors back to 1/0 — never by an exit code. The script carries atrap … EXIT INT TERMrestore with absolute paths.tsc --noEmit(package project) andtsc -p tsconfig.test.json— both exit 0, after building the dependency closure (pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build, exit 0), since the test project resolves workspace deps through their built typings.*.test.tsx, so its green says nothing about the pin.--listFileson the test project: the pin file is 1 of 4539 program inputs, and the source under test is in there too — the pin really is compiled.check:control-bytes0,check:vi-mock-specifiers0,check:vi-mock-inherit0,check:designer-field-key-parity0,check:spec-symbols0 (2 declared deliberate copies, 19 unbacked claims— the count this branch inherited; this change adds no alignment claim),node scripts/check-changeset-presence.mjs0.eslinton both changed files: 0 errors, 0 warnings on the new file. Narrowed from the repo-widepnpm lintdeliberately and declared here: the full farm runs in CI regardless.Changeset:
.changeset/7014-q2-option-label-empty-string.md(@object-ui/app-shell: patch — user-visible).Not in scope, recorded
The same writer also carries only
value/label/color, so a stored option'sdefaultorvisibleWhenis dropped on any picklist edit — both are keysSelectOptionSchemaaccepts (measured here:default: trueACCEPT,visibleWhen: 'true'ACCEPT, parsed to{dialect:'cel', source:'true'}). That is silent data loss rather than a 422, it needs the localOptiontype widened, and #7014's own Tier-1 row 3 already records exactly that absence — so it belongs to Q1, not here. Not touched.🤖 Generated with Claude Code
https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
Generated by Claude Code