Skip to content

fix(plugin-grid): retire four undeclared authored column reads in ObjectGrid.generateColumns() - #6670

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-6458-retire-undeclared-column-reads
Aug 28, 2026
Merged

fix(plugin-grid): retire four undeclared authored column reads in ObjectGrid.generateColumns()#6670
os-sales merged 2 commits into
mainfrom
claude/issue-6458-retire-undeclared-column-reads

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes#6458

ObjectGrid.generateColumns() stops reading the four authored column keys
ListColumn does 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"): 「同意」.

ListColumnSchema is a strictObject, so an author who wrote any of the four
was refused at publish with unrecognized_keys while this renderer happily
honoured the key at runtime. That is the declared != enforced split 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 HEAD confirms both roots: examples/ (byo-backend-console,
console-starter, hello-world, schema-catalog) and apps/ (console,
site). 690 tracked .json / .ts / .tsx / .js / .mjs files under
them: 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, options and
appearance are entirely legal at other tiers — object-field level, widget
level, filter level, form-child level. So the scan walks structure: JSON via
JSON.parse and a recursive walk, TS/TSX via the TypeScript compiler API
(ts.createSourceFile + AST walk), recording every occurrence of the four words
in key position together with the ancestor property chain that leads to it.

Result, per key — never as a total

keykey-position occurrences under examples/ + apps/on a ListColumn column?where they actually are
format302 at fields[].format (object-field tier — this is the fallback road itself), 1 at dashboard.widgets[].format
options71015 form-field, 15 widgets[], 11 fields[], 6 settings-select, 3 globalFilters[], rest form children / object-schema fields maps
appearance00nowhere at all
essential00nowhere at all — a bare grep also returns 0, the strongest form of zero
field98POSITIVE CONTROLfires in the same query shape, incl. 10 at columns[].field in JSON + 5 at var:COLUMNS[].field + 2 at columns[].field in TS

The options row is exactly the trap this card warned about. A bare grep
reports 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:31
is columns[].options — but on object-master-detail-form's details[].columns[],
whose column objects are keyed by name, not field, and which is consumed by
packages/plugin-form/src/MasterDetailForm.tsx (hydrateColumns), not by
ObjectGrid.generateColumns(). Different component, different tier, not this
seam.

Narrower populations agree: of 51 JSON objects that are direct elements of a
columns array, zero carry any of the four. And a separate scan of
packages/ finds zero column fixtures (object literal in a columns array
carrying a field key) authoring any of the four — so no test fixture depended
on 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

  • format and appearance — the object-field fallback
    (objectDefField?.format / ?.appearance). Already the only road every
    measured author uses; the column-level read was a second dialect for the same
    fact.
  • options — the object schema's select options. The column-level override
    was 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.

⚠️Retired for want of authors, not forbidden forever. If a real request for
semantic mobile-column control arrives, the declare route reopens — declare
the key on @objectstack/spec and read it without a cast, which the guard below
permits 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 by ListColumn, 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.

⚠️Correction to the dispatch order, in the card's favour: that cast drop
had already landed on main in PR #6587. ObjectGrid.tsx at today's ref
already reads const prefixConfig = col.prefix;. So this PR does not touch it
and 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:

  • a declared key read through a cast (prefix was) — buys nothing, hides the
    undeclared ones among lookalikes;
  • an undeclared key read at all — the declared != enforced split 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:

  1. both region anchors asserted present by count before any slice, plus a
    region-length floor;
  2. the regex proved able to find cast reads on a synthetic input;
  3. ⭐ the regex proved able to find a cast read in this real file, at real
    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).fitContent in the grouped-width pass, which iterates emitted
    columns rather than the authored input and is finding(components): data-table reads two column keys TableColumn does not declare — headerIcon and fitContent #6424's question, deliberately
    outside 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 readFileSyncs ObjectGrid.tsx straight off disk,
@object-ui/types is aliased to packages/types/src by the root
vitest.config.mts (line 261), and @objectstack/spec@17.2.0 resolves from
node_modules. No dist/ sits on the resolution path, so a stale build cannot
fake this result in either direction.

Mutation proven on disk before any result was read — never from an editor's
exit code:

retired form present: 1 -> 0 (anchored grep -cF)
injected form present: 0 -> 1 (anchored grep -cF)
blob 0905b2ccc12facb49a77ec3998cbf6f539956e4f -> d1a316e9a1c3c19e394f169e1ad96f15d6804572

Result — 3 failed / 5 passed, not a uniform red:

× no UNDECLARED key is read at all - the bound is the EMPTY SET
AssertionError: ... expected [ 'essential' ] to deeply equal []
× each retired key is named individually, so a re-added read says which one
AssertionError: `essential` was RETIRED by objectui#6458's maintainer ruling ...
expected [ 'essential' ] to not include 'essential'
× the split is closed - the schema still refuses `essential`, and nothing reads it
ABLATED_VITEST_EXIT=1

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-prefix assertion.

Restore proven by observation, not by an exit code, under a
trap restore EXIT INT TERM using absolute paths, restoring with an explicit
git checkout HEAD -- ABSPATH (a bare git checkout -- path restores from the
index
, which the mutation would be in):

blob now: 0905b2ccc12facb49a77ec3998cbf6f539956e4f == HEAD blob
git diff HEAD -- packages/plugin-grid/src/ObjectGrid.tsx : 0 bytes
retired form present: 1 injected form present: 0
control re-run on the restored tree: Test Files 1 passed (1) | Tests 8 passed (8)

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 $?before
any tail, because EXIT=$? after cmd | tail reads tail's status and prints
0 for green and red alike.

gateexitthe gate's own verdict line
pnpm --filter @object-ui/plugin-grid type-check0VERDICT command-exit 0; script echoed as tsc --noEmit && tsc -p tsconfig.test.json (so this is not a zero-match pnpm filter)
pnpm exec vitest run packages/plugin-grid/0Test Files 96 passed (96) · Tests 885 passed (885)
check:control-bytes0OK (scanned 5504 tracked text file(s); skipped 85 binary)
check:phantom-deps0Every in-scope import is declared by the package that publishes it.
check:self-import0No package names itself inside its own src/.
check:spec-symbols0spec symbol derivation: 1315 files scanned against 4959 spec export names
check:vi-mock-specifiers0OK (3881 tracked source file(s), 2179 test-named; 472 carry a mock)
check-changeset-presence.mjs02 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
check-changeset-no-major.mjs0No changeset declares a major bump.

The gate family was derived from this repo's ownpackage.json scripts and
.github/workflows/ — objectui has no scripts/pm/dispatch-gates.mjs; that
script lives only in objectstack and answers only about the tree it sits in.

NOT MEASURED, checked rather than assumed

A package typecheck that excludes **/*.test.ts is a true sentence that says
nothing about an edited test file. Checked with --listFiles: the edited test
is in the tsconfig.test.json program (1 hit), with columnEmitBoundary-6004
as the positive control (1 hit) and ObjectGrid.tsx present (1 hit). So the
compile-time IsAny assertion 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 touched
files, and the narrowing is a measurement, not a skip:

  1. Population / type-awareness read from eslint's own config, not asserted:
    eslint.config.js extends js.configs.recommended + tseslint.configs.recommended
    with noparserOptions.project and noprojectService. Type-aware
    linting is off, so this diff cannot move the verdict on any file it did not
    touch.
  2. File count read from the tool's own --format json output: 2 files
    linted, 0 errors.
  3. Ratchet moves the right way, measured against base content via
    eslint --stdin --stdin-filename at the same path (so no disk mutation, and
    the two readings are comparable):
total messagesno-explicit-any(col as any) casts
origin/main2171866
this branch2121811

Exactly -5 on both counters, matching exactly the five cast occurrences
removed (format 1, options 2, appearance 1, essential 1) and nothing
else. The one survivor is fitContent at the grouped-width pass — a different
col, outside the guarded region, owned by #6424.


Scope

Three files, all inside the dispatched fence:

  • packages/plugin-grid/src/ObjectGrid.tsx
  • packages/plugin-grid/src/__tests__/columnReadBoundary-6458.test.ts
  • .changeset/6458-retire-undeclared-column-reads.md (minor — never major,
    per the fixed-group rule)

packages/spec is untouched: the declare route was not ruled here and that
package belongs to the spec seat. ⛔ Sibling #6424 is untouched: its fitContent
was ruled declare in the same batch — same criterion, opposite reading,
because fitContent has shipped authors and these four have none. Folding both
into one PR would destroy that contrast.

Generated by Claude Code


Generated by Claude Code

`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_01CRJge11jso9TpXRWFt1Z49
The 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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3236.0 KB3266.6 KB
Main entry chunk (gzip)157.3 KB350 KB
Entry fileindex-CkkrKDvw.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)509.24KB115.61KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)239.05KB60.06KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.85KB12.89KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)190.33KB45.10KB
plugin-dashboard (index.js)133.43KB34.48KB
plugin-designer (index.js)212.80KB43.15KB
plugin-detail (index.js)245.29KB62.39KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.01KB32.23KB
plugin-gantt (index.js)165.16KB40.33KB
plugin-grid (index.js)201.51KB54.54KB
plugin-kanban (index.js)53.11KB14.62KB
plugin-list (index.js)112.86KB27.54KB
plugin-map (index.js)20.09KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)26.72KB7.71KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)85.87KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)65.97KB21.98KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)2.44KB1.21KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)9.30KB3.22KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-salesClaude

Copy link
Copy Markdown
CollaboratorAuthor

✅ ACCEPT — flipped ready, auto-merge on

Reviewer: domain:ui @ objectui execution seat, PM session 8ca04858-ea8e-5b85-9182-de59aa49e00c.

Gate reading: all 29 check runs read, returned count compared against total_count: 29. 26 success, 3 skipped, 0 failing, 0 still running.

⏱ 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 right

The dispatch order flagged that format, options and appearance are common words occurring at other tiers, and that a bare grep would report authors where none exist — with the failure mode in that direction being worse, because it would stop a ruled retirement on false evidence.

options did exactly that: a bare grep reports 71 authors.

The census that settled it was key-position-aware — a JSON.parse walk plus a TypeScript compiler-API AST walk over 690 tracked files under examples/ and apps/ — and reports per key:

keyoccurrenceson a ListColumn column?
format30 — all at other tiers
options710 — all at other tiers
appearance00
essential00
field (control)98, incl. 10 at columns[].field in JSON

The closest call was chased rather than waved through: DevMasterDetail.tsx:31's columns[].options belongs to object-master-detail-form's details[].columns[], keyed by name not field, and consumed by plugin-form/MasterDetailForm.tsx's hydrateColumns — not by ObjectGrid. One file that would not JSON.parse (apps/site/tsconfig.json, JSONC) was closed by superset — a bare grep over it returns zero of all four words — rather than assumed.

The pin is better than what was asked for

The 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 Tests 3 failed | 5 passed (8), and the three name the key (expected [ 'essential' ] to deeply equal []), while the five unrelated assertions — anchors, the synthetic scanner control, the real-file out-of-region control, declared-key-cast, typed-prefix — stay green.

⭐ The lint ratchet is the measurement I would not have thought to ask for: eslint --stdin --stdin-filename against base content at the same path, so the two readings are comparable without mutating disk. origin/main 217 messages / 186 no-explicit-any / 6 casts → this branch 212 / 181 / 1. Exactly −5 on both counters, matching exactly the five cast occurrences removed and nothing else. That is invariance measured rather than asserted.

Two corrections to my dispatch order, both in the card's favour

  1. The prefix cast drop I listed as work had already landed in PR fix(plugin-grid): read the declared prefix column key without a cast #6587ObjectGrid.tsx at today's ref already reads const prefixConfig = col.prefix;. This PR does not touch it and takes no credit for it. My order was stale.
  2. The card's 2026-08-26 line numbers were indeed stale as warned: the reads were at :1954 / :1979 / :1980 / :1985 / :2101, not the quoted :1815 / :1840 / :1846 / :1951.

⚠️ Carried forward for the family

A measurement recorded on sibling #6424 says cast count is not a reliable instrument for this family — the normalization is: data-table.tsx's two (col as any).fitContent sites are redundant today because col is already any there. That reading names this card as one that reasoned from cast counts. It does not undermine this PR — the census here was key-position-aware and the ratchet was measured against base content, neither of which depends on cast counting — but the caution is now in the seat post so the next card in this family does not inherit the wrong instrument.

Also recorded honestly by the dev: one lock call returned exit 99 (queue timeout) and one was killed by the container's ~10 min foreground cap (exit 143) — both reported as NOT MEASURED and re-run, never as reds. And the issue-comment copy of the report says 6 MCP calls where the true figure is 5; flagged rather than left to differ silently.

ObjectGrid.tsx's one surviving cast (fitContent at :3588) is deliberately not filed again — it is already on #6424, and it now serves as that pin's real-file anti-vacuity control.


Generated by Claude Code

Merged via the queue into main with commit f55d666Aug 28, 2026
30 checks passed
@os-sales
os-sales deleted the claude/issue-6458-retire-undeclared-column-reads branch August 28, 2026 13:04
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

finding(plugin-grid): ObjectGrid honours four undeclared authored column keys via (col as any) — the #6425 shape at the other producer

2 participants

@os-sales@claude