feat(types,fields)!: camelCase the grid widget's eight field-level keys; the snake_case spellings are refused by name (objectui#11610) - #11614
Conversation
…eld-level keys; the snake_case spellings are refused by name (objectui#11610) `GridFieldMetadata`, the form-field face and its zod mirror now declare `minRows`, `maxRows`, `allowAdd`, `allowDelete`, `allowReorder`, `totalField`, `addLabel` and `sortField`. The retired snake_case spellings stay declared only to refuse: `?: never` tombstones on both TS faces, an alias refusal naming the camelCase key on the zod mirror, and a named refusal the `grid` widget draws instead of the grid. One list, `GRID_FIELD_RETIRED_KEYS`, feeds the zod arms and the widget. Both plugin-form adapters, the console's dev harness, the catalog fixture, the grid doc page and the clipboard-paste RFC move to the new spellings; the adapters' grid objects are now checked against `GridFieldMetadata` instead of cast through `any`. Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude <noreply@anthropic.com>
…aws its camelCase keys through the real form path; declare the rename (objectui#11610) Claude-Session: https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Reviewed from the card (#11610: body, triage grade ① Derived judgments1. The rename of the eight 2. The new 3. The retirement form on each face — right, and it is a refusal by name on all three, never a strip, never a conversion.
4. No dual read — right. Every 5. The stored fixture migration — right. 6. The
7. The premise (no stored producer outside objectui's fixtures) — holds, on what I re-measured. Gate verdicts (the check-runs on this head, read three times):
② Semver levelLevel — right for this repository. The changeset declares
The changeset names ③ Boundary flagsDev
Dev Flags from this review: Implemented-by: VERDICT: PASS Generated by Claude Code |
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Second record on this PR, for the head that merged What moved. One commit, Byte-identity, per file. I normalised the hunk headers and the ① Derived judgments1. The rename, the by-reference members, the map export, the refusal on each face, the no-dual-read reading, the fixture migration, the 2. The
3. No test or fixture this branch adds authors a snake_case or undeclared key on a fresh literal whose type #11607 closed. Inventory of every fresh literal in the diff that carries a snake_case or undeclared key, with the type it is written against:
4. 5. No comment this branch wrote describes 6. The main-side hunks of the four shared files touch nothing this branch relies on.
7. The Gate verdicts (the check-runs on this head, every run naming
② Semver levelUnchanged and still right. The changeset
What the merge adds on this axis: ③ Boundary flagsDev
Dev The earlier record's escalations: Raised by the merge: Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #11610
Clause-②: yes (narrowing)
The
gridwidget's eight field-level keys become camelCase in one move across the published type, its zod mirror, the form-field face and every reader. The snake_case spellings retire at once and are refused by name on all three faces, each refusal naming the camelCase key. Ground: objectstack-ai/objectstack#21704 fork 2, ruled B (record5978663135); claim5979906990(seat ruling: refuse by name, no load-time conversion, premised on no stored producer). Dispatch sessionsession_01CPvhwGcirXqBGEdPSb72TZ,mode:subagent.Premise (measured before any edit): no stored producer outside objectui's own fixtures
origin/main316be321(read throughgit grepon a private ref, nothing edited): the eight snake_case words occur only in comments, test strings and one changeset line.packages/speccomponent.zod.tsnames them in three comments (the line-items guidance block, thecustomFieldsfork note, the master-detailsortFieldretirement note); the retirement test fordetails.sortFieldcarries one as a must-not-match judge string;conversions/registry.ts,migrations/registry.tsand the retired-key entry fordetails.sortFieldcarry it in comments. No schema declares or emits any of the eight as a field's metadata, and the spec'sFieldTypeenum has nogridmember at all, so the spec cannot store agridfield. The seat's reading is confirmed.b508ac50(every tracked file,git grepon that commit), each hit classified:GridField.tsx(the eight reads).MasterDetailFormandLineItemsPanel(both hand the grid an object cast throughany), andapps/consoleDevLookup(a dev harness).GridFieldMetadata, the form-field face (FormField), its zod mirror (FormFieldSchema).examples/schema-catalogfields-grid/line-items-grid.json(the only stored document).content/docs/fields/grid.mdx; the clipboard-paste RFC (statesmax_rows/allow_addas GridFieldMetadata's API in its proposed wiring); ADR 0001 (reported below, not edited).GridFieldtest files inpackages/fields(the triage counted five), the master-detail members test and the line-items total test inpackages/plugin-form, threepackages/typestests (strict face, form-field coverage, mirror parity comment), the console registry pin's prose.## MAX_ROWS vs MIN_ROWSheading-anchor string inplugin-markdown's toc test; the en locale comment (a comment about the grid's label, updated); everyCHANGELOG.md(history, untouched). The master-detaildetails.sortFieldthe spec retired is a different key on a different object and stays untouched.customFieldsentries) and the triage's hotcrm / cloud reading (0 hits).Rename map
min_rowsminRowsmax_rowsmaxRowsallow_addallowAddallow_deleteallowDeleteallow_reorderallowReordertotal_fieldtotalFieldadd_labeladdLabelsort_fieldsortFieldOne list holds it:
GRID_FIELD_RETIRED_KEYS(new export of@object-ui/types, with its key typeGridFieldRetiredKey). The zod arms and the widget both read it; a type-level pin holds its keys equal to thenevertombstones and its values equal to the live camelCase members.The retirement form, per face (refused by name, never stripped)
GridFieldMetadataandFormFieldeach declare the eight snake_case keys asnevertombstones with a@deprecatednote naming the replacement.{ type: 'grid', name: 'lines', min_rows: 1 }typedGridFieldMetadatadoes not compile; on the docs face the gate's compile ofgrid.mdxreportsTS2322: Type 'number' is not assignable to type 'undefined'for the same key.@object-ui/types/zod).FormFieldSchemadeclares each snake_case key as analiasKeyRefusalarm. On the mirror, the tolerant face (safeValidateSchema, theobjectui validatedoor) and the strict authoring face,fields[0].min_rows: 1answers exactly one issue,invalid_typeatfields.0.min_rows, whose message leadsUnrecognized key(s) on this form field: `min_rows`. Did you mean `min_rows` → `minRows`?and goes on to say the value stays the same. Any value is refused (the refusal is by name). The camelCase key with the same value parses and is kept on all three doors.@object-ui/fields).GridFieldis now a thin gate over the grid body: a field whose metadata carries any snake_case key (with a defined value) is drawn asRetiredGridFieldKeysinstead of the grid, an inlinerole="alert"(data-testid="grid-field-retired-keys") reading for example[object-ui] Grid field `lines` carries retired snake_case key(s): `min_rows` → `minRows`. …, plus the same text onconsole.erroronce per message. This isRetiredFieldTombstone's settled shape in this package: nothing thrown, nothing silently substituted, rows untouched (onChangeis never called).Producers move with it: both plugin-form adapters write the camelCase keys, and their grid object is now
satisfiesPartial ofGridFieldMetadatainstead ofas any, so a snake_case key written there no longer compiles.Pins and guards, with ablation readings
All ablations ran through the anchored mutation tool (anchor must hit; blob change and restore proven on disk; restore leg
git checkout HEAD, blob after restore equal to the HEAD blob,git diff HEADempty), from committed state.GridFieldwith a lit control without the key (GridField.retiredSnakeKeys-11610.test.tsx):minRowsdisables every Remove at the minimum;maxRowsdisables Add and every Duplicate at the maximum;allowAdd: falseremoves Add and Duplicate;allowDelete: falseremoves Remove;allowReorder: falseremoves the drag handles;totalFielddraws the footer total (30);addLabellabels Add;sortFieldstamps the new indices after a drag. Through the real form path too:fields-grid-keys-camelcase-11610.test.tsxrenders the catalog fixture throughSchemaRendererand the form renderer (labelAdd line item, total109.97, two drag handles / Duplicate / Remove, all enabled), with the snake_case respelling as its control.console.erroris called once with the alert's text; edges pinned: several keys in one alert in map order, a snake key beside its camelCase twin is still refused, anundefinedvalue is not refused (as on the TS and zod faces), a nameless field reads "This grid field", a second mount logs nothing new.undefinedrow) green. Restored.grid-field-keys-camelcase-11610.test.ts, three doors × eight keys, plus lit controls and an any-value row).min_rowsarm): 8 tests red (themin_rowsrow and the any-value row on each of the three doors, and bothform-field-zod-coveragerows). Restored.tsc -p tsconfig.test.json): anEqualrow per member on both faces, akeyofmembership row onFormField, and the map-equals-tombstones rows.GridFieldMetadatatombstones): the eightEqualrows red,TS2339naming each key; the two map rows red; the strict-face lit-control row red; the map'ssatisfiesred (TS2561).@ts-expect-errorrows over freshGridFieldMetadataliterals stayed SILENT: with the member gone the key is still an excess property and the directive swallows it. That is the objectui#8347 lesson, reproduced; theEqualrows are what catch the deletion.total_fieldalone): exactly itsEqualrow (TS2339), the map row and thesatisfiesred.FormFieldtombstones): the eightEqualrows and thekeyofrow red, fourTS2578unused@ts-expect-error(the index signature now accepts the key), the strict-face by-reference rows red, and the mirror-parity ratchet red onFormFieldSchema.sort_fieldalone): itsEqualrow, thekeyofrow, itsTS2578, the strict-face rows and the parity ratchet red, namingsort_field.tsc -p tsconfig.test.jsonruns are in the gates below.grid.mdx's snippet snake_case turnspnpm check:doc-snippetsred at that line (TS2322), restored green.Repo-wide pin sweep: every assertion that read a grid key by its snake_case spelling now asserts the camelCase key's substance (the master-detail grid object's exact key set and values, the strict face's accept and wrong-type rows, the coverage key set, the widget draws). The rejection rows for genuinely wrong values (a string
minRows, a numericaddLabel, …) stay, respelled; the guarded surface grows by the eight refusals.Files and landing point
The claim's surface, plus two adjacent files the census named:
apps/consoleDevLookup.tsx(a producer) and its registry pin's prose;packages/i18nen locale (one comment namingadd_label); the RFC. The landing point is the claim's list; no producer was found in another package.ADR and RFC
docs/adr/0001-master-detail-subform.md(governed, ⛔ not edited) names the keys in three sentences: "Honorsmin_rows/max_rows/allow_add/allow_deletefrom the …", "Drag reorder of lines (allow_reorder), grouping/subtotal rows." and "… drag-to-reorder (asort_field…". Reported for whoever owns that record.content/docs/rfcs/0001-clipboard-paste.md(status Draft) statedmax_rows"from GridFieldMetadata" and wiredfield.allow_add/field.max_rowsas the current API, so it is edited to the camelCase keys.Serial and overlap
The claim's serial step is done. objectui#11605's PR #11612 (
fd060f07) and objectui#8347's PR #11607 (b403bb36) both landed.mainatb403bb36is merged into this branch by merge commit8b56ef26. Its combined diff is empty, so no hunk was hand-resolved.The four files this branch shares with them carry
main's hunks outside this branch's hunks:packages/i18n/src/locales/en.ts,packages/types/src/form.ts,packages/types/src/index.tsandzod-mirror-parity.test.ts. The contract review on the merged head (5981193754) reads every hunk of this branch as byte-identical to the reviewed head. The gate verdict on the merged head is its own check-runs.Gates (the dev's local readings, tree
82b400f9; the current head's verdict is its check-runs)@object-ui/types,@object-ui/fields,@object-ui/plugin-form,@object-ui/i18n,@object-ui/example-schema-catalog,@object-ui/console— all exit 0 (the console run after building@object-ui/plugin-tree, which its import graph needs). The fields and plugin-form test programs list the touched test files (--listFilesOnly).packages/types/353 files / 9482 tests passed;packages/fields/233 passed + 1 skipped / 3647 passed + 7 skipped;packages/plugin-form/163 / 1889 passed + 1 skipped;examples/schema-catalog/+ the console registry pin +packages/i18n/121 / 3807 passed + 13 skipped. Thetypes/fields/plugin-formruns measured a working tree byte-identical to82b400f9(its two later-committed files, the changeset and the catalog test, were already on disk); the rest ran at82b400f9.eslintwith the root config, as each package'slintruns it) over the 25 touched TS/TSX files, counted from--format json: 0 errors; the one finding on a touched line is the pre-existingas anywarning inGridField.test.tsx's fixture. Narrowing statement: the population is every file under the touched packages, read by the one rooteslint.config.js; it enables no type-aware linting and no custom rule reads the filesystem, so this diff cannot move a verdict on an untouched file.pnpm lintitself is CI's.check:doc-snippets(777 blocks judged, 0 failed),check:doc-examples,check:readme-exports,check:new-line-citations(0 new),check:control-bytes,check:spec-symbols,check:designer-field-key-parity,check:prompt-keys,check:handler-key-reads,check:unreferenced-sources,check:phantom-deps,check:self-import,check:test-path-roots,check:doc-types,check:doc-example-ids,check:doc-fences,check:doc-example-readers,check:i18n-drift,check:i18n-keys,check:esm-specifiers,check:side-effects-array: all exit 0..changeset/11610-grid-keys-camelcase.md:@object-ui/typesand@object-ui/fieldsminor, breaking banner, FROM → TO for all eight,Clause-②: yes (narrowing).node scripts/check-governed-queue-guard.mjs --testover the diff answers NOT GOVERNED.Acceptance notes
totalFieldnames the CHILD column summed (the value a specamountFieldcarries), while the spec's owntotalFieldon a master-detail subform orrecord:line_itemsnames the PARENT field the sum is saved to. The mechanical camelCase oftotal_fieldlands on that homonym. Every declaration, the doc page and the changeset say so; the open question in the report asks the seat whether the spec's runtime form field should carry it under this name.drillDown.report, object-formcustomFields, formsections, object-timelineitems,action:group/action:menumembers objectstack#21704), the three objectstack comments that still name the snake_case keys can move with it. Carrier: that stage.DevLookup.tsxkeeps itsas anyfield (a dev harness; only the key moved).The dispatching session is
https://claude.ai/code/session_01CPvhwGcirXqBGEdPSb72TZ.Generated by Claude Code