Skip to content

fix(console): a file field on a FormView renders the shared upload control, not a text box - #10173

Merged
os-elon-musk merged 2 commits into
mainfrom
claude/issue-10167-formpage-file-field-arm
Sep 21, 2026
Merged

os-elon-musk merged 2 commits into
mainfrom
claude/issue-10167-formpage-file-field-arm

Conversation

@os-elon-musk

Copy link
Copy Markdown
Collaborator

Fixes #10167

Clause-②: no — declared on the claim, unchanged on delivery. node scripts/check-governed-queue-guard.mjs --test over this branch's three paths prints NOT GOVERNED — 3 path(s) checked against 5 governed surface(s); none matched. apps/console is an application, not a published contract face, and this repair pulls an outlier back to an already-declared field type rather than widening any accepted set.

Seat session, in prose because footers do not survive an edit: session_01Xr7APep6jm1Zta3KUzPzZf.

Premise, re-taken on origin/main 98178b2

The switch is FieldInput's switch (field.type) in apps/console/src/components/FormPage.tsx. Parsed out of the switch body rather than read by eye, its arms before this branch were, in tree order — 21 of them:

textarea · paragraph · long_text · number · integer · decimal · currency · email · url · password · date · time · datetime · timestamp · boolean · toggle · checkbox · select · picklist · enum · radio

No file, so the field fell through to the default arm's plain text input. The card's premise holds. The same parse answers the dispatch's assumption D: the switch body contains no map, lookup table or nested helper — every arm is a literal case, so an arm-list reading under-reports nothing on the type axis. It under-reports on three other axes; see step 1 below.

The fix — one arm, and no second file control

The file arm asks ADR-0059's resolver (resolveFormWidgetType + getLazyFieldWidget in @object-ui/fields) for the same widget the record form renders. That resolver's own docblock names this the door for a host rendering field widgets outside the form renderer, "so their type support can never drift behind the form surface", and @object-ui/app-shell's ActionParamDialog is already on it.

Writing an input type="file" here instead would have been a second upload pipeline: a second size guard, a second progress surface, and — the part that actually breaks — a second answer to what a submitted file VALUE is. A hand-rolled control puts a raw File into the form's values and the engine answers expected string, received object, which is exactly the shape objectui#10131 measured. Through the shared widget, this route submits what the other renderer submits: the sys_file id the upload adapter minted, or the legacy inline blob when the adapter surfaced none.

The resolution is hoisted to module scope. react-hooks/static-components refuses a component value produced during render, and the arm pins the type at 'file' anyway, so there is nothing per-render to resolve; React.lazy still defers the chunk until the arm first renders.

Two halves the symptom does not name, and why they are not scope creep

The object's declared upload configuration now reaches the widget. This app's local ObjectFieldDef admitted no multiple, no accept, no maxSize — all three of which @objectstack/spec declares on a field and the server serves. Without them the control would be a single-file, unfiltered, unbounded dropzone whatever the author wrote: declared but not delivered. That is the identical shape objectui#5627 recorded in this same file for the conditional rules — "nothing downstream could read a key this file's payload types did not admit".

Submit is blocked while an upload is in flight. The widget contract says outright what onUploadingChange is for: upload widgets fire it "so a host can block submit until a presigned upload settles". A file's value only becomes an id once the adapter resolves, so a submit that beats it stores nothing for the field and still reports success. Ablation leg C below shows the guard is load-bearing and nothing else depends on it.

Step 1 — the enumeration this card asked for

The full table is in the os-dev-report comment on objectui#10167. Headline, measured — population is @objectstack/spec/data's own FieldType enum (49 members), the resolver column is a runtime read of the built @object-ui/fields, and the arm column is parsed from the switch:

  • 35 of the 49 spec field types resolve to a dedicated widget on the shared resolver and have no arm here. One of those, text, is correctly served by the default arm; for the other 34 the default text input is the wrong control.
  • 0 types are unhandled on both sides. There is no "neither side supports it" bucket: every gap in this switch is a gap against a control that already exists.
  • 8 arms here spell names that are not spec FieldType members at all (paragraph, long_text, integer, decimal, timestamp, checkbox, picklist, enum) — a private alias table maintained nowhere else.

⭐ Three dispatch axes an arm list cannot see, and they are why "add the missing arms" would not close the class:

  1. widget. The spec declares widget as an optional string on a field, and the shared chain reads it (sectionFields.ts carries fd.widget; form.tsx resolves f?.widget || f?.field?.widget || f?.type). Three widget keys — object-ref, filter-condition, recipient-picker — are reachable only that way. FormPage reads no widget key anywhere, so it is blind to this axis no matter how many type arms it gains.
  2. Arity. mapFieldTypeToFormType('select', { multiple: true }) returns field:multiselect — a different component. This switch's select arm renders a single-value select element, so a declared multi-value picklist silently degrades here even though select "has an arm".
  3. Retirement. resolveFormWidgetType('owner') returns owner, which the registry answers with a tombstone that refuses visibly — the whole point of objectui#4814. Here owner has no arm and the default renders a working text box, which is precisely the silent degradation the tombstone exists to prevent.

⛔ Nothing above is acted on in this pull request. Per triage it is evidence for the separate decision-box card about whether this renderer should hold a switch at all.

Reverse verification — three legs, direction predicted before each run

Run from the committed tree; each mutation is proven on disk before the run (anchor removed, marker injected) and each restore is proven by blob hash against HEAD plus an empty git diff HEAD.

leg mutation predicted measured
A the file arm removed — the card's own fix reverted all six red 6 failed of 6
B the row stops carrying multiple / accept / maxSize exactly the two declaration tests red 2 failed, 4 passed of 6
C the onUploadingChange signal dropped exactly the in-flight test red 1 failed, 5 passed of 6

B and C are the discriminating legs: they show the two supporting halves each have their own pin, and that neither pin is carried by the arm.

Gates run here

command exit
pnpm exec vitest run apps/console/src/components/FormPage.fileField.test.tsx 0 — Tests 6 passed (6)
pnpm exec turbo run type-check --filter=@object-ui/console --concurrency=2 0 — 36 tasks, includes the whole dependency build
pnpm exec vitest run apps/console/ 0 — Test Files 106 passed (106), Tests 1187 passed (1187)
eslint . in apps/console 0 errors after the hoist (1 before it). FormPage.tsx base vs head: 0 errors / 21 warnings both sides, same rules, same counts; the new test file is 0 / 0
node scripts/check-changeset-presence.mjs 0
node scripts/check-control-bytes.mjs 0
node scripts/check-changeset-claims.mjs 0
node scripts/check-new-cross-file-line-citations.mjs 0 — 0 new citations
node scripts/check-i18n-call-site-keys.mjs 0
node scripts/check-test-path-roots.mjs 0
node scripts/check-vi-mock-specifiers.mjs 0
node scripts/check-governed-queue-guard.mjs --test 0 — NOT GOVERNED

Repo-wide Lint, the four Test shards, Build & E2E and Build Docs are CI's runs, not reproduced here.

Acceptance notes

Observations recorded rather than filed — ⛔ no cards were opened from this seat.

  • noted, not filed: the required marker in this file is a bare * span beside the label, not aria-hidden. ActionParamDialog hides its own, citing objectui#3299, so that the asterisk stays out of the control's accessible name. Both renderers draw the marker; only one keeps it out of the name. Carrier: whoever next touches this file's label block.
  • noted, not filed, and NOT MEASURED: /f/:slug is the anonymous route, and the console's upload adapter runs the authenticated presign/complete protocol. Whether an anonymous visitor may presign is a backend question this seat could not answer from this checkout — the storage service is not in it. The probe is a real anonymous submit of a form with a file field against a running backend. Before this branch the question could not arise, because the control did not exist.
  • noted, not filed: the three arms this switch spells for numbers (integer, decimal, plus currency) all render the same bare input type="number", while the shared resolver gives currency its own widget. Part of the enumeration above, not a separate finding.

Generated by Claude Code

…control

`FormPage` — the renderer behind `/f/:slug` and `/forms/:name` — has a
hand-rolled input switch with no `file` arm, so a `file` field fell
through to the default text input. Typing a filename into a text box
uploads nothing: the capability was absent on those routes, not
degraded.

The new arm writes no second file control. It asks ADR-0059's resolver
(`resolveFormWidgetType` + `getLazyFieldWidget`, the door
`ActionParamDialog` is already on) for the same widget the record form
renders, so this route submits the value the other renderer submits —
the `sys_file` id the upload adapter minted, or the legacy inline blob
when the adapter surfaced none.

Two supporting halves in the same file:

- The object field's declared `multiple` / `accept` / `maxSize` now
  reach the widget. This app's narrowed `ObjectFieldDef` admitted none
  of the three, so the control would have been single-file, unfiltered
  and unbounded whatever the author declared — the same
  "nothing downstream can read a key this file's payload types do not
  admit" shape objectui#5627 recorded here for the conditional rules.
- Submit is blocked while an upload is in flight. That is what the
  widget contract's `onUploadingChange` is for: the value only becomes
  an id once the adapter settles, so an earlier submit stores nothing
  and still reports success.

The other field types this switch does not spell are deliberately
untouched; the enumeration of them is reported on objectui#10167 for
the separate decision about whether this renderer should hold a switch
at all.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xr7APep6jm1Zta3KUzPzZf
`react-hooks/static-components` refuses a component value produced during
render — a fresh component identity remounts its subtree and loses its
state — and it was the one ESLint ERROR this branch added to
`apps/console` (`eslint .` there: 1 error before, 0 after; the file's 21
warnings are unchanged from `origin/main`, same rules, same counts).

Hoisting is also just correct here: the arm pins the type at `'file'`, so
there was nothing per-render to resolve. `React.lazy` still defers the
import until the arm first renders, so the widget does not join this
route's eager bundle.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xr7APep6jm1Zta3KUzPzZf
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 4 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/console-form-container-specs-one-declaration-5596.md

  • names FormPage.tsx → apps/console/src/components/FormPage.tsx — edited by this change

    objectui#5542 converged the LEAF of this contract — the field spec — and left the two containers above it untouched, because converging them was a bigger call than a mechanical import. FormSectionSpec and FormViewSpec were each hand-declared twice under the same names, once in packages/app-shell's SchemaForm.tsx and once in apps/console's FormPage.tsx. Unlike the leaf — whose console copy was a clean subset — these two had already drifted, in both directions, so neither copy was a subset of the other and there were two live answers to "what may an author write":

.changeset/console-form-field-spec-one-declaration-5542.md

  • names FormPage.tsx → apps/console/src/components/FormPage.tsx — edited by this change

    objectui#5040 was not a missing key. It was that two hand-written descriptions of one contract drifted, and nothing could notice, because each was only ever checked against itself. PR metadata-admin: FormFieldSpec declares dependsOn, one declaration for both halves #5537 converged the two app-shell descriptions into views/metadata-admin/form-spec.ts. A third survived in apps/console: FormPage.tsx declared its own nine-key interface FormFieldSpec, under the same name, in a different package — so the same failure mode stayed fully available.

.changeset/console-formpage-runtime-default-seed-5727.md

  • names apps/console/src/components/FormPage.tsx → apps/console/src/components/FormPage.tsx — edited by this change

    readPrefill in apps/console/src/components/FormPage.tsx seeded every declared default unconditionally. A defaultValue may be a literal, or an instruction the server resolves per insert — a DEFAULT_VALUE_TOKENS token (NOW(), current_user) or a CEL Expression envelope. Seeding one of those literally put the text NOW() into a datetime input on both /forms/:name and the public /f/:slug route, and submitting it sent that string as the field's value — which is neither absent nor null, so ObjectQL.applyFieldDefaults never resolved the declared default and the column stored the token text instead of a timestamp.

.changeset/console-formpage-visible-predicates-5594.md

  • names apps/console/src/components/FormPage.tsx → apps/console/src/components/FormPage.tsx — edited by this change

    apps/console/src/components/FormPage.tsx is a second, independent form renderer — its own buildSections, its own JSX — and it serves both the public /f/:slug route and the internal /forms/:name route. It read neither spelling of the FormView field visibility predicate: a repo-wide grep for a visibleWhen / visibleOn read inside that file returned zero. So a field an author conditioned on record.priority == 'urgent' — legal, spec-strict metadata that @objectstack/spec normalises to visibleWhen (ADR-0089), and that the metadata-admin designer both authors and honours — rendered unconditionally on both routes. Fail-open and silent: the author saw the field always, with no diagnostic.

  • names FormPage.tsx → apps/console/src/components/FormPage.tsx — edited by this change

    objectui#2212 recorded this exact symptom and PR fix(form): evaluate view-level FormField.visibleOn with the canonical CEL engine #2214 fixed it — in a different chain: ModalForm → resolveFormViewLayout → @object-ui/plugin-form sectionFields.ts → @object-ui/components renderers/form/form.tsx. FormPage.tsx is on that chain at no point, and Form-view FormField.visibleOn (CEL) is never evaluated — conditional fields always render #2212's regression pin lives with the chain it fixed, so nothing in the suite could see this copy. One contract, two implementations, each only ever checked against itself.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 2d7fff3b8 (merge-base with origin/main): 2 file(s) changed outside .changeset/, read against 1251 pending declaration(s) that publish a body (1816 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3033.1 KB 3104.5 KB
Main entry chunk (gzip) 147.1 KB 350 KB
Entry file index-CZ-xGIGj.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) 16.68KB 6.20KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
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) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 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) 546.12KB 130.78KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 221.99KB 61.72KB
fields (index.js) 251.84KB 63.48KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.02KB 11.00KB
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) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
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) 14.81KB 3.63KB
plugin-calendar (index.js) 51.44KB 14.62KB
plugin-charts (index.js) 71.73KB 20.08KB
plugin-chatbot (index.js) 198.20KB 47.14KB
plugin-dashboard (index.js) 132.96KB 35.17KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 255.19KB 66.48KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 140.25KB 35.85KB
plugin-gantt (index.js) 167.99KB 41.37KB
plugin-grid (index.js) 213.40KB 58.19KB
plugin-kanban (index.js) 48.71KB 15.17KB
plugin-list (index.js) 113.53KB 27.99KB
plugin-map (index.js) 21.74KB 7.07KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.49KB 11.97KB
plugin-timeline (index.js) 30.68KB 8.95KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 85.18KB 21.05KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
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) 109.04KB 36.08KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.38KB 1.98KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.74KB 2.54KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 15.71KB 5.30KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
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) 0.20KB 0.18KB
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) 4.74KB 2.25KB
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 (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
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) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
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

Copy link
Copy Markdown
Collaborator Author

changeset-claim-re-read answered — all 4 pending claims survive this diff. ⛔ No correction owed.

domain:ui seat #1, session_01Xr7APep6jm1Zta3KUzPzZf, 2026-09-21T00:20Z. Answering because the gate asks the one question it will not answer itself, and these bodies publish verbatim into the CHANGELOG — a one-way door.

⚠️ Two of the four are convergence claims, and this diff adds interface members — so this was not a formality. The real question was: did this change re-fork a contract a previous PR converged? Read at source on the branch, ⛔ not inferred from the diff summary.

The load-bearing reading

apps/console/src/components/FormPage.tsx:127:

import type { FormFieldSpec, FormSectionSpec, FormViewSpec } from '@object-ui/app-shell';

⇒ all three converged specs are imported, ⛔ not redeclared. The convergence stands exactly as those two changesets describe it.

The three keys this change adds (multiple, accept, maxSize) went into ObjectFieldDef and RenderableField — both local shapes this app owns, and neither governed by those claims. ObjectFieldDef describes an object's field as object metadata declares it (type, label, group, required, defaultValue, maxLength, options, placeholder, helpText); the converged FormFieldSpec is the FormView override spec. Different contracts, and the diff's own comment says so where it reads them: 「override (FormFieldSpec) declares none of these three」.

⭐ That is the distinction that keeps claim 5542 true: the failure mode it retired was a second declaration of the same contract under the same name. Adding a member to a differently-named, differently-scoped local interface is ⛔ not that.

pending changeset what it asserts verdict
console-form-field-spec-one-declaration-5542.md the third FormFieldSpec in apps/console was converged away ✅ still true — imported at :127, ⛔ not redeclared
console-form-container-specs-one-declaration-5596.md FormSectionSpec / FormViewSpec converged to one declaration each ✅ still true — both imported at :127
console-formpage-runtime-default-seed-5727.md readPrefill no longer seeds a DEFAULT_VALUE_TOKENS token or CEL envelope literally ✅ still true — this diff adds no line in readPrefill and changes no default-seeding path
console-formpage-visible-predicates-5594.md this renderer now reads the FormView visibility predicate ✅ still true — this diff touches no visibleWhen / visibleOn read; its hunks are the switch arm, the two local interfaces, the field row, and the submit gate

⇒ ⛔ no body corrected, because ⛔ none went false. Frontmatter and prose of all four untouched.

domain:ui seat #1 · session_01Xr7APep6jm1Zta3KUzPzZf · gate answer · read against PR head 938c2e6947 at 2026-09-21T00:20Z


Generated by Claude Code

@os-elon-musk
os-elon-musk marked this pull request as ready for review September 21, 2026 00:24
@os-elon-musk
os-elon-musk added this pull request to the merge queue Sep 21, 2026
Merged via the queue into main with commit 7725c10 Sep 21, 2026
45 checks passed
@os-elon-musk
os-elon-musk deleted the claude/issue-10167-formpage-file-field-arm branch September 21, 2026 00:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants