feat(spec)!: an object-grid page block types the seven members the grid reads, and resizableColumns retires for resizable (#21445) - #21463
Conversation
…tombstones resizableColumns Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
…key and D3 entries, step-18 rationale Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
…lumns tombstone, plus the tree-scoped absence pin Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
…he typed bulkActionDefs and the tombstone Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
…ess-ledger counts Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
…umns retirement Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
…ject-grid-typed-members
…heck:objectui-pin-citations Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
…ng computed, or no number Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 60e9e03a3d51e898eeaa90f514205fea20de1f86 && git checkout 60e9e03a3d51e898eeaa90f514205fea20de1f86
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 713b0fa76dbb6c0f60472cbbefd7690eedb1286e 38159d1362f38beff333f798c34530ad821598c1 && git checkout -B drift-repro 713b0fa76dbb6c0f60472cbbefd7690eedb1286e && git merge --no-ff 38159d1362f38beff333f798c34530ad821598c1
node scripts/docs-audit/affected-docs.mjs --json 713b0fa76dbb6c0f60472cbbefd7690eedb1286e
|
Contract reviewServed-tier: Isolated contract reviewer for PR #21463 (card #21445), written at 2026-10-02T20:50Z. Inputs: the card body and its four comments (triage ① Derived judgmentsThe seven typed members of
The census, re-checked. objectstack Generated artifacts.
② Semver level
Changeset, sentence by sentence: the ③ Boundary flagsDev deviations:
Implemented-by: VERDICT: PASS |
…ject-grid-typed-members
|
Regen-provenance: 5961282483 ·
|
Fixes #21445
Clause-②: yes (narrowing)
What this does
ComponentPropsMap['object-grid'](ObjectGridPropsSchema,packages/spec/src/ui/component.zod.ts) now types the seven members objectui'sObjectGridreads with a fixed shape, and retiresresizableColumnsto aretiredKey()tombstone namingresizable. Direction: triage 5958164933, accepted; claim 5958653004.Before this,
rowHeight: 42passed every door and rendered as a compact grid, and every other off-shape value was substituted or dropped with no report. The public door now answers it. Measured through the component-props gate (validateComponentProps, the ruleos validate/os build/os lintrun) on this branch:Item 1 — seven members, each measured at the
.objectui-shapin89cad75d55(plugin-grid/src/ObjectGrid.tsx)rowHeightz.unknown()RowHeightSchema, by referenceresolveRowHeightMode:1311rowColorz.unknown()RowColorConfigSchema, by referenceuseRowColor:2955 (field,colors)navigationz.unknown()NavigationConfigSchema, by reference (the carrierobject-kanban/object-calendar/object-timelinealready take)useNavigationOverlay:2894conditionalFormattingz.unknown()ListViewSchema.shape.conditionalFormatting, the list view's own memberresolveConditionalFormatting:2964bulkActionDefsz.array(z.unknown())z.array(BulkActionDefSchema), the list view's elementresolveBulkActions:4778aggregationsz.unknown()[{ field, type }],type=AggregationFunction(the query AST vocabulary, by reference)useGroupedData/useServerGroupHeaders:3095 / :3113operationsz.unknown(){ create?, update?, delete?, export? }booleans, strict;read/importrefused with guidancecreate:5468,update/delete:1898-1899,export:4088 / :5340 / :6141aggregationsandoperationshave no list-view counterpart, so they declare the measured shape in two module-private schemas (GridAggregationSchema,GridOperationsSchema). No export is added:check:api-surfaceis unchanged.rowHeight, mechanism assumption 4. The renderer's set and the schema's set match.resolveRowHeightModetests membership againstROW_HEIGHT_TO_DENSITY_MODE, which is typed as a record keyed by the spec'sRowHeight, so it admits exactly the five values. Anything else answerscompact.aggregations. The grid's vocabulary (AggregationTypeinuseGroupedData.ts) is exactlyAggregationFunction's six members. The pin holds that set, so a member added to the query AST enum reds here before it widens this door past the grid's reader.operations. These are the four members any read point names. objectui's TypeScript twin also declaresreadandimport, which nothing reads. Each is refused with that reason, not accepted as a toggle that toggles nothing.bulkActionDefsmeasured againstBulkActionDefSchema: the gap, reported and not widenedThe renderer reads members the schema lacks. Every one of these gaps already stands on the list view's door, which takes the same schema:
name,label,icon,variant,operation,execution,patch,params,confirmText,confirmLabel,visible,requiredPermissions,maxRecordsandbatchSizeare all declared.actionDefis read too (useBulkExecutor,BulkActionDialog), and the schema refuses it on purpose withguidance, because the renderer attaches it and an author does not write it.params[].bulkParamToFieldspreads every undeclared param key onto the widget's field bag, somin/max/step/accept/lookupFilters… reach a widget that reads them. The schema refuses them (BULK_PARAM_WIDGET_CONFIG_KEYS, the maintainer ruling recorded inbulk-action.zod.ts).visible. objectui'sExpressionWiretakes{ dialect?, source }withdialectoptional. The spec'sEvaluatedExpressionInputSchemaenvelope requiresdialect.params[].type. objectui types itstring, while the spec types itFieldType.conditionalFormatting: one dialect, the list view'sThe shared evaluator (
@object-ui/coreresolveConditionalFormatting) also tolerates two objectui-native rule spellings,{ field, operator, value, backgroundColor, … }and{ expression, … }. The list view declares neither, so the grid door follows the list view and refuses both. The census found no bag that writes either.Item 2 —
resizableColumnsretired (objectui#6152 ruling A)Tombstone.
resizableColumnsis aretiredKey()with the house prescription: "Useresizable. Rename the key; the value (a boolean) is unchanged." plus theos migrate meta --from 17sentence.tscrefuses it, and so does the parse.resizablestill parses.D2 conversion
object-grid-resizable-columns-removed. It is protocol 18,retiredFromLoadPath,retiredAfter17.6.0, order 57, and it followsresizable ?? resizableColumnsexactly:resizableis absent (or null), the value moves toresizableunchanged;resizableholds a value, the key strips as a lossless delete.It is scoped by component
type. The fixture covers a region grid, a both-keys grid with different values, a kanban sibling and a grid without the key (both untouched), a nested grid and a slotted grid: 4 notices.Registration.
RETIRED_KEYS_BY_MAJOR[18]carriesui/ObjectGridProps:resizableColumns, written bygen:migration-registry.object-grid-resizable-columns-retiredcarriesconversionIds, and it carries the judgment for a grid that authored both keys with different values.STEP18_RATIONALEfragments are added at orders 62 / 63.The typed members' D3 entry is
ui-object-grid-row-members-typed. It has no conversion: a page component'spropertiesis not parsed on the save or load path, and an off-shape value has no rewrite that keeps what the grid shows while honouring what the author wrote.Changeset.
@objectstack/specminor,!, BREAKING banner, a FROM → TO table, and one ADR-0087registeredmarker naming all three ids.Census of authored writers (Zone 2 assumption 3)
origin/main53fd35e3e3: the examples,packages/platform-objects, the spec tests,content/docsandskills. Zeroobject-gridblocks author any of the seven members orresizableColumns. Control: the same matcher findscolumnson both showcase grids.89cad75d55, over 335object-gridnodes, of which 76 carry apropertiesbag (23 of those in parsed JSON documents). The control:columnsis found in 46 bags.README.md"Data Grid" example,"operations": { "create": true, "read": true, "update": true, "delete": true, "export": true }. Itsread: trueis now refused.content/docs/api/schema-reference.mdalready droppedoperations.readfrom its example as a key "which nothing reads" (objectui#11068).needs_decision. The recommendation is to keep the refusal as implemented and correct the README in the objectui#6152 follow-up. The alternative is to declareread/importas two toggles that toggle nothing.resizableColumns: zero writers in either repository. objectui's own mentions at the pin are its TypeScript twin and flat mirror (deprecated / tombstoned), thedata-tablecomponent's own prop, and the?? schema.resizableColumnsread itself.Clause-② — the measured arm
check-widening-tells --declaration no --diff(final diff)check-widening-tells --declaration yes --diffyesz.unknown()bags (aggregations[].field, the fouroperationstoggles). The gate's own text rules that shape: it is "a true refusal although it only narrows … declareClause-②: yes". So the arm isyes (narrowing)although no export-listing row is added.import { … } from './view.zod'list, read as members of a closed set. They are a false positive of the matcher, noted below.Tests — at
52c4c42d72component-object-grid-typed-members.pin.test.ts(new,localproject, 52 tests).navigation(its defaults) andconditionalFormatting(the condition envelope).rowHeight: 42isinvalid_valueatrowHeight;operations.readisunrecognized_keysatoperations, with its guidance text; a no-opcustombulk def iscustomatbulkActionDefs.0.execution.AggregationFunctionset and the exactoperationskey set.resizableColumns: true|falseis refused with the prescription, alone and besideresizable.resizable: true|falseis byte-identical. A@ts-expect-errorassignment ofresizableColumnstoObjectGridPropsis consumed, so thetscchannel is proven bycheck:test-typecheckstaying green.RETIRED_KEYS_BY_MAJORwiring.object-grid-resizable-columns-retirement.test.ts(new,repoproject). This is the tree-scoped absence pin over the five roots@objectstack/specalready declares inscripts/cross-package-test-inputs.mjs, registered invitest.repo-tests.json.pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2: Test Files 602 passed (602), Tests 17769 passed | 1 todo.pnpm --filter @objectstack/spec typecheck: exit 0.check:test-typecheck: OK — 52 file(s) / 246 error(s) / 135 pinned signature(s), unchanged.repoproject. 45 of its 50 files were run green, in four targeted runs. The five not run locally arebuild-schemas-check-mode,dist-freshness,dist-freshness-adoption,publish-smoke-boot-failureandpublish-smoke-port-collision. They are script-machinery tests this diff does not touch;build-schemas-check-modealone runs 7-18 s per case and the project overruns the ~10-minute foreground cap. NOT MEASURED locally; declared to CI.Ablations: one-time proofs, each restored by
scripts/ablation-replace.mjsto blob == HEAD withgit diff HEADemptyThe pin imports
./component.zodby relative path, so it readssrc/and no build sits between the mutation and the run.rowHeightback toz.unknown()rowHeightrefusals and the identity pinresizableColumnsbecomes an undeclared key)unrecognized_keysis not the tombstone)operationsback toz.unknown()resizableColumns: falseplanted inexamples/app-showcase/src/ui/pages/my-work.page.tsThe first attempt at the plant was refused by the tool, because the replacement contained its own anchor. It wrote nothing, and it was re-run with a non-overlapping anchor.
Gates
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderives 114 families at52c4c42d72, identical to the seat's derivation. All 114 exit 0 on that head; each exit code was captured before any pipe.--ranreconciliation: "114 derived famil(ies) accounted for — 114 run, 0 NOT-MEASURED (a DERIVED zero — all 114 recorded an exit code and none of them is 3)".pnpm --filter @objectstack/spec check:generatedpasses on the final head. Two artifacts were regenerated with--fixbecause the check proved them stale:content/docs/references/ui/component.mdx;docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md(+2 strict sites).node --stack-size=4000 node_modules/eslint/bin/eslint.js --no-inline-config --format jsonran on the changed.tsfiles: 10 files, 0 errors, 0 warnings.eslint.config.mjs's**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}minusNEVER_LINTED.parserOptions.project, no typed rules), so this diff cannot move a verdict on an untouched file.pnpm lintis CI's.Acceptance notes (observations, not filed)
dropped-refinements.baseline.jsongains one site.ui/ObjectGridProps'sbulkActionDefs.elementis the list view's ownBulkActionDefSchemarefinement now reaching the grid door, and the gate printed the corrected entry.droppedRefinementSitesmoves 652 → 653.grouping.fields[]nested shape withorder?/collapsed?(their input face). This is generator output, not a hand edit.object-kanban'sconditionalFormattingis stillz.unknown(), and three other rows declarenavigation: z.unknown(). They are named for the family's closure card in the dev report.check-widening-tellsT2 false positive. A name added to a multi-lineimport { … }list is read as a new member of a closed set. It is named in the dev report.resizableColumnsmember becomesnever, the flat mirror gets a tombstone, and the?? schema.resizableColumnsread is dropped;operations.readcomes out of its example;operations.read/.importgo;conditionalFormattingtyping converges on the spec rule.Generated by Claude Code