Skip to content

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

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-12130-form-field-channel-keys
Oct 11, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-12130-form-field-channel-keys

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

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 amendment 6112222681).

Implemented by the os-dev dispatched from the domain:spec @ objectui seat, session https://claude.ai/code/session_01V8qYZseSLfKH3V8tPu2f73, claim comment 6111358337. Base: main at a7d4920 (one commit past the claimed 158c75d; it touches only packages/app-shell and packages/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 form renderer hands each fields[] entry to its field widget twice: as the metadata carrier (field: field.field || field in the one renderFieldComponent call of renderers/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 registered field: widget and one unregistered type (the default branch) in three states (empty; read-only with a value; disabled with a value) through the real SchemaRenderer. 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 on FormFieldSchema are 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.ts FormField, packages/types/src/zod/form.zod.ts FormFieldSchema, kept in parity):

key how declared read by (census) measured effect
language spec FieldSchema by reference code the editor asks for code in it
maxSize spec by reference file, image a larger pick is refused before upload
allowCreate spec by reference lookup, master_detail false withdraws the create offer
descriptionField spec by reference lookup, master_detail the record field under each option
displayField spec by reference lookup, master_detail, user each option's label
lookupColumns spec by reference lookup, master_detail the browse-all dialog's columns
lookupFilters spec by reference lookup, master_detail, user the candidate query's filter
lookupPageSize spec by reference lookup, master_detail the browse-all dialog's page size
idField LookupFieldMetadata member by reference lookup, master_detail the record field a pick submits
picker UserFieldMetadata member by reference lookup, master_detail, user search replaces quick-select and dialog
subtitle UserFieldMetadata member by reference lookup, master_detail, user record fields beside each search candidate
capture as the file widget reads it: 'environment' | 'user' | false file adds a camera input in that mode
crop as the image widget reads it: boolean image false removes the crop action
defaultName as the avatar widget reads it: string avatar the name the empty avatar's initials come from

The 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 defaultValue or defaultChecked by 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 with defaultChecked: true stays aria-checked="false" and submits false; a field with defaultValue submits no value. Each case's control seeds the same value through FormSchema.defaultValues and reads it back. The strict face keeps refusing both by name (pinned in packages/types/src/__tests__/form-field-channel-keys-12130.test.ts). So packages/types/examples/login-form.ts drops its inert defaultChecked: false and content/docs/components/form/form.mdx drops the field-level defaultValue, pointing at defaultValues.

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

  • H1 confirmed. The channel is the renderFieldComponent(resolvedType, { ...fieldProps, field: field.field || field, ...formField, … }) call inside renderFormField. The census controls show the entry proxy reaching the form renderer (visibleWhen), a widget's carrier read (scale by NumberField, reference by LookupField) and a widget's props read (dependsOn by MultiSelectField).
  • H2 confirmed, with one correction. FormFieldSchema is one flat z.object declaring neither key. FormField declares neither either, but it carries an index signature, so the TypeScript face never refused them: only the strict zod face did.
  • H3 answered: the registry set, not timing or host. With components only, text takes the unregistered default branch, which spreads leftover entry keys onto a native input, so defaultValue is drawn (the card's reading, pointer case B). With @object-ui/fields, text resolves to field:text, whose toDomProps whitelist drops the key: empty (pointer case E). input is a built-in branch that writes value={value ?? ''} after its spread: empty in both (cases A and F). The built-in textarea draws 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's checked="" reading is Radix's hidden bubble input; the visible box stays unchecked.
  • H4: no reading moves. PR objectui#12133's seed check (isServerOwnedValue) reads field.field.defaultValue, the object-field stash, never the entry's own key. Measured: a CEL envelope on the entry's own defaultValue still seeds false and submits false; the same envelope under field.defaultValue leaves the box unseeded.
  • H5 confirmed and wider. See the census ledger; fourteen keys are declared here and the remaining rows are classified below.
  • H6 measured. The reader column of the table above is the full list; on every other type, the built-in branches included, each declared key is accepted and read by nothing. Same shape as objectui#11070's flat grid keys.
  • H7. Installed @objectstack/spec 17.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 the field.field stash (H4), not through the entry's own key.
  • H8 confirmed. login-form.ts carried defaultChecked: false, not true. 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 (number sets its input step). But ObjectForm (@object-ui/plugin-form) writes step: 'any' onto the entries it builds, which the spec's numeric FieldSchema.step refuses: declared by reference, @object-ui/plugin-form stops compiling (measured in the scoped build). Options: (A) declare by reference and have ObjectForm stop writing 'any' (a plugin-form change, excluded from this dispatch; its own comment says the registered widgets derive their own step anyway); (B) declare number | 'any' on both faces, departing from the spec; (C) rule it runtime-only. Recommendation: A.
  • avatarField / avatar_field. The lookup and user widgets read both, snake_case first; UserFieldMetadata declares the snake_case one; this face is camelCase (objectui#11610); the spec declares neither. Options: (A) declare avatarField and retire the snake_case read and member in the snake-to-camel family; (B) declare avatar_field by reference; (C) runtime-only. Recommendation: A.
  • className on 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 through colSpan / 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 (field objectui#6609, mobile_fullscreen objectui#3245, currency objectui#11070 round 13).

Acceptance notes

  • File surface. The claim listed packages/types/src/__tests__/ for the enumeration pin. The runtime census cannot live there: @object-ui/types is zero-dependency, and a test importing @object-ui/components / @object-ui/fields / @object-ui/react fails check:phantom-deps. It lives in examples/schema-catalog/test/, which declares those packages and already holds objectui#11070's runtime pins. The face pin is in packages/types/src/__tests__/. The three census files are new; no other claim holds them.
  • Census bounds. Reads made only inside an interaction handler are not enumerated (the census renders, it does not click). The built-in branches' spread of leftover keys onto a native control or a Radix primitive is an open set read by no name; keys that work only there are measured by the effects files, not the census. The development-mode schema validator's reads are dropped.
  • Renderer readings, not fixed here (findings, for the seat to file): a field-level defaultValue is drawn by the built-in textarea branch (both registry sets) and by the default branch (components only) but never submitted, so the user sees text that is not saved; defaultChecked checks 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-check and pnpm --filter @object-ui/example-schema-catalog type-check: exit 0. tsc --listFiles confirms 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, then pnpm check: 286 validated, 147 recognised but not validated, 0 did not validate. objectui validate on a form using displayField, language, maxSize, capture: valid. On one carrying defaultChecked / defaultValue: refused by name.
  • Scoped build (turbo run build over the check-doc-snippet-types --build-filter set): 35 of 35 tasks. Then check-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 names FormField), 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.
  • Targeted eslint (--no-inline-config --format json) on the 8 changed .ts / .tsx files: 8 files, 0 errors, 9 warnings, all no-explicit-any at pre-existing lines of form.ts (the base file reads 9 too). The population is eslint.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.
  • Ablation, committed state, trap-restored, git diff HEAD empty after: (1) FormField.capture widened to any: tsc -p tsconfig.test.json in packages/types exits 2 on two of the face pin's type assertions. (2) language removed from FormFieldSchema: the census turns red with unexpected: ["language ← entry@fields/src/widgets/CodeField.tsx"]. The census reads @object-ui/types/zod through the vitest alias to src, so no build sits between the mutation and the run.

Generated by Claude Code

…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>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 291 chunks) 3177.2 KB 3204.6 KB
Main entry chunk (gzip) 74.2 KB 350 KB
Entry file index-CeTGP7cJ.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) 20.05KB 7.41KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
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) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 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) 587.77KB 141.53KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 275.30KB 70.07KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 11.71KB
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) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
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) 16.04KB 3.92KB
plugin-calendar (index.js) 54.31KB 15.86KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.72KB 48.07KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 233.77KB 50.02KB
plugin-detail (index.js) 250.37KB 66.07KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.80KB 46.04KB
plugin-gantt (index.js) 179.37KB 45.17KB
plugin-grid (index.js) 255.19KB 70.94KB
plugin-kanban (index.js) 53.23KB 16.71KB
plugin-list (index.js) 122.82KB 31.24KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.21KB 11.85KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 92.11KB 23.27KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
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) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
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) 1.13KB 0.65KB
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) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
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) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9a81311ce12c81a623f4edf4d26d202009d35ce5
Local-runs: none

Inputs, and nothing else: card objectui#12130 (body and all five comments: triage 6106872887, pointer 6108177389, claim 6111358337, os-dev-report 6112174014, seat amendment 6112222681); PR #12154 (body, ten-file list, net diff against main at the head sha, read with git diff origin/main...HEAD-SHA and git show); the head's check-runs. The spec member shapes cited below were read from the spec source at the version objectui's lockfile pins (17.7.0). Nothing built, run or re-run.

Check-runs on the head, read at 2026-10-11T18:36Z: 43 total (total_count equals the returned count), 40 success, 3 skipped (the two coverage matrix placeholders and dependabot), 0 failed, 0 in progress. Every derived gate family on the head is green: Type Check, the eight Test shards, Test (dist pins), Lint, Line Citation Gate, the five changeset gates, Doc Snippet / Component / Example Id / Fence checks, Spec Main Shape Gate, Governed Surface Queue Guard, Build and E2E.

① Derived judgments

Every accept-set and public-surface change the diff implies, each named right or wrong:

  1. Strict authoring face widens on form fields[] entries by exactly fourteen keys — language, maxSize, capture, crop, defaultName, allowCreate, descriptionField, displayField, idField, lookupColumns, lookupFilters, lookupPageSize, picker, subtitle — declared on FormFieldSchema, hence on StrictAnyComponentSchema by derivation. Right. Triage 6106872887 directed a runtime census first and then "declare each key on the field schema the widget reads it from"; the census pin form-field-channel-census-12130.test.tsx records each key read by name off the entry or the props bag through the real SchemaRenderer, and the effects file renders each with and without. I verified the readers against main source: FileField reads maxSize (bytes) and capture as 'environment' | 'user' | false | null; ImageField reads crop !== false and maxSize; AvatarField reads defaultName; LookupField reads displayField || reference_field, descriptionField, idField || 'id', allow_create ?? allowCreate, lookup_columns ?? lookupColumns, lookup_page_size ?? lookupPageSize, lookupFilters; UserField reads displayField, picker, subtitle, lookupFilters. The face is flat (one z.object, the shape objectui#11070 left), so each key is accepted on every field type; DECLARED_BY_12130 in the census pins the read-by types per key, so a reader appearing on another type turns it red.

  2. Eight of the fourteen are the spec's FieldSchema members by reference (stripImportedDefaults(SpecFieldSchema).shape.X, the pattern the same file already uses for multiple, rows, reference, scale and the rest). Right. In the spec source at the pinned version: maxSize and lookupPageSize are z.number().int().positive(), lookupFilters[].operator is the nine-member enum (eq, ne, gt, lt, gte, lte, contains, in, notIn), the lookupColumns object arm requires field, language, displayField and descriptionField are z.string(), allowCreate is z.boolean(). The face pin's wrong-value rows (maxSize: 0, lookupPageSize: -10, operator 'equals', a column object without field, language: 3, and the rest) are each refused by those exact shapes, so the changeset's description of the value checks is accurate.

  3. Three are restated from the field-metadata TypeScript types on the zod side — idField: z.string(), picker: z.enum(['search', 'default']), subtitle: z.array(z.string()). Right, with a bound. No zod twin of LookupFieldMetadata or UserFieldMetadata exists under packages/types/src/zod/ (directory listing read), so restating is the only zod spelling available; the TypeScript twin references the members (LookupFieldMetadata['idField'], UserFieldMetadata['picker'], UserFieldMetadata['subtitle']) and the face pin's type-level Equal assertions hold it there. Parity verified on main's field-types.ts: idField?: string, picker?: 'search' | 'default', subtitle?: string[]. The bound, said per AGENTS.md 完善设计器的每一个细节 #9: nothing re-derives the zod enum from the TypeScript union, so a later change to UserFieldMetadata.picker moves FormFieldSchema.picker only if the face pin's wrong-value row is updated by hand.

  4. Three are typed as the widget reads them — capture: 'environment' | 'user' | false, crop: boolean, defaultName: string, TypeScript and zod in parity (z.union([z.enum([...]), z.literal(false)]), z.boolean(), z.string()). Right. Each matches the reader named in (1); neither the spec nor a metadata type declares them, so there was nothing to reference.

  5. The TypeScript face narrows from the index signature's any to typed members. FormField carries [key: string]: any (pre-existing, untouched here), so these keys were never refused by TypeScript; a typed literal with a wrong-typed value is now a compile error. Right, and stated in the changeset. The PR body's H2 correction (only the strict zod face ever refused them) is accurate.

  6. The tolerant face (AnyComponentSchema, safeValidateSchema) narrows: a wrong-typed value at any of the fourteen keys is now refused where the strip-by-default z.object previously dropped it unjudged, and a well-typed value is now kept. Right, stated as BREAKING in the changeset; the face pin asserts both halves (kept on the tolerant face; wrong value refused on both faces). This is the one change in the diff that can move a runtime validation verdict for an existing document, and it moves in the direction AGENTS.md #0.1 requires: one strict contract, the producer fixes the value.

  7. defaultValue and defaultChecked stay refused by name on the strict face. Right. The census records no reader asking for either by name (its CONTROL row); the effects and components-only files show neither becomes the submitted value on checkbox / switch / boolean and seven text-like types, in both registry sets, each case controlled through FormSchema.defaultValues. This is triage's own branch: "where a key is measured inert, the refusal stands." The card's premise is falsified in part — its checked="" reading was Radix's hidden bubble input, and login-form.ts carried defaultChecked: false, not true — and the dev reported premise_still_valid: false rather than inheriting it. The disagreement in pointer 6108177389 (case B against case E) is resolved by registry set, not timing: the unregistered default branch spreads leftover entry keys onto a native input, while field:text's toDomProps drops them. Consistent with BUILTIN_FIELD_TYPES and the single renderFieldComponent(...) call carrying field: field.field || field in form.tsx on main.

  8. packages/types/examples/login-form.ts drops defaultChecked: false. Right. The claim's ⛔ line permitted exactly this on an inert ruling, and the key is inert by measurement. examples/ is not in the package's files list, so no published artefact moves.

  9. content/docs/components/form/form.mdx drops the field-level defaultValue row and annotates defaultValues. Right, within the claim's scope for that file ("only where it states the decided outcome for a field key"). Noted: the fourteen new keys are not added to that page's abridged FormField snippet; the per-field pages under content/docs/fields/ already teach the lookup, user, file and image keys and were outside the claimed surface. The doc gates on the head are green.

  10. Enumeration pin. Triage asked that "the census for this channel becomes a test that fails on a new read-but-undeclared field key." Delivered: the census's unexpected / gone assertion against its LEDGER, with reader attribution, plus a per-row reader pin and the DECLARED_BY_12130 read-by-type pin. Its bounds (no interaction-handler reads; the built-in branches' spread is an open set measured only by the effects file; dev-mode validator reads dropped) are stated in the file header per 完善设计器的每一个细节 #9. The ablation (two legs red in the named places) is the dev's reading at the measurement commit; I did not re-run it, per the read-only shape — the head's green covers the passing side only.

  11. Nothing else on the public surface moves: no renderer file, no packages/components / fields / plugin-form change, no export, no package.json contract field, no governed-surface path. The file list is ten files and matches the report's files_changed exactly. No model identifier in the diff, the PR title or body, the changeset or the three commit messages (grepped); the commit trailers carry the model-free pair AGENTS.md prescribes.

② Semver level

  • Changeset .changeset/12130-form-field-channel-keys.md: '@object-ui/types': minor, with the BREAKING semantics stated in the body (tolerant-face narrowing on wrong-typed values; TypeScript literal narrowing). Matches what the diff publishes. The only published source that changes is packages/types/src/form.ts and packages/types/src/zod/form.zod.ts; @object-ui/example-schema-catalog is in the changeset ignore list and its three new files are tests; content/docs is no package. By this repo's version model (objectui's major follows the @objectstack family major; its own breaking changes ship as minor with the semantics stated), minor is the right level, and major would have been refused by check-changeset-no-major. The five changeset check-runs on the head are green.
  • Clause-②: yes. PR body, changeset ("yes (widening)"), claim 6111358337 and triage 6106872887 agree, and the diff bears it out: the strict face's accept set widens by the fourteen declared keys. Right.

③ Boundary flags

Dev deviations (report 6112174014), each answered or escalated:

  1. File-surface breach — three new test files under examples/schema-catalog/test/, outside the claim's list; the dev went on to a draft PR instead of stopping. Answered. Seat amendment 6112222681 accepted it, and the reason holds on main: @object-ui/types is zero-dependency, so a test there importing renderers fails check:phantom-deps; examples/schema-catalog declares @object-ui/components, fields, react, core and types and already hosts the objectui#11070 runtime pin fields-scale-currency-config-11070.test.tsx. The face pin sits in packages/types/src/__tests__/ as claimed. The root vitest dom project includes examples/**/*.test.tsx, so all three files are in CI's population (the Test shards are green on this head).
  2. Base a7d4920, not the claimed 158c75d — one unrelated commit on packages/app-shell and packages/plugin-detail. Answered: no overlap with this diff. The PR reads behind against today's main; the queue rebuilds on current main, which is the seat's landing concern, not this verdict.
  3. Scope against triage — step, avatarField / avatar_field and className left undeclared though read and effective (step's effects pin passes on this head). Escalated, rightly. Each is a census ledger row marked OPEN with its reason, the seat amendment keeps the card open for them, and the PR was changed to Part of. Named plainly because it is the class park review 5887006513 rules out: on this head the strict face still tells an author to remove step from a number field, where it works. The blocker is real and outside this dispatch: the spec's FieldSchema.step is z.number() and ObjectForm in plugin-form writes 'any', so declaring it by reference breaks that producer's build. These three rows must not be lost; the card carries them to its next round.
  4. Model-free commit trailers instead of the harness's model-named pair — per objectui AGENTS.md. Answered, correct.

Open questions, each:

  • OQ1 step (A: declare by reference and stop ObjectForm writing 'any'; B: number | 'any' on both faces; C: runtime-only). Escalated to the card's next round. A is the #0.1 shape (fix the producer, one dialect); B fossilises a second dialect the spec refuses on object fields; C leaves the false refusal standing. The seat decides; nothing in this diff pre-empts it.
  • OQ2 avatarField / avatar_field (A: camelCase, retire the snake read and UserFieldMetadata.avatar_field in the objectui#11610 family; B: snake by reference; C: runtime-only). Escalated. A matches the face's convention; UserField on main reads avatar_field ?? avatarField ?? 'image', so retiring the snake read is a fields change outside this dispatch.
  • OQ3 className on a field entry (control, field row, or refused). Escalated. It needs a ruling on which element it styles and a renderer fix for the built-in branches, which replace their own classes with it.
  • OQ4 the close keyword against Part of. Answered by the seat: Part of #12130. The PR body carries that line and the sentence "This PR closes nothing"; I scanned the body for a close keyword adjacent to an issue reference and found none, so merging will not close the card.

Out-of-scope findings (four) — escalated to the seat for filing through the filing gate; none gates this PR: (a) a field-level defaultValue drawn by the built-in textarea branch and the default branch but never submitted, the DOM pass-through family; (b) max_size taught by file.mdx and image.mdx and declared on FileFieldMetadata / ImageFieldMetadata while the widgets read only maxSize; (c) snake_case producers and docs (paramToField.ts, FlowReferenceField.tsx, lookup.mdx, user.mdx) for keys the strict face spells camelCase — pre-existing, and the head's doc-fence gate is green; (d) Radix's bubble input reflecting defaultChecked, noted only.

Also noted, not a defect of this diff: the card body's two premise details (defaultChecked: true in login-form.ts; the key draws the box checked) were both wrong, and the dev measured instead of inheriting them, which is what triage's direction asked.

Implemented-by: claude/issue-12130-form-field-channel-keys
Reviewed-by: session_01V8qYZseSLfKH3V8tPu2f73

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 11, 2026 18:38
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 11, 2026 18:38
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 11, 2026
Merged via the queue into main with commit 0665c3b Oct 11, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-12130-form-field-channel-keys branch October 11, 2026 18:57
zhuangjianguo pushed a commit that referenced this pull request Oct 11, 2026
Brings main to faf4ce9 (objectui#12151, #12152, #12153, #12154, #12148,
#12149, #12147, #12158, #12159); none of them touches this branch's files.

Claude-Session: https://claude.ai/code/session_01TYgwmFK1q4KJ6Qq2WRLzsD
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation examples package: types tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants