Skip to content

feat(spec)!: an object-grid page block types the seven members the grid reads, and resizableColumns retires for resizable (#21445) - #21463

Merged
objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-21445-object-grid-typed-members
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-21445-object-grid-typed-members

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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's ObjectGrid reads with a fixed shape, and retires resizableColumns to a retiredKey() tombstone naming resizable. Direction: triage 5958164933, accepted; claim 5958653004.

Before this, rowHeight: 42 passed 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 rule os validate / os build / os lint run) on this branch:

component-props-invalid      properties.rowHeight          rowHeight: Invalid option: expected one of "compact"|"short"|"medium"|"tall"|"extra_tall" (received 42)
component-props-invalid      properties.resizableColumns   `object-grid` property `resizableColumns` was removed in @objectstack/spec 17.7.0 (ADR-0049) — ...
component-props-unknown-key  properties.operations.read    Unrecognized key(s) on this `object-grid` operations block: `read`.

Item 1 — seven members, each measured at the .objectui-sha pin 89cad75d55 (plugin-grid/src/ObjectGrid.tsx)

member was now read point at the pin
rowHeight z.unknown() RowHeightSchema, by reference resolveRowHeightMode :1311
rowColor z.unknown() RowColorConfigSchema, by reference useRowColor :2955 (field, colors)
navigation z.unknown() NavigationConfigSchema, by reference (the carrier object-kanban / object-calendar / object-timeline already take) useNavigationOverlay :2894
conditionalFormatting z.unknown() ListViewSchema.shape.conditionalFormatting, the list view's own member resolveConditionalFormatting :2964
bulkActionDefs z.array(z.unknown()) z.array(BulkActionDefSchema), the list view's element resolveBulkActions :4778
aggregations z.unknown() [{ field, type }], type = AggregationFunction (the query AST vocabulary, by reference) useGroupedData / useServerGroupHeaders :3095 / :3113
operations z.unknown() { create?, update?, delete?, export? } booleans, strict; read / import refused with guidance create :5468, update / delete :1898-1899, export :4088 / :5340 / :6141
  • aggregations and operations have no list-view counterpart, so they declare the measured shape in two module-private schemas (GridAggregationSchema, GridOperationsSchema). No export is added: check:api-surface is unchanged.
  • rowHeight, mechanism assumption 4. The renderer's set and the schema's set match. resolveRowHeightMode tests membership against ROW_HEIGHT_TO_DENSITY_MODE, which is typed as a record keyed by the spec's RowHeight, so it admits exactly the five values. Anything else answers compact.
  • aggregations. The grid's vocabulary (AggregationType in useGroupedData.ts) is exactly AggregationFunction'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 declares read and import, which nothing reads. Each is refused with that reason, not accepted as a toggle that toggles nothing.

bulkActionDefs measured against BulkActionDefSchema: the gap, reported and not widened

The renderer reads members the schema lacks. Every one of these gaps already stands on the list view's door, which takes the same schema:

  • Def level. name, label, icon, variant, operation, execution, patch, params, confirmText, confirmLabel, visible, requiredPermissions, maxRecords and batchSize are all declared. actionDef is read too (useBulkExecutor, BulkActionDialog), and the schema refuses it on purpose with guidance, because the renderer attaches it and an author does not write it.
  • params[]. bulkParamToField spreads every undeclared param key onto the widget's field bag, so min / max / step / accept / lookupFilters … reach a widget that reads them. The schema refuses them (BULK_PARAM_WIDGET_CONFIG_KEYS, the maintainer ruling recorded in bulk-action.zod.ts).
  • visible. objectui's ExpressionWire takes { dialect?, source } with dialect optional. The spec's EvaluatedExpressionInputSchema envelope requires dialect.
  • params[].type. objectui types it string, while the spec types it FieldType.

conditionalFormatting: one dialect, the list view's

The shared evaluator (@object-ui/core resolveConditionalFormatting) 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 — resizableColumns retired (objectui#6152 ruling A)

  • Tombstone. resizableColumns is a retiredKey() with the house prescription: "Use resizable. Rename the key; the value (a boolean) is unchanged." plus the os migrate meta --from 17 sentence. tsc refuses it, and so does the parse. resizable still parses.

  • D2 conversion object-grid-resizable-columns-removed. It is protocol 18, retiredFromLoadPath, retiredAfter 17.6.0, order 57, and it follows resizable ?? resizableColumns exactly:

    • when resizable is absent (or null), the value moves to resizable unchanged;
    • when resizable holds 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] carries ui/ObjectGridProps:resizableColumns, written by gen:migration-registry.
    • The D3 entry object-grid-resizable-columns-retired carries conversionIds, and it carries the judgment for a grid that authored both keys with different values.
    • Two STEP18_RATIONALE fragments 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's properties is 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/spec minor, !, BREAKING banner, a FROM → TO table, and one ADR-0087 registered marker naming all three ids.

Census of authored writers (Zone 2 assumption 3)

  • objectstack origin/main 53fd35e3e3: the examples, packages/platform-objects, the spec tests, content/docs and skills. Zero object-grid blocks author any of the seven members or resizableColumns. Control: the same matcher finds columns on both showcase grids.
  • objectui at the pin 89cad75d55, over 335 object-grid nodes, of which 76 carry a properties bag (23 of those in parsed JSON documents). The control: columns is found in 46 bags.
    • The one value a new shape refuses is objectui's root README.md "Data Grid" example, "operations": { "create": true, "read": true, "update": true, "delete": true, "export": true }. Its read: true is now refused.
    • objectui's own content/docs/api/schema-reference.md already dropped operations.read from its example as a key "which nothing reads" (objectui#11068).
    • This producer is named in the dev report as a 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 declare read / import as 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), the data-table component's own prop, and the ?? schema.resizableColumns read itself.

Clause-② — the measured arm

run exit reading
check-widening-tells --declaration no --diff (final diff) 4 8 tells, below
check-widening-tells --declaration yes --diff 0 the gate never blocks a yes
  • T1, the bulk of the tells. These fire on the new keys inside the two former z.unknown() bags (aggregations[].field, the four operations toggles). The gate's own text rules that shape: it is "a true refusal although it only narrows … declare Clause-②: yes". So the arm is yes (narrowing) although no export-listing row is added.
  • T2, three tells. These fire on the three new names in the multi-line import { … } from './view.zod' list, read as members of a closed set. They are a false positive of the matcher, noted below.

Tests — at 52c4c42d72

  • component-object-grid-typed-members.pin.test.ts (new, local project, 52 tests).
    • §1: each declared shape parses. It parses byte-identical, or to exactly what the shared schema answers for navigation (its defaults) and conditionalFormatting (the condition envelope).
    • §2: each off-shape value is refused, with the code AND the path. rowHeight: 42 is invalid_value at rowHeight; operations.read is unrecognized_keys at operations, with its guidance text; a no-op custom bulk def is custom at bulkActionDefs.0.execution.
    • §3: identity of each by-reference def, plus the exact AggregationFunction set and the exact operations key set.
    • §4: the tombstone. resizableColumns: true|false is refused with the prescription, alone and beside resizable. resizable: true|false is byte-identical. A @ts-expect-error assignment of resizableColumns to ObjectGridProps is consumed, so the tsc channel is proven by check:test-typecheck staying green.
    • §5: the conversion's two arms and the untouched grid, and the step-18 / RETIRED_KEYS_BY_MAJOR wiring.
  • object-grid-resizable-columns-retirement.test.ts (new, repo project). This is the tree-scoped absence pin over the five roots @objectstack/spec already declares in scripts/cross-package-test-inputs.mjs, registered in vitest.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.
  • repo project. 45 of its 50 files were run green, in four targeted runs. The five not run locally are build-schemas-check-mode, dist-freshness, dist-freshness-adoption, publish-smoke-boot-failure and publish-smoke-port-collision. They are script-machinery tests this diff does not touch; build-schemas-check-mode alone 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.mjs to blob == HEAD with git diff HEAD empty

The pin imports ./component.zod by relative path, so it reads src/ and no build sits between the mutation and the run.

mutation result
rowHeight back to z.unknown() 3 failed / 49 passed: both rowHeight refusals and the identity pin
tombstone key renamed away (resizableColumns becomes an undeclared key) 3 failed: all three prescription pins (a bare unrecognized_keys is not the tombstone)
operations back to z.unknown() 6 failed: four refusals, the guidance-text pin and the key-set pin
a resizableColumns: false planted in examples/app-showcase/src/ui/pages/my-work.page.ts the absence pin fails, naming that file

The 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 --commands derives 114 families at 52c4c42d72, identical to the seat's derivation. All 114 exit 0 on that head; each exit code was captured before any pipe.
  • --ran reconciliation: "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:generated passes on the final head. Two artifacts were regenerated with --fix because 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).
  • Lint, a declared narrowing. node --stack-size=4000 node_modules/eslint/bin/eslint.js --no-inline-config --format json ran on the changed .ts files: 10 files, 0 errors, 0 warnings.
    • Population: eslint.config.mjs's **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} minus NEVER_LINTED.
    • Invariance: the config never enables type-aware linting (no parserOptions.project, no typed rules), so this diff cannot move a verdict on an untouched file.
    • The full pnpm lint is CI's.

Acceptance notes (observations, not filed)

  • dropped-refinements.baseline.json gains one site. ui/ObjectGridProps's bulkActionDefs.element is the list view's own BulkActionDefSchema refinement now reaching the grid door, and the gate printed the corrected entry. droppedRefinementSites moves 652 → 653.
  • The regenerated reference page now renders the shared grouping.fields[] nested shape with order? / collapsed? (their input face). This is generator output, not a hand edit.
  • Same family, other rows, not touched here. object-kanban's conditionalFormatting is still z.unknown(), and three other rows declare navigation: z.unknown(). They are named for the family's closure card in the dev report.
  • check-widening-tells T2 false positive. A name added to a multi-line import { … } list is read as a new member of a closed set. It is named in the dev report.
  • objectui follow-up (objectui#6152), once a release carrying this is installable:
    • the TypeScript resizableColumns member becomes never, the flat mirror gets a tombstone, and the ?? schema.resizableColumns read is dropped;
    • the README's operations.read comes out of its example;
    • the twin's operations.read / .import go;
    • the grid's two-dialect conditionalFormatting typing converges on the spec rule.

Generated by Claude Code

claude added 10 commits October 2, 2026 18:35
…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>
…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>
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 17 documentable anchor(s). ⚠️ 4 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json, packages/spec/dropped-refinements.baseline.json, packages/spec/src/migrations/entries/retired-keys/18.ui__ObjectGridProps__resizableColumns.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via crm_account (literal, a string literal in fixture))
  • content/docs/data-modeling/object-extensions.mdx (via crm_account (literal, a string literal in fixture))
  • content/docs/deployment/cli.mdx (via crm_contact (literal, a string literal in fixture))
  • content/docs/deployment/validating-metadata.mdx (via crm_account (literal, a string literal in fixture))
  • content/docs/getting-started/examples.mdx (via crm_account (literal, a string literal in fixture))
  • content/docs/kernel/runtime-services/data-service.mdx (via crm_account (literal, a string literal in fixture))
  • content/docs/kernel/runtime-services/examples.mdx (via crm_account (literal, a string literal in fixture))
  • content/docs/permissions/system-context.mdx (via crm_account (literal, a string literal in fixture))
  • content/docs/protocol/kernel/i18n-standard.mdx (via crm_account (literal, a string literal in fixture))
  • content/docs/ui/actions.mdx (via crm_account (literal, a string literal in fixture))
  • content/docs/ui/doc-pages.mdx (via crm_account (literal, a string literal in fixture), crm_contact (literal, a string literal in fixture))
  • content/docs/ui/index.mdx (via crm_account (literal, a string literal in fixture))
  • content/docs/ui/translations.mdx (via crm_account (literal, a string literal in fixture))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v13.mdx (via crm_account (literal, a string literal in fixture))
  • content/docs/releases/v17/17-1.mdx (via crm_account (literal, a string literal in fixture))
  • content/docs/releases/v17/17-5.mdx (via retiredAfter (symbol, a field of const object objectGridResizableColumnsRemoved), retiredFromLoadPath (symbol, a field of const object objectGridResizableColumnsRemoved))
  • content/docs/releases/v17/17-6.mdx (via RETIRED_KEYS_BY_MAJOR (symbol, a top-level const object), crm_account (literal, a string literal in fixture))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 4 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json, packages/spec/dropped-refinements.baseline.json, packages/spec/src/migrations/entries/retired-keys/18.ui__ObjectGridProps__resizableColumns.ts, …) — pages documenting those are invisible to this run
  • 9 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 713b0fa76dbb6c0f60472cbbefd7690eedb1286e → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 60e9e03a3d51e898eeaa90f514205fea20de1f86 — the merge of head 38159d1362f38beff333f798c34530ad821598c1 into base 713b0fa76dbb6c0f60472cbbefd7690eedb1286e, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 713b0fa76dbb6c0f60472cbbefd7690eedb1286e → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 52c4c42d72858f679b3348f8caede1da7e7e4b45
Local-runs: none

Isolated contract reviewer for PR #21463 (card #21445), written at 2026-10-02T20:50Z. Inputs: the card body and its four comments (triage 5958164933, claim 5958653004, dev report 5960961904, the seat's arm-A answer 5960993628), the PR body, its 14-file list and the net diff origin/main...52c4c42d72 (+1167 / −28), the head's check-runs, read-only objectui at the .objectui-sha pin 89cad75d55 and origin/main's packages/spec/src/ui/view.zod.ts. Nothing was built, run or re-run.

① Derived judgments

The seven typed members of ComponentPropsMap['object-grid'] (ObjectGridPropsSchema), each against the renderer's read at the pin.

  • rowHeight → RowHeightSchema by reference — right. resolveRowHeightMode (ObjectGrid.tsx:1311) admits a string only when ROW_HEIGHT_TO_DENSITY_MODE has it as an own key, and that table is Record keyed by the spec's RowHeight (@object-ui/core normalize-list-view.ts:48, five keys); anything else answers compact. The declared set and the honoured set are the same five values; no honoured value is refused.
  • rowColor → RowColorConfigSchema by reference — right. useRowColor (useRowColor.ts:72-90) reads exactly config.field and config.colors, and returns nothing when either is absent; field required and colors optional is that read. No honoured value is refused.
  • navigation → NavigationConfigSchema by reference — right. useNavigationOverlay reads mode, size, width, openNewTab, preventNavigation (plus the retired view only in its docblock); every one is declared on the carrier object-kanban / object-calendar / object-timeline already take (component.zod.ts:4743, :4911, :6356 on main). No honoured value is refused.
  • conditionalFormatting → ListViewSchema.shape.conditionalFormatting by reference — right, and by-reference is mandatory here since the rule shape is declared nowhere else. What the shared evaluator honours and this door now refuses: the two objectui-native spellings the dev named ({ field, operator, value, backgroundColor, … } and { expression, … }) and, inside the first, the flat colour members backgroundColor / textColor / borderColor at rule level instead of style. The list view refuses them identically today, the tolerance is a consumer-side alias of the kind Prime Directive Add comprehensive test suite for Zod schema validation #12 forbids, and objectui#6152 carries the convergence. One correction to the dev's census sentence ("no bag writes either"): it holds for document bags; objectui's own grid tests at the pin do write the native dialect as host-mounted schemas (plugin-grid/src/__tests__/gridRowDecorationMembers-8071.test.tsx:188-260) — escalated below as an objectui follow-up item, not a spec defect.
  • bulkActionDefs → z.array(BulkActionDefSchema) by identity — right; the pin asserts the same def the list view's element holds. The four gaps the dev reports are real at the pin and each stands on the list view's door by construction, because it is the same schema: actionDef is renderer-attached and refused by guidance on purpose (bulk-action.zod.ts:344); undeclared params[] widget keys that bulkParamToField spreads are refused by BULK_PARAM_WIDGET_CONFIG_KEYS under its recorded ruling (:156, :228); visible takes a string or the { dialect, source } envelope whose dialect is required (EvaluatedExpressionInputSchema), while objectui's ExpressionWire makes dialect optional; params[].type is FieldType (:248) while the twin types it string. Leaving all four alone is right under triage's "report the gap and do not widen it silently": the first two are deliberate refusals already ruled, the last two are the objectui twin being wider than the spec, which is a mirror-parity matter for objectui, not a spec gap. They are NOT on the dev's objectui#6152 follow-up list; escalated below.
  • aggregations → [{ field, type: AggregationFunction }], strict, module-private — right. useGroupedData.ts:17-22 declares AggregationConfig { field: string; type: AggregationType } and nothing else, useServerGrouping.ts:79 compiles the same two members, and AggregationType is the six values of AggregationFunction (the pin asserts the exact set). ListViewSchema has no aggregations member, so there is nothing to reference; no export is added, so api-surface is untouched. No honoured value is refused.
  • operations → strict { create?, update?, delete?, export? } with read / import refused by guidance — right as arm A (seat answer 5960993628). Reads at the pin: the replace-not-merge default (:1818-1822), update / delete (:1898-1899, :4485), export (:4088, :5340, :6141), create (:5468); a grep of plugin-grid/src for operations.read / operations.import finds no read. The one refused writer is objectui's README.md:226 (ruled A). One more objectui-side fact for the follow-up: ObjectView composes the grid node's operations as { ...operations, create: false } (plugin-view/src/ObjectView.tsx:1586, :2485) and reads operations.read itself (:1720), so a view that authors read lands it on the composed grid node; that matters once objectui's strict face judges composed nodes against this row.

resizableColumns retired. Route and kit checked against spec-property-retirement/SKILL.md on origin/main:

  • Tombstone: retiredKey() on a strict shape is the right route (§2 row 1); the guidance string keeps the five house conventions (fully qualified key in the object-grid property form the defaultSort tombstone uses; "was removed in @objectstack/spec 17.7.0 (ADR-0049)", 17.7.0 being the minor this minor changeset cuts from 17.6.0; the dash clause; "Use resizable. Rename the key; the value (a boolean) is unchanged."; the os migrate meta --from 17 sentence). Both channels are pinned (tsc via a consumed @ts-expect-error, the parse via the prescription regex).
  • D2 conversion object-grid-resizable-columns-removed: protocol 18, retiredFromLoadPath: true, retiredAfter: '17.6.0' = packages/spec/package.json (the census pin's rule), inserted at its identifier's sort position in MAJOR_18_CONVERSIONS with order: 57 = highest + 1, scoped by component.type === 'object-grid'. Every arm matches the renderer's schema.resizable ?? schema.resizableColumns ?? true: resizable set → the legacy key strips as a lossless delete (resizable wins, as ?? says); resizableColumns alone → renamed with its value unchanged, so resizableColumns: false alone becomes resizable: false and keeps defeating the renderer's ?? true default; both set and disagreeing → resizable wins and the legacy value is discarded, exactly the ??, with the discarded value named for the author in the D3 reason; resizable: null → ?? reads through null, and the rename overwrites it. Idempotent by construction (the key is gone after either arm). The fixture's four notices are three renames (region, nested page:card.children, slotted) and one strip; the kanban sibling and the keyless grid are untouched; the emitted path follows the defaultSort precedent (destination path).
  • RETIRED_KEYS_BY_MAJOR[18] row ui/ObjectGridProps:resizableColumns from the entries file 18.ui__ObjectGridProps__resizableColumns.ts (the defaultSort naming), generated into registry.ts, spelled as the authorable-surface/ui.json line minus [RETIRED].
  • D3: object-grid-resizable-columns-retired (one per retirement family, carrying conversionIds) and ui-object-grid-row-members-typed (a narrowing with no conversion, the ui-object-grid-export-options-closed precedent) — both with non-empty reason and acceptanceCriteria, inserted at their ids' sort positions.
  • Two STEP18_RATIONALE fragments at orders 62 and 63 = highest (61) + 1 and + 2, at their ids' sort positions. The pre-existing duplicate orders 56 / 60 (rationale) and 55 (conversions) are on main, not this PR's, and the sort breaks ties by id.
  • Nothing missing, nothing doubled: no packages/spec/liveness/ ledger covers component props (zero rows for ObjectGridProps or resizableColumns on main), so no ledger row is owed and Spec property liveness is green; no form input, i18n label or skill names the key; content/docs carried it only in the regenerated reference row, now never / [REMOVED]. authorable-surface.base.json still spells the key by design (a pinned anchor at baseRev, never the live surface). The repo-root sdui.manifest.json description still says "the deprecated resizableColumns is only read when this is absent" because it mirrors objectui's registry text at the pin (plugin-grid/src/index.tsx:265); it refreshes with the pin bump — one more line for the objectui follow-up.
  • Absence pin: tree-scoped, over exactly the radius turbo.json declares for @objectstack/spec#test:repo (packages/** by extension, examples/** json / md / yaml plus examples/*/src/**/*.ts, content/**, skills/**, scripts/**), registered in vitest.repo-tests.json, exclusions structural (the kit, release prose, gitignored build output), no allowlist file. Right.

The census, re-checked. objectstack origin/main: the two showcase grids (my-work.page.ts, command-center.page.ts) write columns, filter, objectName, title, width and none of the seven; the conditionalFormatting / bulkActionDefs / rowColor / userActions.rowHeight hits in examples/app-showcase/src/ui/views/*.view.ts are list-view members judged by ListViewSchema, unaffected; packages/platform-objects writes none; zero resizableColumns writers. objectui at the pin: the one refused writer is README.md:226 (operations.read); rowHeight: 40 hits are view-config overlays (updateViewConfig), not grid bags; navigation: 'drawer' is a timeline diagnostics test; resizableColumns: false in ObjectView.tableSlotRelay-10976.test.tsx:250 is a WITHHELD-key probe asserting the key never reaches the grid node, not a writer. The census stands, with the grid-test note above.

Generated artifacts.

  • content/docs/references/ui/component.mdx: the seven rows and the new nested shapes (aggregations[number], conditionalFormatting[number], rowColor, bulkActionDefs[number], navigation, operations) render the schema; the resizableColumns row is never with the [REMOVED] prescription. Caused by this diff and right. Two further moves are generator-caused and the dev names only one: ObjectGridProps.grouping.fields[] now renders order? / collapsed?, AND ObjectGridProps.data[provider='api'].read / .write now render method? — the input face of defaulted fields — while the identical shared defs on sibling rows on the same page (ObjectKanbanProps.grouping, ObjectGanttProps / ObjectMapProps / ObjectTreeProps.data[provider='api']) still render order / method (counts main → branch: method?: 0 → 2, order?: 0 → 1, collapsed?: 0 → 1). Both faces describe one schema and check:generated is green (Lint & Repo Gates), so the artifact is right as output; the page is now internally inconsistent, which is a generator determinism defect this diff's evaluation order (the ListViewSchema import and .shape access) exposed. Escalated below.
  • authorable-surface/ui.json: one line gains [RETIRED]; the surface lists top-level keys only, so the typed members add no rows. Right.
  • docs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md: +2 strict sites (GridAggregationSchema, GridOperationsSchema), 188 → 190 and component.zod.ts 58 → 60. Right.
  • dropped-refinements.baseline.json: ui/ObjectGridProps gains bulkActionDefs.element, 652 → 653 — it is ui/ListView's existing bulkActionDefs.element site reaching the grid door by identity; conditionalFormatting adds no site because the list view has none for it either. Right.

② Semver level

@objectstack/spec minor with the BREAKING banner, under the launch-window convention check-changeset-no-major enforces (Check Changeset green). Clause-②: yes (narrowing) is the right arm. The dispatch's no (narrowing) predated the measurement; check-widening-tells.mjs:1584-1589 names one shape that is a true refusal although it only narrows — "a member bounded inside a bag that was a universal acceptor (z.unknown() / z.any())" — with the remedy "declare Clause-②: yes and route it to at-tier review", and aggregations[].field / .type and the four operations toggles are exactly that shape. yes takes at least minor; (narrowing) is BREAKING and the banner is present. The seat accepted the switch in 5960993628.

Changeset, sentence by sentence: the ! title; the Clause-② line; exactly one ADR-0087 marker, registered, naming the three ids, all new in this diff (the gate's rule: every id resolves at HEAD and at least one is new); the BREAKING paragraph is factual — the row is read by the component-props gate (packages/lint/src/validate-component-props.ts) on objectstack validate / build / lint, and a page component's properties is z.record(z.string(), z.unknown()) on the load path (page.zod.ts:324), so a stored page still saves and loads; the seven member bullets match ①; "ObjectGridProps (and ObjectGridPropsParsed) … resizableColumns is never" matches the exports at :4122 / :4130 and pin §4; every FROM → TO row is right, including "resizableColumns: true beside resizable: false → resizable: false" (the ??) and rowHeight: 'comfortable' being a density name, not a spec value; the one-line fix is right; the retirement-kit paragraph matches the registries; the census paragraph matches the re-check; "Deployed metadata was not measured" is the honest bound. PR body lines one and two are Fixes #21445 and Clause-②: yes (narrowing), matching the changeset.

③ Boundary flags

Dev deviations:

  1. Arm yes (narrowing) instead of the dispatched no (narrowing) — answered in ②: measured, honest, accepted by the seat.
  2. operations narrowed to the four read members (arm A). The seat answered A in 5960993628; the measurement holds (no reader of read / import in plugin-grid/src). Carried to objectui#6152: the README line, the twin's two members, and the ObjectView relay fact in ①.
  3. Fixes #21445 kept — the seat did not route the card to the decision inbox; "The card this PR closes must claim this branch" and "Part-of PR must not also close its card" are green. No action.
  4. Five repo-project files NOT MEASURED locally (build-schemas-check-mode, dist-freshness, dist-freshness-adoption, publish-smoke-boot-failure, publish-smoke-port-collision) — measured by the Test Core (1/6) … (6/6) check-runs, which run pnpm turbo run test test:repo (ci.yml:826) and so @objectstack/spec#test:repo = vitest run --project repo. At the final read of the head's check-runs (2026-10-02T20:49:38Z) all six shards had concluded success, so the five files are measured, and the whole roster stood at 33 success and 2 skipped by design (Console Pin Gate, Packed-tarball smoke (opt-in)), with nothing failed and nothing still running.
  5. Plain git merge of origin/main 086ad0aa68 — that commit touched .changeset/21426-native-number-comparand.md and two packages/service-analytics files only; no generated artifact, so no os-regen-merge was owed. Right.

out_of_scope_findings, each as a reading escalated to the seat:

  1. check-widening-tells T2 fires on import specifiers. Confirmed a matcher false positive by the gate's own definition (T2 is a member of z.enum / z.union / z.discriminatedUnion / an as const array; import { … } specifiers are none). It moved nothing here because T1 forced yes. The gate's own remedy text says to repair the matcher with a --self-test case or file that repair as its own card; the dev's finding is class a with a named repro — escalated to the seat to file.
  2. Sibling rows still z.unknown(). Confirmed on main: object-kanban.conditionalFormatting (:4670), navigation on object-map (:5502), object-gantt (:5715), object-tree (:6040). Escalated to the family's closure card.
  3. objectui#6152 follow-up list. Confirmed as the dev lists it; the seat should add: the two twin-wider bulkActionDefs gaps (visible.dialect optional, params[].type: string); the ObjectView → grid relay of operations whole (ObjectView.tsx:2485) beside its own operations.read read (:1720); the grid tests that write the native conditionalFormatting dialect (gridRowDecorationMembers-8071.test.tsx); and the index.tsx:265 registry description that still names the deprecated resizableColumns (mirrored into this repo's sdui.manifest.json).
  4. The grouping.fields[] rendering. Wider than reported (the method? move too) and inconsistent across sibling rows on one page — a generator determinism defect with a named repro (this diff). Escalated to the seat to file; not a contract face and not a FAIL reason.

Implemented-by: claude/issue-21445-object-grid-typed-members
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Regen-provenance: 5961282483 · 52c4c42d72 → 38159d1362 · git diff --name-only 713b0fa76d 38159d1362 minus git diff --name-only 086ad0aa68 52c4c42d72 → (empty)

domain:spec seat 1 (session_01UtnxvdiN376GF3sgXwAw4d), holder of claim 5958653004 · 2026-10-02T21:02Z

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 2, 2026 21:23
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 2, 2026 21:23
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit aa46322 Oct 2, 2026
37 checks passed
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 protocol:ui size/xl tests tooling

Projects

None yet

2 participants