Skip to content

fix(plugin-form): tabbed, split and wizard saves write through the simple form's outbound sequence (objectui#10563) - #10626

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-10563-form-arms-edit-payload
Sep 25, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-10563-form-arms-edit-payload

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #10563
Clause-②: no

What this changes

TabbedForm, SplitForm and WizardForm now build what a save writes through the same sequence as the simple form. That sequence is sanitizeFormData, which applies the server-owned roster, the system flag, computed, formula and read-only columns, unknown keys, and the field-level verdict from fieldWriteGate. Then comes omitServerResolvedDefaults on a create, and dirtyEditPayload against the record the form read on an edit. Before this PR an edit on these layouts sent the whole record, including id, owner_id, created_by, updated_at, the formula column and the field the caller may not edit. That payload went to both the host submitHandler and the OCC-guarded update. A simple form with mobile.stepper renders through WizardForm, so it is fixed by the same change.

  • The simple form's inline sequence moves into one internal function, formWritePayload in packages/plugin-form/src/writePayload.ts. The simple form now calls it, and the three layouts call it in place of their raw data / mergedData. There is no second sanitiser and no per-layout copy. index.tsx does not re-export the module, so the published surface is unchanged.
  • Each of the three layouts keeps the loaded-record snapshot the simple form keeps. snapshotLoadedRecord runs at its own findOne, the snapshot is cleared on the create / no-record branch, and advanceLoadedRecord runs after a landed write. This is the same LoadedRecordSnapshot object and helpers, held in a ref like the simple, modal and drawer forms hold it. Each layout does its own findOne, so each takes its own snapshot. The wizard's existing persistedRecord state is left alone: it feeds field-rule previous and must not advance after a save.
  • The dispatch said the stepper might need the simple form's snapshot passed down. That is not needed. WizardForm reads the record itself on that route and seeds its values from that read, so its own snapshot is the right baseline.

Premise check

Reproduced before any edit, on base 21d34d5e2. The pin was written first, then 20 rows, and run against the unmodified sources: Tests 16 failed | 4 passed (20). The 4 passing rows were the simple-form control. The tabbed edit row received { created_by, id, name: 'Mine v2', owner_id, score, stage, total, updated_at } where the simple form sent { name: 'Mine v2' }.

Create path. The simple form sanitises a create too: the strip runs before the create/edit branch. The three layouts only ran omitServerResolvedDefaults on a create. So the same gap existed on create, and this PR closes it. The pin's create row, a create seeded with a whole record, is red on base for all four non-simple routes.

One step past the claim's file surface: the render half of objectui#10120

The claim scoped the three layout files to "their writePayload only". This PR also touches their buildSectionFields callback, in its own commit 4c2bf0c9f. That commit can be dropped on its own if the seat rules against it.

Measured with a throwaway probe, not committed, under a principal with deal.score editable: false. The simple form, the simple form with sections, modal and drawer rendered score disabled. tabbed, split and wizard rendered it as a live input. ObjectForm gates a section's field objects before routing, but a field named by bare string has no name until the layout resolves it. Before this PR, typing into that input failed the save with a 403. With the new strip in place, the typed value would be dropped while the save returns 200. Each layout now passes its resolved section fields through the same applyFieldPermissions render pass ModalForm uses. I did this in place because the strip this PR adds is what makes that drop reachable, and because it is the other half of the same objectui#10120 invariant on the same three files. The claim's file surface needs the matching amendment.

Behaviour a host or author can notice

  • ⚠️ A host submitHandler on these layouts receives, in edit mode, the changed fields, or the full stripped payload when nothing changed. A master-detail header laid out tabbed renders TabbedForm with MasterDetailForm's submitViaBatch as its seam. Its parent operation now carries only the changed fields, as a simple header already did. MasterDetailForm and occSave.tsx are not touched.
  • A field named on the server-owned roster is stripped even when an object declares it as an ordinary field. owner is an example. The other layouts already did this. wizardSkipValidation.test.tsx used a business field literally named owner, and it is renamed assignee. That keeps the test's meaning, and a note in its header says why.
  • With a non-empty customFields, the object definition is not handed to the strip, which is the simple form's rule. An inline member the object does not declare is still written.
  • The create-default omission now uses isCreateFormMode (mode: 'create' or no recordId), the simple form's predicate. Before, it used mode === 'create'.

Docs and changesets

  • ObjectFormSchema.submitHandler JSDoc in @object-ui/types: the sentence that listed tabbed / wizard / split and the stepper as handing over every collected value is replaced. The rule now names every layout. The three layouts' own submitHandler JSDoc copies gain the edit-mode sentence the modal and drawer copies carry.
  • README "What an edit save writes" and the plugin-form.mdx section of the same name no longer exempt these layouts. They also say that every layout strips a create and renders a refused field disabled.
  • The pending objectui#10156 changeset keeps its reading of that change. Its "Not covered" paragraph is in the past tense ("at this change") and carries a dated note naming objectui#10563, the form PR objectui#10533 used for a pending entry.
  • New .changeset/10563-form-arms-write-payload.md, patch on @object-ui/plugin-form and @object-ui/types. @object-ui/types is included because its published .d.ts JSDoc changes.

Tests and gates, at b89752678

  • pnpm exec vitest run packages/plugin-form/ packages/types/ plus apps/console/src/components/FormPage.outcomeToast.test.tsx, FormPage.sectionGroup.test.tsx, FormPage.test.ts, FormPage.viewSpec.test.ts, examples/schema-catalog/test/catalog-gallery-render.test.tsx and packages/i18n/src/__tests__/raw-key-call-sites-3546.test.tsx, run from the repo root: Test Files 363 passed (363), Tests 7222 passed | 1 skipped (7223).
  • New pin packages/plugin-form/src/formArmsWritePayload-10563.test.tsx: 31 rows. There are six per route (simple as control, tabbed, split, wizard, and simple with mobile.stepper): edit, nothing changed, host seam, create, render and inline member. One more row covers a master-detail header laid out tabbed.
  • Red leg at b89752678: the four layout sources were put back to base blobs under the committed pin, with a trap restore checked by blob hash and an empty git diff HEAD / --cached. The result was Tests 24 failed | 7 passed (31). The 7 passes are the six simple-control rows and the stepper render row. The stepper's fields were already gated by the simple form before the wizard saw them.
  • Type-check, after turbo run build --filter='@object-ui/plugin-form^...' (11 tasks): pnpm --filter @object-ui/plugin-form --filter @object-ui/types run type-check exit 0. Both packages echo type-check: Done. tsc -p tsconfig.test.json --listFiles includes the two test files and writePayload.ts. The @object-ui/types change is JSDoc only, with no type change, so no reverse type check applies.
  • pnpm check:control-bytes: check-control-bytes: OK. pnpm check:new-line-citations: VERDICT new-cross-file-line-citations: 0 new citation(s). node scripts/check-changeset-presence.mjs: 8 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s). check:pending-changeset-literals, check-changeset-no-major, check-changeset-fixed, check-doc-links, check:doc-fences, check:doc-types, check:doc-example-ids, check:unreferenced-sources, check:test-path-roots, check:handler-key-reads and check:installed-pin-claims all exit 0. check-governed-queue-guard --test: NOT GOVERNED.
  • pnpm check:changeset-claims (report-only) flags 21 pending changesets that name objectql.ts, ObjectForm.tsx, WizardForm.tsx or plugin-form.mdx. I read every paragraph. None concerns submitHandler or a save payload, and all still hold. The one that did go false is the objectui#10156 entry, corrected above. check-changeset-overwrite reports that modification as its legitimate "correcting a declaration" case.
  • eslint over the 8 touched .ts/.tsx files (8 files in the JSON report, 0 errors), per rule against base content linted at the same paths. react-hooks/exhaustive-deps goes from 2 to 1 in TabbedForm.tsx and in SplitForm.tsx: objectSchema was read but not listed. The new writePayload.ts has 0 warnings. The new pin has 9 no-explicit-any, from as any fixtures in the sibling files' style. Everything else is unchanged. eslint.config.js has no type-aware linting (no parserOptions.project / projectService), so a verdict on an untouched file cannot move.
  • NOT MEASURED: pnpm check:readme-exports, reason: it needs every package built, and exits the population COLLAPSED with 25 of 40 unbuilt. The README edit is prose only, with no fenced code touched. CI builds everything and runs it.

Ablation, at b89752678

Each leg went through ablation-replace.mjs, which requires the anchor to hit once, verifies the write on disk and restores by blob with git diff HEAD empty.

  • A: SplitForm routed back to raw data. Tests 5 failed | 26 passed (31): exactly the five split payload rows. The simple control and every other route stay green.
  • B: the strip removed inside formWritePayload, with the diff kept. Tests 15 failed | 16 passed (31): the nothing-changed, create and inline-member rows on all five routes. The edit and host-seam rows stay green because the diff alone keeps unchanged refused columns off an edit. That is why the nothing-changed row exists.
  • C: the TabbedForm render gate removed. Tests 1 failed | 30 passed (31): the tabbed render row only.

Acceptance notes

  • ModalForm and DrawerForm still spell the same steps inline. They differ from the simple form in two ways: they hand the object definition to the strip whatever customFields holds, and they omit create defaults on mode === 'create' alone. They were not moved here because they are outside this card and neither difference was shown to break a save. Moving them onto formWritePayload would be a separate change.
  • objectui#10564 (MasterDetailForm) and objectui#10565 (occSave.tsx) remain open. The seat takes them serially after this lands.

Attribution: domain:ui seat #1, session session_01BA3nKVUwKQJf8DBxrSVtNC.


Generated by Claude Code

…mple form's outbound sequence

The simple form builds what a save writes in one inline sequence:
sanitizeFormData (server-owned, computed, read-only, unknown keys, and the
field-level-security verdict from fieldWriteGate), omitServerResolvedDefaults
on a create, and dirtyEditPayload against the record it read on an edit.
TabbedForm, SplitForm and WizardForm carried none of it, so an edit wrote the
whole record (id, owner_id, created_by, updated_at, formula and FLS-refused
columns) to both the host submitHandler and the OCC-guarded update, and a
create seeded with a whole record posted it back. A simple form whose mobile
stepper renders through WizardForm took the same route.

The sequence moves into one internal function, formWritePayload, which the
simple form now calls and the three layouts call in place of their raw
payload. Each layout keeps the loaded-record snapshot the simple form keeps
(snapshotLoadedRecord at its findOne, advanceLoadedRecord after a landed
write). No new export.

Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
Co-authored-by: Claude <noreply@anthropic.com>
…nd pending changeset stop exempting tabbed, split and wizard

The ObjectFormSchema.submitHandler JSDoc named the tabbed, wizard and split
layouts and the mobile stepper route as handing over every collected value;
that is false once they write through the shared sequence. The three arms'
own JSDoc copies now say what an edit-mode handler receives, as the modal and
drawer copies do. The README and the plugin-form docs page say every layout
strips and diffs. The pending objectui#10156 changeset keeps its reading of
that change and gains a dated note naming objectui#10563.

Pins: an inline-member row per route and a master-detail header laid out
tabbed. wizardSkipValidation's fixture field `owner` is renamed `assignee`:
`owner` is on the server-owned roster, which the wizard now strips as every
other layout does.

Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
Co-authored-by: Claude <noreply@anthropic.com>
…ued records

No behaviour change; drops the four no-explicit-any warnings the new module
carried.

Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
Co-authored-by: Claude <noreply@anthropic.com>
…d non-editable

ObjectForm gates a section's field OBJECTS with applyFieldPermissions before
routing to a layout, but a field a section names by bare string has no name
to ask about until the layout resolves it, so the three layouts rendered a
field the caller may read but not edit as a live input (the simple, modal and
drawer forms render it disabled). With the save now stripping that field
through formWritePayload, a value typed there would be dropped behind a 200.
Each layout now passes its resolved section fields through the same
applyFieldPermissions render pass ModalForm uses.

Pin: a render row per route, with the editable field as its control.

Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation package: types plugin tests labels Sep 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 21 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/5903-objectgantt-declared-keys.md

  • names packages/types/src/objectql.ts → packages/types/src/objectql.ts — edited by this change

    Both halves move together. The TS declaration (packages/types/src/objectql.ts) and its zod mirror (src/zod/objectql.zod.ts) gain the same ten keys at the same requiredness — all optional — and no KnownDrift entry is added. navigation is taken from @objectstack/spec's NavigationConfigSchema by reference rather than restated, matching ObjectGridSchema.navigation.

.changeset/6237-wizard-step-config-split.md

  • names WizardForm.tsx → packages/plugin-form/src/WizardForm.tsx — edited by this change

    WizardStepConfig is now declared independently in WizardForm.tsx, which is simply what SplitFormSectionConfig, ModalFormSectionConfig and DrawerFormSectionConfig already do: each layout owns its group shape, documents className / gridClassName in its own terms, and declares visibleWhen only where its renderer honours it. The derivation flips from subtractive to additive — a key is authorable on a wizard step only if someone writes it there.

  • names content/docs/plugins/plugin-form.mdx → content/docs/plugins/plugin-form.mdx — edited by this change

    Documentation repair in the same stroke: the support table in the README and in content/docs/plugins/plugin-form.mdx still said formType: 'tabbed' sections drop the predicate. That stopped being true when the tabbed arm landed — the row now reads Yes, the surrounding prose no longer claims two inert arms or a diagnostic that fires for tabbed, and the wizard row stays No, which is still exactly true.

.changeset/7322-object-kanban-group-by-limit.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    Breaking for authored metadata: ObjectKanbanSchema.groupField is RETIRED (objectui#7322, ADR-0049 enforce-or-remove), and the two keys the object-kanban renderer actually reads — groupBy and limit — are now DECLARED and validated on both published faces: the TypeScript interface in objectql.ts and the Zod mirror in zod/objectql.zod.ts.

.changeset/7363-objectql-union-arms.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    ObjectGallerySchema and ObjectDataTableSchema are members of ObjectQLComponentSchema on both faces — the TS union in objectql.ts and the zod union in zod/objectql.zod.ts — so AnyComponentSchema, and with it validateSchema / safeValidateSchema / objectui validate, has an arm for object-gallery and object-data-table nodes (objectui#7363).

.changeset/7632-shared-record-source-config.md

  • names packages/types/src/objectql.ts → packages/types/src/objectql.ts — edited by this change

    That ladder is published contract on both faces — packages/types/src/objectql.ts and its zod mirror both ship .describe() strings naming getDataConfig's order (objectui#6939, maintainer ruling 2026-09-02), pinned by objectql-record-source-refinement-6939.test.ts — and it was hand-copied into five plugin components with no gate holding them together. A change to the ruled order had five edit sites and nothing that noticed a missed one; that is the AGENTS.md #0.1 drift class.

.changeset/7804-objectql-handler-key-arms.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    The four plain objectql.ts node faces declare the nine handler keys their registered renderers read (objectui#7804, the objectql.ts slice): ObjectFormSchema.onCancel / .onError / .onOpenChange / .onStepChange / .onSuccess, ObjectGallerySchema.onCardClick / .onRowClick, ObjectGridSchema.onNavigate and ObjectViewSchema.onNavigate.

.changeset/7804-tree-view-handler-slot.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    'runtime-slot' and not 'retired', measured at this key's own channel. 'retired' publishes "no renderer reads this key, so nothing could ever run it" — true of the two siblings already tombstoned on this arm (onSelectChange, onExpandChange) and flatly false here, since the read is live and INVOKED. ⚠️ No in-repo host builds a tree-view node carrying the key: the channel is wired end to end and only the supplier is absent, which is the same shape as ObjectFormSchema.onStepChange in this card's objectql.ts slice and is not evidence of a dead read. The TypeScript declaration is unchanged and still callable, so a programmatic host supplies it exactly as before.

.changeset/7963-alert-dialog-footer-keys-retired.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    Nothing else moves. These spellings are overloaded across the tree and every other owner is a live key on a different declaration — FormSchema.cancelLabel, objectql.ts's confirmLabel, plugin-designer's ConfirmDialog React props, plugin-grid's def.confirmLabel, and plugin-form's ModalForm / DrawerForm, which build a local cancelLabel from schema.cancelText. None is an AlertDialogSchema; none is touched, and a pin asserts it. No fixture, catalog schema, example app or doc fence authored any of the three on an alert-dialog node, so no shipped document is stranded.

.changeset/8738-object-form-fields-description.md

  • names ObjectForm.tsx → packages/plugin-form/src/ObjectForm.tsx — edited by this change

    The registration declared { name: 'fields', type: 'array' } with no description, so an author had nowhere to read that this key's members are bare field names — a different vocabulary from sections[].fields, which also accepts the spec FormFieldSchema object (identity key field, e.g. { field: 'note', colSpan: 2 }). Moving one of those objects to the top-level fields resolves to no name and is skipped by SimpleObjectForm (ObjectForm.tsx) and by buildFlatFields (flatFields.ts, shared by the drawer/modal presentations). Behaviour is unchanged by this entry; it only adds the description text an author would need to avoid the drop before writing it.

.changeset/8767-object-grid-refuses-string-sort.md

  • names packages/types/src/objectql.ts → packages/types/src/objectql.ts — edited by this change

    Migration. Write the array: sort: [{ field: 'name', order: 'desc' }]. Both keys are required. SortConfig.order carries no ? in @object-ui/types (packages/types/src/objectql.ts) and no .optional() in its zod mirror, and the protocol's own reusable SortItemSchema requires order as well — measured: that schema refuses [{ field: 'name' }] with invalid_value at 0.order. Do not omit it: this block's array arm interpolates whatever is present, so an omitted order lowers to $orderby: 'name undefined' today. That is pre-existing behaviour on the arm this change does not touch, and it is filed as a successor card rather than widened into here.

.changeset/8801-object-kanban-allow-collapse-retired.md

  • names packages/types/src/objectql.ts → packages/types/src/objectql.ts — edited by this change

    • the declarations retired here — packages/types/src/objectql.ts and its mirror packages/types/src/zod/objectql.zod.ts; - the pins that assert the retirement — object-kanban-allow-collapse-retired-8801.test.ts and bare-kanban-node-key-retired-8802.test.ts; - a comment in packages/types/src/zod/complex.zod.ts, recording that the deleted retiredZeroReadKanbanKey helper once carried this spelling on the SIBLING arm; - one row of content/docs/api/schema-reference.md; - the .changeset/ release notes that discuss it — this one, the two historical entries covering the sibling arm's own spelling, and objectui#9629's note recording the correction to this paragraph.

.changeset/8885-object-chart-drilldown-title-compareto.md

  • names packages/types/src/objectql.ts → packages/types/src/objectql.ts — edited by this change

    ObjectChart.tsx reads all three off schema, and until now neither published copy declared any of them: not the TS interface (packages/types/src/objectql.ts) and not the zod mirror (packages/types/src/zod/objectql.zod.ts). They rode BaseSchema's index signature / .passthrough() and arrived unvalidated. drillDown was the sharpest case — this component's registry inputs advertise it to the designer palette, and @objectstack/spec publishes ChartDrillDownSchema for exactly this carrier, so an author was offered a key that neither published shape mentioned.

.changeset/8913-object-kanban-columns-declared.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    What moved. ObjectKanbanSchema gains columns on both halves that move together — the TypeScript interface (objectql.ts) and its Zod mirror (zod/objectql.zod.ts). Retiring the bare kanban node type key (objectui#8802) removed the only face that judged a lane, and object-kanban had never declared the key, so it rode BaseSchema's [key: string]: any / .passthrough(): read by the renderer at three sites, named by no published face.

.changeset/8990-object-kanban-groupby-optional.md

  • names packages/types/src/objectql.ts → packages/types/src/objectql.ts — edited by this change

    @objectstack/spec declares the key optional — groupBy: z.string().optional() on ObjectKanbanPropsSchema — while this package required it on the TypeScript declaration (packages/types/src/objectql.ts) and on the Zod mirror (packages/types/src/zod/objectql.zod.ts). objectui was therefore narrower than the protocol on a published key: ObjectKanbanSchema.safeParse and safeValidateSchema refused an object-kanban node the protocol accepts, and such a node could not be annotated with its own type.

.changeset/9092-inline-locale-declared-face.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    • AppComponentSchema.label (app.ts) - ObjectGridSchema.label and .description (objectql.ts) - PageNodeSchema.aria.ariaLabel (layout.ts)

.changeset/9309-object-gallery-filter-destination-typed.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    ObjectGallerySchema.filter is typed as the destination its own docblock names — QueryParams['$filter'] — on both faces, the TS interface in objectql.ts and the zod mirror in zod/objectql.zod.ts (objectui#9309).

.changeset/9511-record-id-is-a-string.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    The three authorable keys, each on BOTH faces. ObjectFormSchema.recordId (objectql.ts + zod/objectql.zod.ts), DetailViewSchema.resourceId (views.ts + zod/views.zod.ts) and DetailSchema.resourceId (crud.ts + zod/crud.zod.ts). ⚠️ The crud pair is DetailSchema, not DetailViewSchema, and it reaches the same renderer — not by symbol but by data flow: plugin-detail registers the 'detail' node type onto DetailView. A read that follows TypeScript symbols alone finds two keys and is incomplete.

.changeset/9549-tree-filter-declared.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    ObjectTreeSchema.filter is declared on both faces, in the shape objectui#9309 settled for ObjectGallerySchema.filter: QueryParams['$filter'] by indexed access on the TS interface in objectql.ts, and the same two-arm union (array first) on the zod mirror in zod/objectql.zod.ts (objectui#9549).

.changeset/9550-object-tree-root-barrel.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    ObjectQLComponentSchema declares the node types an ObjectQL block may be. Every one of its arms was a named export of this package's root barrel except ObjectTreeSchema, which was declared in objectql.ts, applied by the union, and re-exported by the ./zod barrel (objectui#7917) — while no TypeScript consumer could name it. There is no ./objectql subpath to reach around the barrel: the package's exports map is pinned by packages/types/src/__tests__/package-exports-manifest.test.ts, and the root barrel was the only route to this type.

.changeset/9606-object-kanban-card-title.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    Both published faces of the object-kanban arm now name the key: the zod mirror ObjectKanbanSchema in zod/objectql.zod.ts and its TypeScript twin, the ObjectKanbanSchema interface in objectql.ts. Both declare it OPTIONAL, at the same requiredness the other face uses, so the two faces accept and refuse the same documents. (Located and cited by SYMBOL: line addresses in zod/objectql.zod.ts have drifted before, and this change is itself about a drifted mirror.)

.changeset/9628-kanban-column-collapsed-honoured.md

  • names objectql.ts → packages/types/src/objectql.ts — edited by this change

    The key was declared on both published faces of the object-kanban arm — the lane element of ObjectKanbanSchema (objectql.ts and its Zod mirror) and the runtime lane KanbanColumn (complex.ts and its mirror) — and read by KanbanEnhanced alone, a module no production source imports. An authored { "id": "todo", "title": "To Do", "collapsed": true } therefore parsed green on both faces and reached a board that did nothing with it: KanbanImpl's only collapse is the SWIMLANE row's, held in viewer state under objectui:kanban-collapsed:ANGLE-BRACKETS(swimlaneField) and never keyed to a lane's declared value. That is the ADR-0049 declared-but-unhonoured shape.

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.

Angle-bracketed names in the quoted prose above are rewritten as ANGLE-BRACKETS(name): GitHub deletes tag-shaped fragments from a stored body, and a quote that silently loses the identifier it is about is worse than a visible repair.

Compared the checked-out tree with 402a266c9 (merge-base with origin/main): 10 file(s) changed outside .changeset/, read against 1480 pending declaration(s) that publish a body (2064 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3050.0 KB 3104.5 KB
Main entry chunk (gzip) 147.9 KB 350 KB
Entry file index-3gheNvvZ.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.57KB 6.15KB
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) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.17KB 10.58KB
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.39KB 1.21KB
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.52KB 3.45KB
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.13KB 7.95KB
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.99KB 130.83KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.91KB 62.28KB
fields (index.js) 259.05KB 65.69KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
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.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
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) 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.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.01KB 3.93KB
plugin-calendar (index.js) 51.40KB 14.61KB
plugin-charts (index.js) 74.94KB 20.89KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.50KB 35.37KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 232.96KB 61.65KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 149.07KB 38.02KB
plugin-gantt (index.js) 169.62KB 41.91KB
plugin-grid (index.js) 215.37KB 58.92KB
plugin-kanban (index.js) 48.26KB 15.04KB
plugin-list (index.js) 114.60KB 28.31KB
plugin-map (index.js) 22.42KB 7.38KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.67KB 8.95KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 87.84KB 21.96KB
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) 116.21KB 38.14KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
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) 18.27KB 6.20KB
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) 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) 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

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 25, 2026 12:49
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit e0f8202 Sep 25, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-10563-form-arms-edit-payload branch September 25, 2026 13:02
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 package: types plugin tests

Projects

None yet

1 participant