Uh oh!
There was an error while loading. Please reload this page.
fix(plugin-grid): retire four undeclared authored column reads in ObjectGrid.generateColumns() - #6670
Conversation
`ObjectGrid.generateColumns()` read `format`, `options`, `appearance` and
`essential` off the authored column through `(col as any)`. `ListColumnSchema`
is a strict object and refuses all four at publish with `unrecognized_keys`,
so the renderer honoured keys the contract rejected — the `declared !=
enforced` split AGENTS.md #0.1 exists to stop.
All four reads are removed, per the maintainer's 2026-08-28 ruling ("B on all
four") under the standing zero-authors rule. Re-measured on this branch before
deleting: zero authored occurrences of any of the four on a column across
`examples/` and `apps/`, with `field` as the positive control.
`columnReadBoundary-6458.test.ts`'s bound on undeclared cast reads in that
branch moves to the empty set, so a new one goes red on arrival.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49The positional-mobile-visibility assertion used `expect(region).toContain(...)` on a ~200-line region, so a failure printed the whole region and buried the one line that mattered. Assert the boolean with an explicit message instead — same strictness, a diagnosis you can read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
✅ 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
|
os-sales
commented
Aug 28, 2026
✅ ACCEPT — flipped ready, auto-merge onReviewer: Gate reading: all 29 check runs read, returned count compared against ⏱ This PR sat green for ~35 minutes before being flipped, because this seat hit the GitHub API rate limit at 12:17Z and backed off rather than retrying. The delay was the reviewer's, not the change's. ⭐ The trap this run was warned about, hit, and got rightThe dispatch order flagged that
The census that settled it was key-position-aware — a
The closest call was chased rather than waved through: The pin is better than what was asked forThe order asked for the boundary bound to go to the empty set. What landed also makes the bound no longer depend on the list of four, so a fifth key arriving is refused exactly as loudly as one of the four returning. The deletion alone would be undone by the next person adding a cast; this is what makes the retirement stick. The ablation is differentiated, not uniform: re-adding one read gives ⭐ The lint ratchet is the measurement I would not have thought to ask for: Two corrections to my dispatch order, both in the card's favour
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#6458
ObjectGrid.generateColumns()stops reading the four authored column keysListColumndoes not declare —format,options,appearance,essential— per the maintainer's 2026-08-28 ruling (live director session, batch #4 item
4, presented as "all four B"): 「同意」.
ListColumnSchemais astrictObject, so an author who wrote any of the fourwas refused at publish with
unrecognized_keyswhile this renderer happilyhonoured the key at runtime. That is the
declared != enforcedsplit AGENTS.md#0.1 exists to stop, and it was reachable only through
(col as any).The authority for retiring rather than declaring is the standing
zero-authors rule (2026-08-27, recorded on #6355 / objectstack#12668): zero
authors means immediate retirement, no deprecation window.
All verification below is at
8353222a, the final commit, on a clean tree(
git status --porcelain= 0 lines).1. Premise re-verified on this ref — the census was re-run, not quoted
The ruling rests on a zero-author measurement taken 2026-08-26. A key that had
gained an author since would change the answer, and a retirement is not
reversible from the author's side, so it was re-derived from scratch here.
The population exists before anything is counted
git ls-tree HEADconfirms both roots:examples/(byo-backend-console,console-starter,hello-world,schema-catalog) andapps/(console,site). 690 tracked.json/.ts/.tsx/.js/.mjsfiles underthem: 446 JSON parsed, 243 TS parsed, 1 unparsed (
apps/site/tsconfig.json— JSONC the tolerant reader still rejects). That one file is closed by
superset, not assumed: a bare word grep over it — which is a superset of
any key-position census — returns zero occurrences of all four words.
The census is key-position aware, and that is load-bearing
A bare grep is wrong here in both directions.
format,optionsandappearanceare entirely legal at other tiers — object-field level, widgetlevel, filter level, form-child level. So the scan walks structure: JSON via
JSON.parseand a recursive walk, TS/TSX via the TypeScript compiler API(
ts.createSourceFile+ AST walk), recording every occurrence of the four wordsin key position together with the ancestor property chain that leads to it.
Result, per key — never as a total
examples/+apps/ListColumncolumn?formatfields[].format(object-field tier — this is the fallback road itself), 1 atdashboard.widgets[].formatoptionswidgets[], 11fields[], 6 settings-select, 3globalFilters[], rest form children / object-schemafieldsmapsappearanceessentialfieldcolumns[].fieldin JSON + 5 atvar:COLUMNS[].field+ 2 atcolumns[].fieldin TS⭐ The
optionsrow is exactly the trap this card warned about. A bare grepreports 71 "authors" and would have stopped a ruled retirement on false
evidence. Every one of the 71 is at a tier where the key is legal.
The single closest call is worth naming:
apps/console/src/dev/DevMasterDetail.tsx:31is
columns[].options— but onobject-master-detail-form'sdetails[].columns[],whose column objects are keyed by
name, notfield, and which is consumed bypackages/plugin-form/src/MasterDetailForm.tsx(hydrateColumns), not byObjectGrid.generateColumns(). Different component, different tier, not thisseam.
Narrower populations agree: of 51 JSON objects that are direct elements of a
columnsarray, zero carry any of the four. And a separate scan ofpackages/finds zero column fixtures (object literal in acolumnsarraycarrying a
fieldkey) authoring any of the four — so no test fixture dependedon the removed behaviour either.
Conclusion: the premise holds on this ref. All four keys have zero authors,
per key, with a positive control that fires in the same query shape and the same
container.
2. What each retirement leaves as the only road
formatandappearance— the object-field fallback(
objectDefField?.format/?.appearance). Already the only road everymeasured author uses; the column-level read was a second dialect for the same
fact.
options— the object schema's select options. The column-level overridewas exactly the shape that lets AI-authored metadata drift from the schema it
is supposed to obey. One source for options beats two.
essential— mobile visibility stays positional:colIndex === 0.semantic mobile-column control arrives, the declare route reopens — declare
the key on
@objectstack/specand read it without a cast, which the guard belowpermits by construction. objectstack#12715 is the precedent: removed while
unenforced, re-introduced once demand and enforcement met. The changeset and the
in-code comment both say this, so neither reads as a door closed forever. What
stays ruled out is the third road — a renderer-side tolerance for a key the
schema refuses.
3.
prefix— stated separately, because it is a separate thing (and already done)prefixis declared byListColumn, so its cast was never a retirement:it was pure noise that made a schema-admitted key look exactly like the four
undeclared ones, and threw away
ColumnPrefix's typing at every use.had already landed on
mainin PR #6587.ObjectGrid.tsxat today's refalready reads
const prefixConfig = col.prefix;. So this PR does not touch itand takes no credit for it — the "no declared key is read through a cast"
assertion in the guard simply continues to hold. Mentioned because the order
listed it as work to do.
4. The boundary pin moves in the same PR — this is the load-bearing half
The deletion alone is undone by the next person who adds a cast.
columnReadBoundary-6458.test.ts(landed by #6587) had a subset bound on undeclared cast reads, deliberately
loose while the four were unadjudicated. That bound is now the empty set.
Both halves of the defect are now refused mechanically:
prefixwas) — buys nothing, hides theundeclared ones among lookalikes;
declared != enforcedsplit itself.The bound does not depend on the list of four, so a fifth key arriving is
refused exactly as loudly as one of the four returning. A separate per-key
assertion exists only for diagnosis — so a re-added read names which verdict
it re-opens rather than printing an anonymous
expected [x] to equal [].Anti-vacuity — three controls, and one of them had to move
The guarded region's correct answer is now zero cast reads, so a scanner
that silently matched nothing would be indistinguishable from success:
region-length floor;
scale — necessarily from outside the guarded region now, since inside
it the whole point is that there is nothing left to find. It is drawn from
(col as any).fitContentin the grouped-width pass, which iterates emittedcolumns rather than the authored input and is finding(components):
data-tablereads two column keysTableColumndoes not declare —headerIconandfitContent#6424's question, deliberatelyoutside this guard.
Control 3 previously drew from inside the region — which worked only while
the region still contained cast reads. Leaving it there would have made it the
very thing it exists to rule out. The test says so, and says it must never be
re-pointed back inside.
5. Ablation — the pin bites, and it is a differentiated red
Re-added one retired read (
essential) and measured.No rebuild is needed, and that is a property of the pin rather than a
shortcut: the guard
readFileSyncsObjectGrid.tsxstraight off disk,@object-ui/typesis aliased topackages/types/srcby the rootvitest.config.mts(line 261), and@objectstack/spec@17.2.0resolves fromnode_modules. Nodist/sits on the resolution path, so a stale build cannotfake this result in either direction.
Mutation proven on disk before any result was read — never from an editor's
exit code:
Result — 3 failed / 5 passed, not a uniform red:
Assertions unrelated to that key stayed green, which is what makes this a
measurement rather than a blanket failure: the anchor test, the synthetic
scanner control, the real-file out-of-region control, the declared-key-cast
assertion, and the typed-
prefixassertion.Restore proven by observation, not by an exit code, under a
trap restore EXIT INT TERMusing absolute paths, restoring with an explicitgit checkout HEAD -- ABSPATH(a baregit checkout -- pathrestores from theindex, which the mutation would be in):
6. Gate verdicts — each quoted from the gate's own output, exit code captured before any pipe
All at
8353222a. Every run redirects to a file and captures$?beforeany
tail, becauseEXIT=$?aftercmd | tailreadstail's status and prints0for green and red alike.pnpm --filter @object-ui/plugin-grid type-checkVERDICT command-exit 0; script echoed astsc --noEmit && tsc -p tsconfig.test.json(so this is not a zero-match pnpm filter)pnpm exec vitest run packages/plugin-grid/Test Files 96 passed (96)·Tests 885 passed (885)check:control-bytesOK (scanned 5504 tracked text file(s); skipped 85 binary)check:phantom-depsEvery in-scope import is declared by the package that publishes it.check:self-importNo package names itself inside its own src/.check:spec-symbolsspec symbol derivation: 1315 files scanned against 4959 spec export namescheck:vi-mock-specifiersOK (3881 tracked source file(s), 2179 test-named; 472 carry a mock)check-changeset-presence.mjs2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)check-changeset-no-major.mjsNo changeset declares a major bump.The gate family was derived from this repo's own
package.jsonscripts and.github/workflows/— objectui has noscripts/pm/dispatch-gates.mjs; thatscript lives only in objectstack and answers only about the tree it sits in.
NOT MEASURED, checked rather than assumed
A package
typecheckthat excludes**/*.test.tsis a true sentence that saysnothing about an edited test file. Checked with
--listFiles: the edited testis in the
tsconfig.test.jsonprogram (1 hit), withcolumnEmitBoundary-6004as the positive control (1 hit) and
ObjectGrid.tsxpresent (1 hit). So thecompile-time
IsAnyassertion in that file is real.Lint — a declared narrowing, with all three evidences
Repo-wide
eslint .is left to CI. This run is narrowed to the two touchedfiles, and the narrowing is a measurement, not a skip:
eslint.config.jsextendsjs.configs.recommended+tseslint.configs.recommendedwith no
parserOptions.projectand noprojectService. Type-awarelinting is off, so this diff cannot move the verdict on any file it did not
touch.
--format jsonoutput: 2 fileslinted, 0 errors.
eslint --stdin --stdin-filenameat the same path (so no disk mutation, andthe two readings are comparable):
no-explicit-any(col as any)castsorigin/mainExactly -5 on both counters, matching exactly the five cast occurrences
removed (
format1,options2,appearance1,essential1) and nothingelse. The one survivor is
fitContentat the grouped-width pass — a differentcol, outside the guarded region, owned by #6424.Scope
Three files, all inside the dispatched fence:
packages/plugin-grid/src/ObjectGrid.tsxpackages/plugin-grid/src/__tests__/columnReadBoundary-6458.test.ts.changeset/6458-retire-undeclared-column-reads.md(minor— nevermajor,per the fixed-group rule)
⛔
packages/specis untouched: the declare route was not ruled here and thatpackage belongs to the spec seat. ⛔ Sibling #6424 is untouched: its
fitContentwas ruled declare in the same batch — same criterion, opposite reading,
because
fitContenthas shipped authors and these four have none. Folding bothinto one PR would destroy that contrast.
Generated by Claude Code
Generated by Claude Code