fix(app-shell): a number column in an objectList config field, so a screen field's Min / Max save as numbers (objectui#11664) - #11670
Merged
objectstack-fleet[bot] merged 9 commits intoOct 6, 2026
Conversation
…, so a screen field's Min / Max save as numbers (objectui#11664) `columnsFor` mapped a `type: 'number'` / `'integer'` item property to a text cell, and `FlowObjectListField` stringified a stored number on read and committed the trimmed string on save. A screen field's `min` / `max` therefore landed as strings the screen contract (`ScreenFieldConfigSchema`, `z.number()`) refuses, both when authored in the designer and when a code-authored row was re-saved. - `FlowConfigColumn['kind']` gains `number`; `columnsFor` reuses the top-level `scalarField` mapping to reach it. - The number cell is the `Input type="number"` the top-level number field renders, flushed on blur like the text cell. A stored number reads and commits as a number (`0` included); an empty cell commits no key; a string stored there is kept verbatim, never coerced. Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
…e engine screen descriptor (objectui#11664) Mounts the real FlowNodeInspector on the screen descriptor's configSchema (transcribed from objectstack screen-nodes.ts) and judges every saved fields list with the installed ScreenConfigSchema: authored Min 1 / Max 10 save as numbers, a code-authored `min: 0` survives a re-save of another column, a typed 0 commits 0, an emptied cell drops the key, a non-numeric entry never commits a string, a stored string is kept verbatim, and a text column's numeric-looking string stays a string. Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
… (objectui#11664) Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
…bjectui#11664) Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
…t was built, so a re-save keeps a code-authored Min / Max (objectui#11664) Measured in a live console against objectstack main: the inspector renders the hand-written fallback columns until the engine's configSchema answers, then the engine's. FlowObjectListField built its rows against the first set, which has no `min` / `max` column, and re-derived them only when the VALUE changed, so editing any cell of a code-authored screen field dropped its stored `min: 0` / `max: 5` (and every other engine-only key) on save. Each row now keeps the stored item it was read from. When the column set changes, a column the rows do not hold yet, or one whose kind changed, reads its cell from that item; every other cell, unflushed typing included, is kept, and nothing is committed until the author's next edit. The effect is keyed on the columns' keys and kinds, not on the array's identity. The screen pins now run on the real descriptor hook with a late answer, in the order the console sees. Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
…er column changeset (objectui#11664) Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
… (objectui#11664) Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
… live rows (objectui#11664) Claude-Session: https://claude.ai/code/session_015W8GBu6sBiqus2L2xjMsAL Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contributor
Author
ACCEPT: PR objectui#11670, head
|
objectstack-fleet
Bot
deleted the
claude/issue-11664-objectlist-number-column
branch
October 6, 2026 04:38
This was referenced Oct 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #11664
Clause-②: no
What changes
The flow designer edits a screen node's
fieldsas an object list, one row per screen field. The engine's screen descriptor publishes the item propertiesminandmaxastype: 'number', but the list had no number column, so both were text cells. Authoring Min 1 / Max 10 savedmin: '1',max: '10', and every run of the flow then failed at the screen node againstScreenFieldConfigSchema(z.number()). Triage's direction (comment 6007030030) is applied as ruled:FlowConfigColumn['kind']gainsnumber.columnsFormaps anumber/integeritem property to it by reusing the top-levelscalarFieldmapping, so there is no second number detector. An enum still wins and stays a select, as at the top level.Input type="number"control the top-level number field renders, flushed on blur like the text cell beside it. A typed0commits0. An emptied cell commits no key at all: not'',nullorNaN. An entry the browser cannot read as a number commits nothing, as in the top-level number field.toRowsandrowsToListkeep a stored number a number. A string already stored in a number column is kept verbatim until the author types over it. Stored data is not coerced, and the screen contract still refuses that string. Other columns' strings are unchanged.A second reason pin 2 failed, found in the live console
Driving the real designer against an objectstack
mainbackend showed that triage's pin 2 (re-saving a code-authored screen withmin: 0keeps0) failed for a second reason, which neither the card nor a synchronous unit test could see. The inspector renders the hand-written fallback columns untilGET /api/v1/automation/actionsanswers, then the engine's columns.FlowObjectListFieldbuilt its rows against the first set, which has nomin/maxcolumn, and rebuilt them only when the value changed. So editing any cell of the first selected screen field saved it withoutminandmax, and withoutoptions,defaultValue,placeholder,inlineHelpTextandreference. This happens onmaintoo: there the bounds were dropped, not turned into strings.The fix is in the same file, inside the claimed surface. Each row keeps the stored item it was read from. When the column set changes, a column the rows do not hold yet, or one whose kind changed, reads its cell from that item. Every other cell is kept, including unflushed typing. Nothing is written when the schema arrives; the author's next edit writes. The effect is keyed on a string of the columns' keys and kinds, not on the array's identity (AGENTS.md #10). The screen pins now run on the real descriptor hook with a late answer, in the order the console sees.
Tests
Final head
d8002feunless a row says otherwise. Each exit code was written to a file as it ran.pnpm exec vitest runon the newFlowNodeInspector.screenNumberBounds-11664.test.tsx, the fourFlowObjectListField.*suites andjson-schema-to-fields.test.tspnpm exec vitest related --runonFlowObjectListField.tsxpnpm exec vitest run packages/app-shell/at14f9b48(before the merge)91d45ca(after mergingmainf9f4a62)turbo run build --filter='@object-ui/app-shell^...', thenpnpm --filter @object-ui/app-shell type-check/lint/buildtype-checkrunstsconfig.test.json, which includessrc/**/*.test.tsx.lint: 0 errors. The one warning inFlowObjectListField.tsx(react-hooks/refsat the rows map) is also onmain.check:control-bytes,check:new-line-citations,check:changeset-claims,check:pending-changeset-literals,check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape,check:test-path-roots,check:spec-symbols,check:designer-field-key-parity,check:i18n-keys,check:i18n-designer-parity,check:installed-pin-claims,check:unreferenced-sources,check:action-forward-parity,check:handler-key-reads,check:shell-escape-residue,check-changeset-presence.mjs,check-changeset-no-major.mjsThe final suite was narrowed, and the narrowing is declared here. After
91d45cathe commits touch onlyFlowObjectListField.tsx, its test file and the changeset.vitest relatedselects every test file whose import graph reaches that file. CI runs the full suite.Ablations. Each behaviour this PR adds was ablated at
d8002fewith objectstack'sscripts/ablation-replace.mjsin WRAP mode. Every leg landed on disk with its anchor going from 1 to 0, and every restore was proven: the blob equalsHEADandgit diff HEADis empty. Every leg turned the pinned file red:columnsFornumber mappingNumber()instead of keptrowsToListcommits a numberrowsToListkeeps a stored stringtype="number"Live check. The console ran from this worktree (vite, port 5216) against objectstack
mainfaf8dce482(the showcase in its own worktree with--fresh, port 4116). The browser was Playwright Chromium at/opt/pw-browsers/chromium, signed in as the seeded admin with the session cookie. A flowstart→screen→endwas seeded throughPUT /api/v1/meta/flow/NAME. The screen node was edited in the designer and published, the flow was read back through the API and run withPOST /api/v1/automation/NAME/trigger. For the before leg, the three source files were swapped to theirfc3c2ccbytes and restored afterwards, with the restore proven.min: 0,max: 5, Label edited"min":"1","max":"10"; trigger answers 400FLOW_FAILED: "screen 'ask': config does not satisfy the screen contract — config.fields[0].min: Invalid input: expected number, received string; config.fields[0].max: …"min/max; trigger pauses at the screen with no bounds"min":1,"max":10; trigger pauses at screenaskwithmin: 1, max: 10"min":0,"max":5; trigger pauses withmin: 0, max: 5The
configSchemathis backend served forscreenequals the test's transcribedSCREEN_CONFIG_SCHEMA, compared as canonical JSON.Clause-② fence. On the final build, a walk of relative imports from
packages/app-shell/dist/index.d.tsreaches 164 declaration files, 22 of them underviews/metadata-admin. None isflow-node-config.d.ts,FlowObjectListField.d.ts,json-schema-to-fields.d.tsor the inspectors'index.d.ts. The builtflow-node-config.d.tscarries the new'number'member. No export, entry-reachable prop or type member, or language-pack key is added.Acceptance notes
InspectorNumberField, is a label above anInput type="number"that commits on every keystroke. The cell reuses that input and its empty-commits-nothing rule, but not the wrapper: the wrapper's label would duplicate the row label, and its per-keystroke commits differ from the row's flush on blur. No i18n key is added; the placeholder is the column's schema description, as for every other cell.0is0. A browser number input reports''for an entry it cannot read, so that entry commits nothing; typed over a stored number, it removes that number on blur, exactly as the top-level field does.'1'shows as 1; a non-numeric one shows a blank box. The row keeps the string either way until typed over (pinned).fieldscolumns (Name, Label, Type, Required, Visible when) but nomin/max, so it has no number cell.configSchemais served, a re-save of a screen'sfieldsdrops every key the hand-written columns do not list:min,max,options,defaultValue,placeholder,inlineHelpText,reference. Measured with a scratch test at14f9b48. A backend that publishes the screen descriptor leaves that table once its answer lands. Carrier: none.options[].valueanddefaultValuewith notype, so they are text columns, and a stored number there still saves as a string throughString(v). Both arez.unknown()in the spec, so nothing refuses the string. This is outside the ruling, which covers number columns. Carrier: none.91d45ca,AppContent.deniedVsUnpublished.test.tsx("that way back follows the DECLARED landing") failed once on a network-escape guard: reads of/api/v1/meta,/meta/packageand/meta/diagnosticsunder full parallel load. Alone it passes 15/15. This diff does not touch that file.main'sf9f4a62changedHomePage.tsx. Carrier: none.Generated by Claude Code