Repository navigation
fix(types): a runtime census of the form-field channel; declare the keys field widgets read, keep field-level defaultValue / defaultChecked refused (objectui#12130) - #12154
Conversation
…e any declaration (objectui#12130) Measures, through the real SchemaRenderer, every key of a form `fields[]` entry the runtime reads by name: a recording proxy over the entry (the form renderer and every field widget's metadata carrier) and over each registered field widget's props, three render states per type. The keys read and not declared on FormFieldSchema are pinned as a ledger with their readers, so a new read-but-undeclared key turns the census red. Beside it, what each ledgered key does (with and without it, through the real registrations), and the two keys the card named: neither `defaultChecked` nor `defaultValue` is read by name, and neither becomes the submitted value, with or without `@object-ui/fields` registered. Claude-Session: https://claude.ai/code/session_01V8qYZseSLfKH3V8tPu2f73 Co-authored-by: Claude <noreply@anthropic.com>
…asures read and effective (objectui#12130) The census measured them read by name off a form `fields[]` entry, and the effects pins show each one changing what its widget does, while the strict authoring face told authors to remove them. Declared on FormFieldSchema and FormField: the spec's FieldSchema members by reference (`step`, `language`, `maxSize`, `allowCreate`, `descriptionField`, `displayField`, `lookupColumns`, `lookupFilters`, `lookupPageSize`), `idField` / `picker` / `subtitle` by reference to the field metadata types, and `capture`, `crop`, `defaultName` typed as their widgets read them. `defaultValue` and `defaultChecked` stay undeclared: no reader asks for either and neither becomes the field's value. The census ledger now holds the remaining read-but-undeclared keys, each with why its refusal stands or that the question is open. Claude-Session: https://claude.ai/code/session_01V8qYZseSLfKH3V8tPu2f73 Co-authored-by: Claude <noreply@anthropic.com>
…c stop teaching keys the census measures inert (objectui#12130) `ObjectForm` writes `step: 'any'` onto the form fields it builds, a value the spec's numeric `FieldSchema.step` refuses, so declaring `step` by reference breaks that producer's build. It stays undeclared and moves to the census ledger as open, with the effect it has recorded beside it. The `login-form` example drops `defaultChecked: false`, and the form doc drops the field-level `defaultValue`: no reader asks for either key, and neither becomes the field's value. A form's initial values are `FormSchema.defaultValues`. One changeset for `@object-ui/types`. Claude-Session: https://claude.ai/code/session_01V8qYZseSLfKH3V8tPu2f73 Co-authored-by: Claude <noreply@anthropic.com>
✅ 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 reviewServed-tier: Inputs, and nothing else: card objectui#12130 (body and all five comments: triage Check-runs on the head, read at 2026-10-11T18:36Z: 43 total ( ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each named right or wrong:
② Semver level
③ Boundary flagsDev deviations (report
Open questions, each:
Out-of-scope findings (four) — escalated to the seat for filing through the filing gate; none gates this PR: (a) a field-level Also noted, not a defect of this diff: the card body's two premise details ( Implemented-by: VERDICT: PASS Generated by Claude Code |
Part of #12130
Clause-②: yes
Declaring the measured keys widens the strict authoring face's accept set on
fields[]entries; their value checks narrow the tolerant face, where the same keys used to be stripped unjudged.This PR closes nothing. It lands the census, its enumeration pin, fourteen declarations, and the measured refusal of
defaultValue/defaultChecked. The three read keys under "Decisions left open" (step,avatarField/avatar_field,className) stay with the card for its next round (seat amendment6112222681).Implemented by the os-dev dispatched from the
domain:spec@ objectui seat, sessionhttps://claude.ai/code/session_01V8qYZseSLfKH3V8tPu2f73, claim comment 6111358337. Base:mainata7d4920(one commit past the claimed158c75d; it touches onlypackages/app-shellandpackages/plugin-detail).What changed
1. A runtime census of the form-field channel, committed before any declaration (
examples/schema-catalog/test/form-field-channel-census-12130.test.tsx).The
formrenderer hands eachfields[]entry to its field widget twice: as the metadata carrier (field: field.field || fieldin the onerenderFieldComponentcall ofrenderers/form/form.tsx) and as props, because the entry's leftover keys are spread into the widget's props bag. The census renders every built-in branch, every registeredfield:widget and one unregistered type (the default branch) in three states (empty; read-only with a value; disabled with a value) through the realSchemaRenderer. Two recording proxies log every read of an ABSENT key: one over the entry, attributed to the source module on the stack, and one over each widget's props. A props read counts only when the entry can deliver it: the census re-renders with the key written on the entry and keeps it only if the widget receives that exact value. The pin asserts that the keys read and not declared onFormFieldSchemaare exactly a ledger, with the readers each row records. A widget that starts reading a new undeclared key turns it red, naming the key and the reader.2. What each key does (
form-field-channel-effects-12130.test.tsx, real registrations;form-field-channel-components-only-12130.test.tsx, without@object-ui/fields). Every ledgered key that is declared here is rendered without and with it, and the widget's observable changes. Each case's first render is its control.3. Declarations (
packages/types/src/form.tsFormField,packages/types/src/zod/form.zod.tsFormFieldSchema, kept in parity):languageFieldSchemaby referencecodemaxSizefile,imageallowCreatelookup,master_detailfalsewithdraws the create offerdescriptionFieldlookup,master_detaildisplayFieldlookup,master_detail,userlookupColumnslookup,master_detaillookupFilterslookup,master_detail,userlookupPageSizelookup,master_detailidFieldLookupFieldMetadatamember by referencelookup,master_detailpickerUserFieldMetadatamember by referencelookup,master_detail,usersearchreplaces quick-select and dialogsubtitleUserFieldMetadatamember by referencelookup,master_detail,usercapturefilewidget reads it:'environment' | 'user' | falsefilecropimagewidget reads it:booleanimagefalseremoves the crop actiondefaultNameavatarwidget reads it:stringavatarThe face is flat: each key is accepted on every field type and read only by the widgets named (H6 below). The census re-derives that column on every run (
DECLARED_BY_12130).4. The two keys the card named stay refused. No reader on the channel asks for
defaultValueordefaultCheckedby name (asserted by the census). They reach a control only through the built-in branches' spread of leftover entry keys, and neither becomes the field's value on any type measured, with or without@object-ui/fields: a checkbox or switch withdefaultChecked: truestaysaria-checked="false"and submitsfalse; a field withdefaultValuesubmits no value. Each case's control seeds the same value throughFormSchema.defaultValuesand reads it back. The strict face keeps refusing both by name (pinned inpackages/types/src/__tests__/form-field-channel-keys-12130.test.ts). Sopackages/types/examples/login-form.tsdrops its inertdefaultChecked: falseandcontent/docs/components/form/form.mdxdrops the field-leveldefaultValue, pointing atdefaultValues.5.
form-field-zod-coverage.test.ts's declared key list gains the keys; one.changeset/12130-form-field-channel-keys.md(@object-ui/types: minor, breaking semantics stated).Readings per mechanism hypothesis
renderFieldComponent(resolvedType, { ...fieldProps, field: field.field || field, ...formField, … })call insiderenderFormField. The census controls show the entry proxy reaching the form renderer (visibleWhen), a widget's carrier read (scalebyNumberField,referencebyLookupField) and a widget's props read (dependsOnbyMultiSelectField).FormFieldSchemais one flatz.objectdeclaring neither key.FormFielddeclares neither either, but it carries an index signature, so the TypeScript face never refused them: only the strict zod face did.texttakes the unregistered default branch, which spreads leftover entry keys onto a native input, sodefaultValueis drawn (the card's reading, pointer case B). With@object-ui/fields,textresolves tofield:text, whosetoDomPropswhitelist drops the key: empty (pointer case E).inputis a built-in branch that writesvalue={value ?? ''}after its spread: empty in both (cases A and F). The built-intextareadraws it in both sets. The same readings hold with the real lazy registrations and with eagerly resolved widgets. In no case does the value reach the submit. The card'schecked=""reading is Radix's hidden bubble input; the visible box stays unchecked.isServerOwnedValue) readsfield.field.defaultValue, the object-field stash, never the entry's own key. Measured: a CEL envelope on the entry's owndefaultValuestill seedsfalseand submitsfalse; the same envelope underfield.defaultValueleaves the box unseeded.@objectstack/spec17.7.0,liveness/field.json,props.defaultValue:{"status":"live","evidence":"packages/objectql/src/engine.ts"}, the same as the seat's source reading. It records the engine applying an object field's default on insert, which reaches a form only through thefield.fieldstash (H4), not through the entry's own key.login-form.tscarrieddefaultChecked: false, nottrue. The census rules the key inert, so it is removed.Decisions left open (not declared; each is a ledger row marked OPEN)
step. Read and effective (numbersets its input step). ButObjectForm(@object-ui/plugin-form) writesstep: 'any'onto the entries it builds, which the spec's numericFieldSchema.steprefuses: declared by reference,@object-ui/plugin-formstops compiling (measured in the scoped build). Options: (A) declare by reference and haveObjectFormstop writing'any'(a plugin-form change, excluded from this dispatch; its own comment says the registered widgets derive their own step anyway); (B) declarenumber | 'any'on both faces, departing from the spec; (C) rule it runtime-only. Recommendation: A.avatarField/avatar_field. Thelookupanduserwidgets read both, snake_case first;UserFieldMetadatadeclares the snake_case one; this face is camelCase (objectui#11610); the spec declares neither. Options: (A) declareavatarFieldand retire the snake_case read and member in the snake-to-camel family; (B) declareavatar_fieldby reference; (C) runtime-only. Recommendation: A.classNameon a field entry. Read off props by several widgets and forwarded to the control by the rest; on a built-in branch it replaces the branch's own classes. Options: (A) declare it as the control's classes; (B) leave it refused (layout goes throughcolSpan/span/fieldContainerClass); (C) declare it for the field row, a renderer change. No recommendation without a ruling on which element it styles.The other ledger rows keep their refusal, each by an existing rule: second spellings (
allow_create,lookup_columns,lookup_page_size,reference_field,defaultCurrency; AGENTS.md #0.1), runtime values no document can carry (dataSource,onCreateNew,onUploadingChange), and runtime-only keys already ruled (fieldobjectui#6609,mobile_fullscreenobjectui#3245,currencyobjectui#11070 round 13).Acceptance notes
packages/types/src/__tests__/for the enumeration pin. The runtime census cannot live there:@object-ui/typesis zero-dependency, and a test importing@object-ui/components/@object-ui/fields/@object-ui/reactfailscheck:phantom-deps. It lives inexamples/schema-catalog/test/, which declares those packages and already holds objectui#11070's runtime pins. The face pin is inpackages/types/src/__tests__/. The three census files are new; no other claim holds them.defaultValueis drawn by the built-intextareabranch (both registry sets) and by the default branch (components only) but never submitted, so the user sees text that is not saved;defaultCheckedchecks only Radix's hidden bubble input. Same seam: the built-in branches' spread of leftover entry keys onto the control.Verification (at
9a81311)pnpm --filter @object-ui/types type-checkandpnpm --filter @object-ui/example-schema-catalog type-check: exit 0.tsc --listFilesconfirms both test projects compile the new test files.vitest run packages/types/: 369 files, 10177 tests passed.vitest run examples/schema-catalog/: 45 files, 2341 passed.vitest run scripts/__tests__/: 180 files passed, 2 skipped.pnpm --filter '@object-ui/cli...' build, thenpnpm check:286 validated, 147 recognised but not validated, 0 did not validate.objectui validateon a form usingdisplayField,language,maxSize,capture: valid. On one carryingdefaultChecked/defaultValue: refused by name.turbo run buildover thecheck-doc-snippet-types --build-filterset): 35 of 35 tasks. Thencheck-doc-snippet-types,check-doc-example-types,check-doc-json-fences,check-doc-component-types,check-prompt-component-keys,check:doc-fences: all exit 0.check:element-data-source-declaration,check:designer-field-key-parity,check:component-surface-parity(report-only; no row namesFormField),check:spec-symbols,check:control-bytes,check:new-line-citations(0 new),check:phantom-deps,check:test-path-roots,check:vi-mock-override-shape,check:unused-deps,check:unreferenced-sources,check:pending-changeset-literals,check:doc-example-ids,check-lint-coverage, and the five changeset gates: all exit 0.--no-inline-config --format json) on the 8 changed.ts/.tsxfiles: 8 files, 0 errors, 9 warnings, allno-explicit-anyat pre-existing lines ofform.ts(the base file reads 9 too). The population iseslint.config.js's**/*.{ts,tsx}minus its global ignores; no config enables type-aware linting, so this diff cannot move a verdict on an untouched file.git diff HEADempty after: (1)FormField.capturewidened toany:tsc -p tsconfig.test.jsoninpackages/typesexits 2 on two of the face pin's type assertions. (2)languageremoved fromFormFieldSchema: the census turns red withunexpected: ["language ← entry@fields/src/widgets/CodeField.tsx"]. The census reads@object-ui/types/zodthrough the vitest alias tosrc, so no build sits between the mutation and the run.Generated by Claude Code