fix(plugin-grid): retire the three inert fieldMeta copies the cell never reads - #7186

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7166-inert-fieldmeta-copies
Sep 1, 2026
Merged

fix(plugin-grid): retire the three inert fieldMeta copies the cell never reads#7186
os-warren merged 1 commit into
mainfrom
claude/issue-7166-inert-fieldmeta-copies

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7166

ObjectGrid no longer copies descriptionField, lookupColumns or lookupFilters onto a relational column's fieldMeta. No behaviour change — all three still reach the inline lookup picker, by a different seam.

Copy set: 10 keys to 7. Re-measured on 47035ce79 (the card's own numbers were taken on 51449a043, before PR #7167 landed into these files).

Ruling item 3 — the gating measurement: RELATIONAL_META_KEYS is INTERNAL

Clause 2 is not engaged, measured three ways, each with a live control:

proberelationalMetaKeyscontrol (useRowColor)
re-export in src/index.tsx barrelabsent (no export * either)line 30
built dist/index.d.ts0 occurrences of any of its symbolspresent
built dist/index.js export listnot exportedpresent

Every import of ./relationalMetaKeys in the repo lives inside packages/plugin-grid/src/ — the implementation plus two of its own tests. The cross-package hits are prose in docblocks; plugin-dashboard keeps its own independent CELL_RELATIONAL_META_KEYS literal rather than importing this one. Narrowing the copy set therefore moves no published surface.

What was measured

applyRelationalMeta writes onto the fieldMeta that generateColumns hands to CellRenderer as the field prop — 6 JSX passes across the three column-building paths, and nowhere else (as a whole object it otherwise only reaches applyRelationalMeta, which mutates it).

  • LookupCellRenderer reads exactly reference_to, reference, display_field, displayField, reference_field, plus options.
  • UserCellRenderer has zero occurrences of field — control: LookupCellRenderer has 8.

Stated by receiver rather than by count: packages/fields/src/index.tsx, the file holding every cell renderer, contains zero occurrences of the three retired keys, against a control of 22 occurrences of the display_field / displayField / reference_to spellings the cell does read.

Their only readers off a field meta are LookupField and UserField — the two editor widgets — and the editor is not fed this bag. renderCellEditor looks the field up in the object schema and spreads the whole def into the widget ({ name: ctx.column.accessorKey, ...fieldDef }), so every key a def carries reaches LookupField whether or not it is copied. The copies were dead writes: the #6711 / #6874 class arriving from the opposite direction — those keys had no declaration, these have no reader on this path.

The evidence bar this card exists to raise

Two of the three, descriptionField and lookupColumns, were added by #6875 as its fix, classified from a read-set derivation alone. Its third key displayField is genuinely delivered and stays copied — and it is the one that arrived with a rendering test.

A derivation establishes that a consumer READS a key. It does not establish that a given BAG is how the consumer gets it. Recorded in both docblocks.

So this is pinned by rendering, in both directions — new __tests__/relationalMetaCopySet-7166.test.tsx:

  • The cell. Four lookup columns over one referenced record, differing only in the key under test, all resolve the same text; the displayField control column resolves something else. The control is what makes the three zeros readings rather than a fixture that never reached the lookup path.
  • The editor. Each retired key still takes effect in the inline picker with the copy set no longer carrying it — descriptionField drives the secondary line, lookupColumns shapes the picker columns, lookupFilters scopes the candidates — each against a sibling control column that declares nothing. The referenced schema is deliberately two fields, so deriveLookupColumns derives nothing and the controls stay load-bearing.

Scope: 3 keys, not 6

The card measures six keys with no reader on this bag. They are two populations. The three snake_case legacy-alias spellings — description_field, lookup_filters, id_field — are deliberately kept. They are recorded as legacy aliases precisely because a host DataSource outside these repos may hand-feed them; that is a producer-side argument and this measurement is entirely reader-side. Their reader-side verdict is now recorded on the table with the open producer question stated, so the next pass inherits a measurement instead of a silence.

⛔ Reported, not fixed: the derivation gate cannot see this class

The gate's read set is a union over three consumers, two of which are not fed this bag. Membership means "some consumer reads this key", never "this bag is how that consumer gets it" — and only the second claim justifies a copy. All three retired keys remain in the extracted read set, so every derived assertion passes whichever verdict they carry.

Measured, not asserted (ablation 2 below): re-adding lookupFilters to the copy set leaves every derived assertion in the gate green and turns red only the hand-written pins. Re-scoping the derivation around the cell alone, with the editor widgets classified separately, is a design change to #6875's mechanism and is filed rather than made here.

The gate is strengthened, not weakened: every deferred verdict is now mechanically proved spec-declared against the installed FieldSchema, and the sibling pins turn the retired lookupFilters into a live negative assertion on a fixture that still declares it.

Tests

All runs are the canonical repo-root invocation (the repo's vitest guard refuses a package-cwd run as a false green). Union run on the final commit f4f299e23:

pnpm exec vitest run packages/plugin-grid/src packages/fields/src/__tests__/lookupCellDisplayField.test.tsx
Test Files 106 passed (106)
Tests 956 passed (956)

pnpm --filter @object-ui/plugin-grid run type-check — clean (first attempt reported TS2307: Cannot find module '@object-ui/*', which is an unbuilt dependency closure and NOT a result; re-run after pnpm --filter '@object-ui/plugin-grid^...' build). The new test file is confirmed present in the tsconfig.test.json program via --listFiles, so that green does cover it.

eslint . --no-inline-config was run repo-wide, not narrowed: 4100 files, 75 files with errors, all pre-existing and none in this diff; the changed files are E0.

Ablations

Both targets resolve from source (the root config aliases every @object-ui/* to src/, and both are relative imports), so no rebuild leg applies. Each mutation was proven on disk by marker count and blob hash — never an editor's exit code — and each restore by state: git diff HEAD, git diff --cached and git status --short all empty, blob back to the HEAD blob, marker gone.

  1. Break the editor's schema spread (predicted before running: the editor half goes red, the cell half stays green) — Tests 3 failed | 2 passed. Exactly B1/B2/B3 red; test A and the premise test green. The retirement's safety claim is load-bearing.
  2. Re-add lookupFilters to the copy set (predicted: derived gate assertions stay green, hand-written pins go red) — Tests 8 failed | 19 passed. The only derivation-gate failure is the hand-written finding(plugin-grid): 6 of the 10 keys ObjectGrid copies onto a column's fieldMeta have no reader on that bag — the picker keys reach the editor by a different seam #7166 pin; every derived assertion stayed green. The cell and editor tests also stayed green, which is itself the finding: the copy makes no observable difference.

Generated by Claude Code


Generated by Claude Code

…never reads
`applyRelationalMeta` no longer copies `descriptionField`, `lookupColumns` or
`lookupFilters` onto a relational column's `fieldMeta`. No behaviour change:
all three still reach the inline lookup picker, by a different seam.
`fieldMeta` is handed to `<CellRenderer>` as the `field` prop and nowhere else
— six JSX passes across the three column-building paths. A relational column
resolves to `LookupCellRenderer`, which reads exactly `reference_to`,
`reference`, `display_field`, `displayField`, `reference_field` and `options`;
a `user` column resolves to `UserCellRenderer`, which destructures `{ value }`
and reads no field meta at all. Measured by receiver: `fields/src/index.tsx`,
the file holding every cell renderer, contains zero occurrences of the three
retired keys, against a control of 22 for the spellings the cell does read.
Their only readers off a field meta are `LookupField` and `UserField` — the two
editor widgets — which `renderCellEditor` feeds from the schema def directly,
so every key a def carries reaches them whether or not it is copied.
Two of the three were added by objectui#6875, classified from a read-set
derivation alone; its third key `displayField` is genuinely delivered and stays
copied. A derivation establishes that a consumer reads a key, not that a given
bag is how the consumer gets it — recorded in both docblocks.
- New `relationalMetaCopySet-7166.test.tsx` renders both halves: the cell (four
columns differing only in the key under test resolve the same text, with a
`displayField` control that differs) and the editor (each retired key still
takes effect in the picker, each against a control column declaring nothing).
- The three snake_case `legacy-alias` spellings are deliberately kept: their
retention rests on an open producer-side question this reader-side
measurement does not touch. Their verdict is now recorded on the table.
- The derivation gate cannot enforce this retirement — its read set is a union
over three consumers, two of which are not fed this bag — and now says so.
Absence is pinned by an explicit assertion plus the rendering test.
- Gate strengthened: every `deferred` verdict is now mechanically proved
spec-declared, and the sibling pins turn the retired `lookupFilters` into a
live negative assertion on a fixture that still declares it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.2 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-VEpn3hxK.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)68.45KB19.20KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)207.19KB56.03KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

os-dev-report

{
"issue": 7166,
"status": "done",
"branch": "claude/issue-7166-inert-fieldmeta-copies",
"pr": "https://github.com/objectstack-ai/objectui/pull/7186",
"premise_still_valid": true,
"summary": "RULING ITEM 3 FIRST, and it came back INTERNAL, so I proceeded: `RELATIONAL_META_KEYS` is not re-exported from the plugin-grid barrel (fully enumerated export list, no `export *`), the built `dist/index.d.ts` holds zero occurrences of any of the module's symbols, and it is absent from the built bundle's export list — each with `useRowColor` as a live control that hits in all three probes. Clause 2 not engaged. A2.1 re-derived on 47035ce79 and it still holds: the copy set was 10, six had no reader on the bag. Retired the 3 modern spellings (`descriptionField`, `lookupColumns`, `lookupFilters`) through the derivation table by flipping their verdict to `deferred`, the verdict this repo already uses for the same class; copy set now derives to 7, verified at runtime. Kept the 3 snake_case legacy aliases with their reader-side verdict recorded on the table and the unanswered producer question stated beside it. A2.3, A2.4 and A2.5 all confirmed on head by re-measurement, not inherited: 6 `field={fieldMeta}` JSX passes across the three column-building paths and no other whole-object use except `applyRelationalMeta`; `UserCellRenderer` has zero occurrences of `field` against a control of 8 in `LookupCellRenderer`; and `packages/fields/src/index.tsx`, which holds every cell renderer, has zero occurrences of all six keys against a control of 22 for the spellings the cell does read. The gate was strengthened, not weakened. Note for the PM: the platform appended a second copy of the signature footer to the PR body; I left it, because a PATCH edit would downgrade both footers to the bare form and lose the session URL.",
"tests": "All runs used the canonical repo-root vitest invocation (this repo's guard refuses a package-cwd run as a false green) and went through the shared verify lock. UNION on the final commit f4f299e23: `pnpm exec vitest run packages/plugin-grid/src packages/fields/src/__tests__/lookupCellDisplayField.test.tsx` gave `Test Files 106 passed (106)` / `Tests 956 passed (956)`, VERDICT command-exit 0. Baseline before any edit was 5 files / 27 tests; the relational suites after the change are 6 files / 34 tests. `pnpm --filter @object-ui/plugin-grid run type-check` clean — the FIRST attempt returned TS2307 'Cannot find module @object-ui/*' across files I never touched, which is an unbuilt dependency closure and NOT MEASURED rather than a red; re-ran after `pnpm --filter '@object-ui/plugin-grid^...' build` (13 packages) and it passed with the script name echoed, so it was not a zero-match no-op. Confirmed the new test file is actually IN the tsconfig.test.json program with `--listFiles` (1 hit, control `lookupPickerKeys-7154` also 1, 1770 files). LINT: ran `eslint . --no-inline-config` REPO-WIDE rather than narrowing — 4100 files by eslint's own count, 75 files with errors, every one pre-existing and none in this diff; my files are E0 (one unused import it caught was fixed). Control-character self-scan clean over all 6 changed files, with the pattern itself verified against 0x01 and 0x0b after my first control (a tab) proved nothing because 0x09 is deliberately outside the class. ABLATIONS — both targets resolve from SOURCE (the root config aliases every @object-ui/* to src/, and both are relative imports), so no rebuild leg applies and none was needed; each mutation was proven on disk by marker count AND blob hash versus the HEAD blob, never by an editor's exit code, and each restore proven by STATE with `git diff HEAD`, `git diff --cached` and `git status --short` all empty plus the blob back to the HEAD blob and the marker gone. Both ran against the COMMITTED implementation. (1) Break `renderCellEditor`'s schema spread — direction predicted before running as 'the editor half goes red, the cell half stays green' — result `Tests 3 failed | 2 passed`: exactly B1/B2/B3 red, test A and the premise test green. (2) Re-add `lookupFilters` to the copy set — direction predicted as 'the derived gate assertions stay green, only the hand-written pins go red' — result `Tests 8 failed | 19 passed`, and the ONLY derivation-gate failure was my hand-written pin while every derived assertion stayed green. One flaw recorded rather than hidden: in ablation 2 my `deferred`-count grep was malformed by nested quotes and printed a bogus 0 — the exact 'empty output read as a zero' trap; the mutation was still proven by the injected-marker count and the blob change, and I re-counted cleanly afterwards (7 deferred, control `legacy-alias` 4).",
"mcp_calls": "9 — REST was 403 on every repo-scoped read and `gh` is absent, so the duplicate check for the filed finding used ONE targeted MCP search_issues (3 hits, so a non-empty reading needing no control query). Channel switch declared.",
"open_questions": [
{
"question": "The 3 snake_case legacy aliases (`description_field`, `lookup_filters`, `id_field`) have the SAME reader-side verdict as the three I retired — no reader on this bag — and are kept only on a producer-side argument that no evidence gathered in these two repos can confirm or refute: does any host DataSource outside objectui and objectstack actually hand-feed these spellings onto a field def? Their verdict is now recorded on the table, so this is not blocking, but it is the question that decides whether a future card retires them.",
"options": [
"A. Leave as is. The reader-side verdict is now recorded with the open producer question stated beside it, so the next pass inherits a measurement instead of a silence.",
"B. Commission a producer survey outside these two repos (host DataSource implementations, any published adapter) and retire whichever aliases come back with zero producers.",
"C. Retire all three now on the reader-side measurement alone, treating the legacy-alias verdict as unbacked."
],
"recommendation": "A, and B only if someone independently wants the producer inventory for its own sake. On the four axes: REAL BUSINESS NEED — there is no measured pull for retiring them; they cost three conditional writes per relational column build and nothing else, while the failure mode of being wrong is a silently broken host integration we cannot see from here. LONG-TERM SOUNDNESS — the honest state is now encoded rather than implied; retiring on reader-side evidence would set the precedent that a back-compat verdict can be overturned by a measurement that does not address it. AI-ERROR-RESISTANCE — this is the axis that argues hardest against C: these are runtime-only spellings no spec-compliant producer can emit, so the strict FieldSchema already stops an AI-authored app from declaring them, and removing the runtime tolerance buys no authoring-time safety while risking a live host. STARTUP SCOPE — C spends real risk on tidiness with zero pull, and B spends survey effort on the same. C is explicitly out of scope per the ruling and I did not take it."
}
],
"out_of_scope_findings": [
"filed as #7187: the relational copy-set derivation cannot judge a COPY — its read set is a union over three consumers, two of which are never fed that bag, so read-set membership proves only that some consumer reads a key and not that this bag is how it arrives. Pinned by my ablation 2: re-adding a retired key leaves every DERIVED gate assertion green and turns red only the hand-written pins. Re-scoping it around the cell alone is a design change to #6875's mechanism, which #7166 explicitly declined to make, so it is filed rather than ridden. Unassigned, labels finding + domain:ui.",
"not filed, recorded here: #7165 (the dependsOn uneditable-column bug) was named out of scope by the dispatch and I did not touch it; the `dependsOn` gating behaviour it concerns is still pinned as current behaviour by #7154's suite, which stayed green throughout."
]
}

Generated by Claude Code

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — domain:ui seat (session session_012wwHa4aaFybxXrfmfHioDM), reviewer of record

Verdict: ACCEPT. Arming on green.

Ruling item 3 was answered first and answered properly

I gated this card on one question — is RELATIONAL_META_KEYS public surface? — because a "yes" made the whole thing unlandable at today's tier. It came back internal, and not on one probe but three, each carrying useRowColor as a live control that hit in all three:

  1. not re-exported from the plugin-grid barrel (fully enumerated export list, no export * — which is what makes a negative readable at all);
  2. the built dist/index.d.ts holds zero occurrences of any of the module's symbols;
  3. absent from the built bundle's export list.

⭐ Three independent probes rather than one, and the export * check is the detail that matters: a barrel with a star export would have made probe 1 meaningless while still looking like a clean negative. Clause ② not engaged, correctly established rather than assumed.

⭐ The self-reported flaw is the most valuable line in the report

"in ablation 2 my deferred-count grep was malformed by nested quotes and printed a bogus 0 — the exact 'empty output read as a zero' trap"

Reported rather than buried, with the recovery stated: the mutation was still proven by the injected-marker count and the blob change, so the ablation's validity never depended on the broken probe, and the count was re-taken cleanly afterwards (7 deferred, control legacy-alias 4).

⚠️ This is the third instance of that exact trap in this seat's work today — I hit it verifying #7139 (a malformed control that printed empty beside a subject's 0), and again this morning pointing a pack probe at a barrel file. It is not a rare slip; it is the default failure of any grep written inside nested quoting. A dev who reports it, and who can show the result did not rest on it, is worth more than one who never trips it.

The same instinct shows up on the control-byte scan: the first control was a tab, which proved nothing because 0x09 is deliberately outside the class, so the pattern was re-verified against 0x01 and 0x0b. Noticing that your control is a dud is the whole skill.

The retirement was done through the table, and the gate came out stronger

Copy set 10 → 7, verified at runtime, by flipping the three modern spellings to deferred — the verdict this repo already uses for the class, rather than a new one. And the gate was strengthened, not weakened: every deferred verdict is now mechanically proved spec-declared, and the two sibling pins turn the retired lookupFilters into a live negative assertion on a fixture that still declares it. That last part is what stops this retirement from being invisible to the next reader.

Ablation 2 is the proof the scope call was right: re-adding a retired key leaves every derived gate assertion green and turns red only the hand-written pins (Tests 8 failed | 19 passed). Ablation 1 split the population exactly as predicted (3 failed | 2 passed, B1/B2/B3 red, test A and the premise test green).

The three legacy aliases: A, leave as is — ruling the open question

The snake_case aliases have the same reader-side verdict as the three retired, and are kept only on a producer-side argument no evidence from these two repos can address. A is right, and the axis that decides it is the one the report names: these are runtime-only spellings no spec-compliant producer can emit, so the strict FieldSchema already stops an AI-authored app from declaring them. Removing the runtime tolerance buys no authoring-time safety while risking a live host integration we cannot see. Three conditional writes per column build is not a cost worth that.

C would set a precedent worth refusing explicitly: a back-compat verdict cannot be overturned by a measurement that does not address it. The reader-side census is silent on producers by construction. Recording the verdict beside the open question — which is what shipped — is the durable improvement.

#7187 is the right filing, and it generalises the lesson that produced this card

the relational copy-set derivation cannot judge a COPY — its read set is a union over three consumers, two of which are never fed that bag

⭐ That is the mechanised form of the rule #6875 cost us: a derivation establishes that a consumer reads a key; it does not establish that this bag is how it arrives. And it is now pinned by ablation 2 rather than asserted — the gate's own blindness is demonstrated, not argued. Re-scoping the derivation around the cell alone is a design change to #6875's mechanism, which this card explicitly declined to make, so filing was correct.

Also correct


Nothing to change.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(plugin-grid): retire the three inert fieldMeta copies the cell never reads - #7186

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7166-inert-fieldmeta-copies
Sep 1, 2026
Merged

fix(plugin-grid): retire the three inert fieldMeta copies the cell never reads#7186
os-warren merged 1 commit into
mainfrom
claude/issue-7166-inert-fieldmeta-copies

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7166

ObjectGrid no longer copies descriptionField, lookupColumns or lookupFilters onto a relational column's fieldMeta. No behaviour change — all three still reach the inline lookup picker, by a different seam.

Copy set: 10 keys to 7. Re-measured on 47035ce79 (the card's own numbers were taken on 51449a043, before PR #7167 landed into these files).

Ruling item 3 — the gating measurement: RELATIONAL_META_KEYS is INTERNAL

Clause 2 is not engaged, measured three ways, each with a live control:

proberelationalMetaKeyscontrol (useRowColor)
re-export in src/index.tsx barrelabsent (no export * either)line 30
built dist/index.d.ts0 occurrences of any of its symbolspresent
built dist/index.js export listnot exportedpresent

Every import of ./relationalMetaKeys in the repo lives inside packages/plugin-grid/src/ — the implementation plus two of its own tests. The cross-package hits are prose in docblocks; plugin-dashboard keeps its own independent CELL_RELATIONAL_META_KEYS literal rather than importing this one. Narrowing the copy set therefore moves no published surface.

What was measured

applyRelationalMeta writes onto the fieldMeta that generateColumns hands to CellRenderer as the field prop — 6 JSX passes across the three column-building paths, and nowhere else (as a whole object it otherwise only reaches applyRelationalMeta, which mutates it).

  • LookupCellRenderer reads exactly reference_to, reference, display_field, displayField, reference_field, plus options.
  • UserCellRenderer has zero occurrences of field — control: LookupCellRenderer has 8.

Stated by receiver rather than by count: packages/fields/src/index.tsx, the file holding every cell renderer, contains zero occurrences of the three retired keys, against a control of 22 occurrences of the display_field / displayField / reference_to spellings the cell does read.

Their only readers off a field meta are LookupField and UserField — the two editor widgets — and the editor is not fed this bag. renderCellEditor looks the field up in the object schema and spreads the whole def into the widget ({ name: ctx.column.accessorKey, ...fieldDef }), so every key a def carries reaches LookupField whether or not it is copied. The copies were dead writes: the #6711 / #6874 class arriving from the opposite direction — those keys had no declaration, these have no reader on this path.

The evidence bar this card exists to raise

Two of the three, descriptionField and lookupColumns, were added by #6875 as its fix, classified from a read-set derivation alone. Its third key displayField is genuinely delivered and stays copied — and it is the one that arrived with a rendering test.

A derivation establishes that a consumer READS a key. It does not establish that a given BAG is how the consumer gets it. Recorded in both docblocks.

So this is pinned by rendering, in both directions — new __tests__/relationalMetaCopySet-7166.test.tsx:

  • The cell. Four lookup columns over one referenced record, differing only in the key under test, all resolve the same text; the displayField control column resolves something else. The control is what makes the three zeros readings rather than a fixture that never reached the lookup path.
  • The editor. Each retired key still takes effect in the inline picker with the copy set no longer carrying it — descriptionField drives the secondary line, lookupColumns shapes the picker columns, lookupFilters scopes the candidates — each against a sibling control column that declares nothing. The referenced schema is deliberately two fields, so deriveLookupColumns derives nothing and the controls stay load-bearing.

Scope: 3 keys, not 6

The card measures six keys with no reader on this bag. They are two populations. The three snake_case legacy-alias spellings — description_field, lookup_filters, id_field — are deliberately kept. They are recorded as legacy aliases precisely because a host DataSource outside these repos may hand-feed them; that is a producer-side argument and this measurement is entirely reader-side. Their reader-side verdict is now recorded on the table with the open producer question stated, so the next pass inherits a measurement instead of a silence.

⛔ Reported, not fixed: the derivation gate cannot see this class

The gate's read set is a union over three consumers, two of which are not fed this bag. Membership means "some consumer reads this key", never "this bag is how that consumer gets it" — and only the second claim justifies a copy. All three retired keys remain in the extracted read set, so every derived assertion passes whichever verdict they carry.

Measured, not asserted (ablation 2 below): re-adding lookupFilters to the copy set leaves every derived assertion in the gate green and turns red only the hand-written pins. Re-scoping the derivation around the cell alone, with the editor widgets classified separately, is a design change to #6875's mechanism and is filed rather than made here.

The gate is strengthened, not weakened: every deferred verdict is now mechanically proved spec-declared against the installed FieldSchema, and the sibling pins turn the retired lookupFilters into a live negative assertion on a fixture that still declares it.

Tests

All runs are the canonical repo-root invocation (the repo's vitest guard refuses a package-cwd run as a false green). Union run on the final commit f4f299e23:

pnpm exec vitest run packages/plugin-grid/src packages/fields/src/__tests__/lookupCellDisplayField.test.tsx
Test Files 106 passed (106)
Tests 956 passed (956)

pnpm --filter @object-ui/plugin-grid run type-check — clean (first attempt reported TS2307: Cannot find module '@object-ui/*', which is an unbuilt dependency closure and NOT a result; re-run after pnpm --filter '@object-ui/plugin-grid^...' build). The new test file is confirmed present in the tsconfig.test.json program via --listFiles, so that green does cover it.

eslint . --no-inline-config was run repo-wide, not narrowed: 4100 files, 75 files with errors, all pre-existing and none in this diff; the changed files are E0.

Ablations

Both targets resolve from source (the root config aliases every @object-ui/* to src/, and both are relative imports), so no rebuild leg applies. Each mutation was proven on disk by marker count and blob hash — never an editor's exit code — and each restore by state: git diff HEAD, git diff --cached and git status --short all empty, blob back to the HEAD blob, marker gone.

  1. Break the editor's schema spread (predicted before running: the editor half goes red, the cell half stays green) — Tests 3 failed | 2 passed. Exactly B1/B2/B3 red; test A and the premise test green. The retirement's safety claim is load-bearing.
  2. Re-add lookupFilters to the copy set (predicted: derived gate assertions stay green, hand-written pins go red) — Tests 8 failed | 19 passed. The only derivation-gate failure is the hand-written finding(plugin-grid): 6 of the 10 keys ObjectGrid copies onto a column's fieldMeta have no reader on that bag — the picker keys reach the editor by a different seam #7166 pin; every derived assertion stayed green. The cell and editor tests also stayed green, which is itself the finding: the copy makes no observable difference.

Generated by Claude Code


Generated by Claude Code

…never reads
`applyRelationalMeta` no longer copies `descriptionField`, `lookupColumns` or
`lookupFilters` onto a relational column's `fieldMeta`. No behaviour change:
all three still reach the inline lookup picker, by a different seam.
`fieldMeta` is handed to `<CellRenderer>` as the `field` prop and nowhere else
— six JSX passes across the three column-building paths. A relational column
resolves to `LookupCellRenderer`, which reads exactly `reference_to`,
`reference`, `display_field`, `displayField`, `reference_field` and `options`;
a `user` column resolves to `UserCellRenderer`, which destructures `{ value }`
and reads no field meta at all. Measured by receiver: `fields/src/index.tsx`,
the file holding every cell renderer, contains zero occurrences of the three
retired keys, against a control of 22 for the spellings the cell does read.
Their only readers off a field meta are `LookupField` and `UserField` — the two
editor widgets — which `renderCellEditor` feeds from the schema def directly,
so every key a def carries reaches them whether or not it is copied.
Two of the three were added by objectui#6875, classified from a read-set
derivation alone; its third key `displayField` is genuinely delivered and stays
copied. A derivation establishes that a consumer reads a key, not that a given
bag is how the consumer gets it — recorded in both docblocks.
- New `relationalMetaCopySet-7166.test.tsx` renders both halves: the cell (four
columns differing only in the key under test resolve the same text, with a
`displayField` control that differs) and the editor (each retired key still
takes effect in the picker, each against a control column declaring nothing).
- The three snake_case `legacy-alias` spellings are deliberately kept: their
retention rests on an open producer-side question this reader-side
measurement does not touch. Their verdict is now recorded on the table.
- The derivation gate cannot enforce this retirement — its read set is a union
over three consumers, two of which are not fed this bag — and now says so.
Absence is pinned by an explicit assertion plus the rendering test.
- Gate strengthened: every `deferred` verdict is now mechanically proved
spec-declared, and the sibling pins turn the retired `lookupFilters` into a
live negative assertion on a fixture that still declares it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.2 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-VEpn3hxK.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)68.45KB19.20KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)207.19KB56.03KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

os-dev-report

{
"issue": 7166,
"status": "done",
"branch": "claude/issue-7166-inert-fieldmeta-copies",
"pr": "https://github.com/objectstack-ai/objectui/pull/7186",
"premise_still_valid": true,
"summary": "RULING ITEM 3 FIRST, and it came back INTERNAL, so I proceeded: `RELATIONAL_META_KEYS` is not re-exported from the plugin-grid barrel (fully enumerated export list, no `export *`), the built `dist/index.d.ts` holds zero occurrences of any of the module's symbols, and it is absent from the built bundle's export list — each with `useRowColor` as a live control that hits in all three probes. Clause 2 not engaged. A2.1 re-derived on 47035ce79 and it still holds: the copy set was 10, six had no reader on the bag. Retired the 3 modern spellings (`descriptionField`, `lookupColumns`, `lookupFilters`) through the derivation table by flipping their verdict to `deferred`, the verdict this repo already uses for the same class; copy set now derives to 7, verified at runtime. Kept the 3 snake_case legacy aliases with their reader-side verdict recorded on the table and the unanswered producer question stated beside it. A2.3, A2.4 and A2.5 all confirmed on head by re-measurement, not inherited: 6 `field={fieldMeta}` JSX passes across the three column-building paths and no other whole-object use except `applyRelationalMeta`; `UserCellRenderer` has zero occurrences of `field` against a control of 8 in `LookupCellRenderer`; and `packages/fields/src/index.tsx`, which holds every cell renderer, has zero occurrences of all six keys against a control of 22 for the spellings the cell does read. The gate was strengthened, not weakened. Note for the PM: the platform appended a second copy of the signature footer to the PR body; I left it, because a PATCH edit would downgrade both footers to the bare form and lose the session URL.",
"tests": "All runs used the canonical repo-root vitest invocation (this repo's guard refuses a package-cwd run as a false green) and went through the shared verify lock. UNION on the final commit f4f299e23: `pnpm exec vitest run packages/plugin-grid/src packages/fields/src/__tests__/lookupCellDisplayField.test.tsx` gave `Test Files 106 passed (106)` / `Tests 956 passed (956)`, VERDICT command-exit 0. Baseline before any edit was 5 files / 27 tests; the relational suites after the change are 6 files / 34 tests. `pnpm --filter @object-ui/plugin-grid run type-check` clean — the FIRST attempt returned TS2307 'Cannot find module @object-ui/*' across files I never touched, which is an unbuilt dependency closure and NOT MEASURED rather than a red; re-ran after `pnpm --filter '@object-ui/plugin-grid^...' build` (13 packages) and it passed with the script name echoed, so it was not a zero-match no-op. Confirmed the new test file is actually IN the tsconfig.test.json program with `--listFiles` (1 hit, control `lookupPickerKeys-7154` also 1, 1770 files). LINT: ran `eslint . --no-inline-config` REPO-WIDE rather than narrowing — 4100 files by eslint's own count, 75 files with errors, every one pre-existing and none in this diff; my files are E0 (one unused import it caught was fixed). Control-character self-scan clean over all 6 changed files, with the pattern itself verified against 0x01 and 0x0b after my first control (a tab) proved nothing because 0x09 is deliberately outside the class. ABLATIONS — both targets resolve from SOURCE (the root config aliases every @object-ui/* to src/, and both are relative imports), so no rebuild leg applies and none was needed; each mutation was proven on disk by marker count AND blob hash versus the HEAD blob, never by an editor's exit code, and each restore proven by STATE with `git diff HEAD`, `git diff --cached` and `git status --short` all empty plus the blob back to the HEAD blob and the marker gone. Both ran against the COMMITTED implementation. (1) Break `renderCellEditor`'s schema spread — direction predicted before running as 'the editor half goes red, the cell half stays green' — result `Tests 3 failed | 2 passed`: exactly B1/B2/B3 red, test A and the premise test green. (2) Re-add `lookupFilters` to the copy set — direction predicted as 'the derived gate assertions stay green, only the hand-written pins go red' — result `Tests 8 failed | 19 passed`, and the ONLY derivation-gate failure was my hand-written pin while every derived assertion stayed green. One flaw recorded rather than hidden: in ablation 2 my `deferred`-count grep was malformed by nested quotes and printed a bogus 0 — the exact 'empty output read as a zero' trap; the mutation was still proven by the injected-marker count and the blob change, and I re-counted cleanly afterwards (7 deferred, control `legacy-alias` 4).",
"mcp_calls": "9 — REST was 403 on every repo-scoped read and `gh` is absent, so the duplicate check for the filed finding used ONE targeted MCP search_issues (3 hits, so a non-empty reading needing no control query). Channel switch declared.",
"open_questions": [
{
"question": "The 3 snake_case legacy aliases (`description_field`, `lookup_filters`, `id_field`) have the SAME reader-side verdict as the three I retired — no reader on this bag — and are kept only on a producer-side argument that no evidence gathered in these two repos can confirm or refute: does any host DataSource outside objectui and objectstack actually hand-feed these spellings onto a field def? Their verdict is now recorded on the table, so this is not blocking, but it is the question that decides whether a future card retires them.",
"options": [
"A. Leave as is. The reader-side verdict is now recorded with the open producer question stated beside it, so the next pass inherits a measurement instead of a silence.",
"B. Commission a producer survey outside these two repos (host DataSource implementations, any published adapter) and retire whichever aliases come back with zero producers.",
"C. Retire all three now on the reader-side measurement alone, treating the legacy-alias verdict as unbacked."
],
"recommendation": "A, and B only if someone independently wants the producer inventory for its own sake. On the four axes: REAL BUSINESS NEED — there is no measured pull for retiring them; they cost three conditional writes per relational column build and nothing else, while the failure mode of being wrong is a silently broken host integration we cannot see from here. LONG-TERM SOUNDNESS — the honest state is now encoded rather than implied; retiring on reader-side evidence would set the precedent that a back-compat verdict can be overturned by a measurement that does not address it. AI-ERROR-RESISTANCE — this is the axis that argues hardest against C: these are runtime-only spellings no spec-compliant producer can emit, so the strict FieldSchema already stops an AI-authored app from declaring them, and removing the runtime tolerance buys no authoring-time safety while risking a live host. STARTUP SCOPE — C spends real risk on tidiness with zero pull, and B spends survey effort on the same. C is explicitly out of scope per the ruling and I did not take it."
}
],
"out_of_scope_findings": [
"filed as #7187: the relational copy-set derivation cannot judge a COPY — its read set is a union over three consumers, two of which are never fed that bag, so read-set membership proves only that some consumer reads a key and not that this bag is how it arrives. Pinned by my ablation 2: re-adding a retired key leaves every DERIVED gate assertion green and turns red only the hand-written pins. Re-scoping it around the cell alone is a design change to #6875's mechanism, which #7166 explicitly declined to make, so it is filed rather than ridden. Unassigned, labels finding + domain:ui.",
"not filed, recorded here: #7165 (the dependsOn uneditable-column bug) was named out of scope by the dispatch and I did not touch it; the `dependsOn` gating behaviour it concerns is still pinned as current behaviour by #7154's suite, which stayed green throughout."
]
}

Generated by Claude Code

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — domain:ui seat (session session_012wwHa4aaFybxXrfmfHioDM), reviewer of record

Verdict: ACCEPT. Arming on green.

Ruling item 3 was answered first and answered properly

I gated this card on one question — is RELATIONAL_META_KEYS public surface? — because a "yes" made the whole thing unlandable at today's tier. It came back internal, and not on one probe but three, each carrying useRowColor as a live control that hit in all three:

  1. not re-exported from the plugin-grid barrel (fully enumerated export list, no export * — which is what makes a negative readable at all);
  2. the built dist/index.d.ts holds zero occurrences of any of the module's symbols;
  3. absent from the built bundle's export list.

⭐ Three independent probes rather than one, and the export * check is the detail that matters: a barrel with a star export would have made probe 1 meaningless while still looking like a clean negative. Clause ② not engaged, correctly established rather than assumed.

⭐ The self-reported flaw is the most valuable line in the report

"in ablation 2 my deferred-count grep was malformed by nested quotes and printed a bogus 0 — the exact 'empty output read as a zero' trap"

Reported rather than buried, with the recovery stated: the mutation was still proven by the injected-marker count and the blob change, so the ablation's validity never depended on the broken probe, and the count was re-taken cleanly afterwards (7 deferred, control legacy-alias 4).

⚠️ This is the third instance of that exact trap in this seat's work today — I hit it verifying #7139 (a malformed control that printed empty beside a subject's 0), and again this morning pointing a pack probe at a barrel file. It is not a rare slip; it is the default failure of any grep written inside nested quoting. A dev who reports it, and who can show the result did not rest on it, is worth more than one who never trips it.

The same instinct shows up on the control-byte scan: the first control was a tab, which proved nothing because 0x09 is deliberately outside the class, so the pattern was re-verified against 0x01 and 0x0b. Noticing that your control is a dud is the whole skill.

The retirement was done through the table, and the gate came out stronger

Copy set 10 → 7, verified at runtime, by flipping the three modern spellings to deferred — the verdict this repo already uses for the class, rather than a new one. And the gate was strengthened, not weakened: every deferred verdict is now mechanically proved spec-declared, and the two sibling pins turn the retired lookupFilters into a live negative assertion on a fixture that still declares it. That last part is what stops this retirement from being invisible to the next reader.

Ablation 2 is the proof the scope call was right: re-adding a retired key leaves every derived gate assertion green and turns red only the hand-written pins (Tests 8 failed | 19 passed). Ablation 1 split the population exactly as predicted (3 failed | 2 passed, B1/B2/B3 red, test A and the premise test green).

The three legacy aliases: A, leave as is — ruling the open question

The snake_case aliases have the same reader-side verdict as the three retired, and are kept only on a producer-side argument no evidence from these two repos can address. A is right, and the axis that decides it is the one the report names: these are runtime-only spellings no spec-compliant producer can emit, so the strict FieldSchema already stops an AI-authored app from declaring them. Removing the runtime tolerance buys no authoring-time safety while risking a live host integration we cannot see. Three conditional writes per column build is not a cost worth that.

C would set a precedent worth refusing explicitly: a back-compat verdict cannot be overturned by a measurement that does not address it. The reader-side census is silent on producers by construction. Recording the verdict beside the open question — which is what shipped — is the durable improvement.

#7187 is the right filing, and it generalises the lesson that produced this card

the relational copy-set derivation cannot judge a COPY — its read set is a union over three consumers, two of which are never fed that bag

⭐ That is the mechanised form of the rule #6875 cost us: a derivation establishes that a consumer reads a key; it does not establish that this bag is how it arrives. And it is now pinned by ablation 2 rather than asserted — the gate's own blindness is demonstrated, not argued. Re-scoping the derivation around the cell alone is a design change to #6875's mechanism, which this card explicitly declined to make, so filing was correct.

Also correct


Nothing to change.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-grid): retire the three inert fieldMeta copies the cell never reads - #7186

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7166-inert-fieldmeta-copies
Sep 1, 2026
Merged

fix(plugin-grid): retire the three inert fieldMeta copies the cell never reads#7186
os-warren merged 1 commit into
mainfrom
claude/issue-7166-inert-fieldmeta-copies

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7166

ObjectGrid no longer copies descriptionField, lookupColumns or lookupFilters onto a relational column's fieldMeta. No behaviour change — all three still reach the inline lookup picker, by a different seam.

Copy set: 10 keys to 7. Re-measured on 47035ce79 (the card's own numbers were taken on 51449a043, before PR #7167 landed into these files).

Ruling item 3 — the gating measurement: RELATIONAL_META_KEYS is INTERNAL

Clause 2 is not engaged, measured three ways, each with a live control:

proberelationalMetaKeyscontrol (useRowColor)
re-export in src/index.tsx barrelabsent (no export * either)line 30
built dist/index.d.ts0 occurrences of any of its symbolspresent
built dist/index.js export listnot exportedpresent

Every import of ./relationalMetaKeys in the repo lives inside packages/plugin-grid/src/ — the implementation plus two of its own tests. The cross-package hits are prose in docblocks; plugin-dashboard keeps its own independent CELL_RELATIONAL_META_KEYS literal rather than importing this one. Narrowing the copy set therefore moves no published surface.

What was measured

applyRelationalMeta writes onto the fieldMeta that generateColumns hands to CellRenderer as the field prop — 6 JSX passes across the three column-building paths, and nowhere else (as a whole object it otherwise only reaches applyRelationalMeta, which mutates it).

  • LookupCellRenderer reads exactly reference_to, reference, display_field, displayField, reference_field, plus options.
  • UserCellRenderer has zero occurrences of field — control: LookupCellRenderer has 8.

Stated by receiver rather than by count: packages/fields/src/index.tsx, the file holding every cell renderer, contains zero occurrences of the three retired keys, against a control of 22 occurrences of the display_field / displayField / reference_to spellings the cell does read.

Their only readers off a field meta are LookupField and UserField — the two editor widgets — and the editor is not fed this bag. renderCellEditor looks the field up in the object schema and spreads the whole def into the widget ({ name: ctx.column.accessorKey, ...fieldDef }), so every key a def carries reaches LookupField whether or not it is copied. The copies were dead writes: the #6711 / #6874 class arriving from the opposite direction — those keys had no declaration, these have no reader on this path.

The evidence bar this card exists to raise

Two of the three, descriptionField and lookupColumns, were added by #6875 as its fix, classified from a read-set derivation alone. Its third key displayField is genuinely delivered and stays copied — and it is the one that arrived with a rendering test.

A derivation establishes that a consumer READS a key. It does not establish that a given BAG is how the consumer gets it. Recorded in both docblocks.

So this is pinned by rendering, in both directions — new __tests__/relationalMetaCopySet-7166.test.tsx:

  • The cell. Four lookup columns over one referenced record, differing only in the key under test, all resolve the same text; the displayField control column resolves something else. The control is what makes the three zeros readings rather than a fixture that never reached the lookup path.
  • The editor. Each retired key still takes effect in the inline picker with the copy set no longer carrying it — descriptionField drives the secondary line, lookupColumns shapes the picker columns, lookupFilters scopes the candidates — each against a sibling control column that declares nothing. The referenced schema is deliberately two fields, so deriveLookupColumns derives nothing and the controls stay load-bearing.

Scope: 3 keys, not 6

The card measures six keys with no reader on this bag. They are two populations. The three snake_case legacy-alias spellings — description_field, lookup_filters, id_field — are deliberately kept. They are recorded as legacy aliases precisely because a host DataSource outside these repos may hand-feed them; that is a producer-side argument and this measurement is entirely reader-side. Their reader-side verdict is now recorded on the table with the open producer question stated, so the next pass inherits a measurement instead of a silence.

⛔ Reported, not fixed: the derivation gate cannot see this class

The gate's read set is a union over three consumers, two of which are not fed this bag. Membership means "some consumer reads this key", never "this bag is how that consumer gets it" — and only the second claim justifies a copy. All three retired keys remain in the extracted read set, so every derived assertion passes whichever verdict they carry.

Measured, not asserted (ablation 2 below): re-adding lookupFilters to the copy set leaves every derived assertion in the gate green and turns red only the hand-written pins. Re-scoping the derivation around the cell alone, with the editor widgets classified separately, is a design change to #6875's mechanism and is filed rather than made here.

The gate is strengthened, not weakened: every deferred verdict is now mechanically proved spec-declared against the installed FieldSchema, and the sibling pins turn the retired lookupFilters into a live negative assertion on a fixture that still declares it.

Tests

All runs are the canonical repo-root invocation (the repo's vitest guard refuses a package-cwd run as a false green). Union run on the final commit f4f299e23:

pnpm exec vitest run packages/plugin-grid/src packages/fields/src/__tests__/lookupCellDisplayField.test.tsx
Test Files 106 passed (106)
Tests 956 passed (956)

pnpm --filter @object-ui/plugin-grid run type-check — clean (first attempt reported TS2307: Cannot find module '@object-ui/*', which is an unbuilt dependency closure and NOT a result; re-run after pnpm --filter '@object-ui/plugin-grid^...' build). The new test file is confirmed present in the tsconfig.test.json program via --listFiles, so that green does cover it.

eslint . --no-inline-config was run repo-wide, not narrowed: 4100 files, 75 files with errors, all pre-existing and none in this diff; the changed files are E0.

Ablations

Both targets resolve from source (the root config aliases every @object-ui/* to src/, and both are relative imports), so no rebuild leg applies. Each mutation was proven on disk by marker count and blob hash — never an editor's exit code — and each restore by state: git diff HEAD, git diff --cached and git status --short all empty, blob back to the HEAD blob, marker gone.

  1. Break the editor's schema spread (predicted before running: the editor half goes red, the cell half stays green) — Tests 3 failed | 2 passed. Exactly B1/B2/B3 red; test A and the premise test green. The retirement's safety claim is load-bearing.
  2. Re-add lookupFilters to the copy set (predicted: derived gate assertions stay green, hand-written pins go red) — Tests 8 failed | 19 passed. The only derivation-gate failure is the hand-written finding(plugin-grid): 6 of the 10 keys ObjectGrid copies onto a column's fieldMeta have no reader on that bag — the picker keys reach the editor by a different seam #7166 pin; every derived assertion stayed green. The cell and editor tests also stayed green, which is itself the finding: the copy makes no observable difference.

Generated by Claude Code


Generated by Claude Code

…never reads
`applyRelationalMeta` no longer copies `descriptionField`, `lookupColumns` or
`lookupFilters` onto a relational column's `fieldMeta`. No behaviour change:
all three still reach the inline lookup picker, by a different seam.
`fieldMeta` is handed to `<CellRenderer>` as the `field` prop and nowhere else
— six JSX passes across the three column-building paths. A relational column
resolves to `LookupCellRenderer`, which reads exactly `reference_to`,
`reference`, `display_field`, `displayField`, `reference_field` and `options`;
a `user` column resolves to `UserCellRenderer`, which destructures `{ value }`
and reads no field meta at all. Measured by receiver: `fields/src/index.tsx`,
the file holding every cell renderer, contains zero occurrences of the three
retired keys, against a control of 22 for the spellings the cell does read.
Their only readers off a field meta are `LookupField` and `UserField` — the two
editor widgets — which `renderCellEditor` feeds from the schema def directly,
so every key a def carries reaches them whether or not it is copied.
Two of the three were added by objectui#6875, classified from a read-set
derivation alone; its third key `displayField` is genuinely delivered and stays
copied. A derivation establishes that a consumer reads a key, not that a given
bag is how the consumer gets it — recorded in both docblocks.
- New `relationalMetaCopySet-7166.test.tsx` renders both halves: the cell (four
columns differing only in the key under test resolve the same text, with a
`displayField` control that differs) and the editor (each retired key still
takes effect in the picker, each against a control column declaring nothing).
- The three snake_case `legacy-alias` spellings are deliberately kept: their
retention rests on an open producer-side question this reader-side
measurement does not touch. Their verdict is now recorded on the table.
- The derivation gate cannot enforce this retirement — its read set is a union
over three consumers, two of which are not fed this bag — and now says so.
Absence is pinned by an explicit assertion plus the rendering test.
- Gate strengthened: every `deferred` verdict is now mechanically proved
spec-declared, and the sibling pins turn the retired `lookupFilters` into a
live negative assertion on a fixture that still declares it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.2 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-VEpn3hxK.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)68.45KB19.20KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)207.19KB56.03KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

os-dev-report

{
"issue": 7166,
"status": "done",
"branch": "claude/issue-7166-inert-fieldmeta-copies",
"pr": "https://github.com/objectstack-ai/objectui/pull/7186",
"premise_still_valid": true,
"summary": "RULING ITEM 3 FIRST, and it came back INTERNAL, so I proceeded: `RELATIONAL_META_KEYS` is not re-exported from the plugin-grid barrel (fully enumerated export list, no `export *`), the built `dist/index.d.ts` holds zero occurrences of any of the module's symbols, and it is absent from the built bundle's export list — each with `useRowColor` as a live control that hits in all three probes. Clause 2 not engaged. A2.1 re-derived on 47035ce79 and it still holds: the copy set was 10, six had no reader on the bag. Retired the 3 modern spellings (`descriptionField`, `lookupColumns`, `lookupFilters`) through the derivation table by flipping their verdict to `deferred`, the verdict this repo already uses for the same class; copy set now derives to 7, verified at runtime. Kept the 3 snake_case legacy aliases with their reader-side verdict recorded on the table and the unanswered producer question stated beside it. A2.3, A2.4 and A2.5 all confirmed on head by re-measurement, not inherited: 6 `field={fieldMeta}` JSX passes across the three column-building paths and no other whole-object use except `applyRelationalMeta`; `UserCellRenderer` has zero occurrences of `field` against a control of 8 in `LookupCellRenderer`; and `packages/fields/src/index.tsx`, which holds every cell renderer, has zero occurrences of all six keys against a control of 22 for the spellings the cell does read. The gate was strengthened, not weakened. Note for the PM: the platform appended a second copy of the signature footer to the PR body; I left it, because a PATCH edit would downgrade both footers to the bare form and lose the session URL.",
"tests": "All runs used the canonical repo-root vitest invocation (this repo's guard refuses a package-cwd run as a false green) and went through the shared verify lock. UNION on the final commit f4f299e23: `pnpm exec vitest run packages/plugin-grid/src packages/fields/src/__tests__/lookupCellDisplayField.test.tsx` gave `Test Files 106 passed (106)` / `Tests 956 passed (956)`, VERDICT command-exit 0. Baseline before any edit was 5 files / 27 tests; the relational suites after the change are 6 files / 34 tests. `pnpm --filter @object-ui/plugin-grid run type-check` clean — the FIRST attempt returned TS2307 'Cannot find module @object-ui/*' across files I never touched, which is an unbuilt dependency closure and NOT MEASURED rather than a red; re-ran after `pnpm --filter '@object-ui/plugin-grid^...' build` (13 packages) and it passed with the script name echoed, so it was not a zero-match no-op. Confirmed the new test file is actually IN the tsconfig.test.json program with `--listFiles` (1 hit, control `lookupPickerKeys-7154` also 1, 1770 files). LINT: ran `eslint . --no-inline-config` REPO-WIDE rather than narrowing — 4100 files by eslint's own count, 75 files with errors, every one pre-existing and none in this diff; my files are E0 (one unused import it caught was fixed). Control-character self-scan clean over all 6 changed files, with the pattern itself verified against 0x01 and 0x0b after my first control (a tab) proved nothing because 0x09 is deliberately outside the class. ABLATIONS — both targets resolve from SOURCE (the root config aliases every @object-ui/* to src/, and both are relative imports), so no rebuild leg applies and none was needed; each mutation was proven on disk by marker count AND blob hash versus the HEAD blob, never by an editor's exit code, and each restore proven by STATE with `git diff HEAD`, `git diff --cached` and `git status --short` all empty plus the blob back to the HEAD blob and the marker gone. Both ran against the COMMITTED implementation. (1) Break `renderCellEditor`'s schema spread — direction predicted before running as 'the editor half goes red, the cell half stays green' — result `Tests 3 failed | 2 passed`: exactly B1/B2/B3 red, test A and the premise test green. (2) Re-add `lookupFilters` to the copy set — direction predicted as 'the derived gate assertions stay green, only the hand-written pins go red' — result `Tests 8 failed | 19 passed`, and the ONLY derivation-gate failure was my hand-written pin while every derived assertion stayed green. One flaw recorded rather than hidden: in ablation 2 my `deferred`-count grep was malformed by nested quotes and printed a bogus 0 — the exact 'empty output read as a zero' trap; the mutation was still proven by the injected-marker count and the blob change, and I re-counted cleanly afterwards (7 deferred, control `legacy-alias` 4).",
"mcp_calls": "9 — REST was 403 on every repo-scoped read and `gh` is absent, so the duplicate check for the filed finding used ONE targeted MCP search_issues (3 hits, so a non-empty reading needing no control query). Channel switch declared.",
"open_questions": [
{
"question": "The 3 snake_case legacy aliases (`description_field`, `lookup_filters`, `id_field`) have the SAME reader-side verdict as the three I retired — no reader on this bag — and are kept only on a producer-side argument that no evidence gathered in these two repos can confirm or refute: does any host DataSource outside objectui and objectstack actually hand-feed these spellings onto a field def? Their verdict is now recorded on the table, so this is not blocking, but it is the question that decides whether a future card retires them.",
"options": [
"A. Leave as is. The reader-side verdict is now recorded with the open producer question stated beside it, so the next pass inherits a measurement instead of a silence.",
"B. Commission a producer survey outside these two repos (host DataSource implementations, any published adapter) and retire whichever aliases come back with zero producers.",
"C. Retire all three now on the reader-side measurement alone, treating the legacy-alias verdict as unbacked."
],
"recommendation": "A, and B only if someone independently wants the producer inventory for its own sake. On the four axes: REAL BUSINESS NEED — there is no measured pull for retiring them; they cost three conditional writes per relational column build and nothing else, while the failure mode of being wrong is a silently broken host integration we cannot see from here. LONG-TERM SOUNDNESS — the honest state is now encoded rather than implied; retiring on reader-side evidence would set the precedent that a back-compat verdict can be overturned by a measurement that does not address it. AI-ERROR-RESISTANCE — this is the axis that argues hardest against C: these are runtime-only spellings no spec-compliant producer can emit, so the strict FieldSchema already stops an AI-authored app from declaring them, and removing the runtime tolerance buys no authoring-time safety while risking a live host. STARTUP SCOPE — C spends real risk on tidiness with zero pull, and B spends survey effort on the same. C is explicitly out of scope per the ruling and I did not take it."
}
],
"out_of_scope_findings": [
"filed as #7187: the relational copy-set derivation cannot judge a COPY — its read set is a union over three consumers, two of which are never fed that bag, so read-set membership proves only that some consumer reads a key and not that this bag is how it arrives. Pinned by my ablation 2: re-adding a retired key leaves every DERIVED gate assertion green and turns red only the hand-written pins. Re-scoping it around the cell alone is a design change to #6875's mechanism, which #7166 explicitly declined to make, so it is filed rather than ridden. Unassigned, labels finding + domain:ui.",
"not filed, recorded here: #7165 (the dependsOn uneditable-column bug) was named out of scope by the dispatch and I did not touch it; the `dependsOn` gating behaviour it concerns is still pinned as current behaviour by #7154's suite, which stayed green throughout."
]
}

Generated by Claude Code

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — domain:ui seat (session session_012wwHa4aaFybxXrfmfHioDM), reviewer of record

Verdict: ACCEPT. Arming on green.

Ruling item 3 was answered first and answered properly

I gated this card on one question — is RELATIONAL_META_KEYS public surface? — because a "yes" made the whole thing unlandable at today's tier. It came back internal, and not on one probe but three, each carrying useRowColor as a live control that hit in all three:

  1. not re-exported from the plugin-grid barrel (fully enumerated export list, no export * — which is what makes a negative readable at all);
  2. the built dist/index.d.ts holds zero occurrences of any of the module's symbols;
  3. absent from the built bundle's export list.

⭐ Three independent probes rather than one, and the export * check is the detail that matters: a barrel with a star export would have made probe 1 meaningless while still looking like a clean negative. Clause ② not engaged, correctly established rather than assumed.

⭐ The self-reported flaw is the most valuable line in the report

"in ablation 2 my deferred-count grep was malformed by nested quotes and printed a bogus 0 — the exact 'empty output read as a zero' trap"

Reported rather than buried, with the recovery stated: the mutation was still proven by the injected-marker count and the blob change, so the ablation's validity never depended on the broken probe, and the count was re-taken cleanly afterwards (7 deferred, control legacy-alias 4).

⚠️ This is the third instance of that exact trap in this seat's work today — I hit it verifying #7139 (a malformed control that printed empty beside a subject's 0), and again this morning pointing a pack probe at a barrel file. It is not a rare slip; it is the default failure of any grep written inside nested quoting. A dev who reports it, and who can show the result did not rest on it, is worth more than one who never trips it.

The same instinct shows up on the control-byte scan: the first control was a tab, which proved nothing because 0x09 is deliberately outside the class, so the pattern was re-verified against 0x01 and 0x0b. Noticing that your control is a dud is the whole skill.

The retirement was done through the table, and the gate came out stronger

Copy set 10 → 7, verified at runtime, by flipping the three modern spellings to deferred — the verdict this repo already uses for the class, rather than a new one. And the gate was strengthened, not weakened: every deferred verdict is now mechanically proved spec-declared, and the two sibling pins turn the retired lookupFilters into a live negative assertion on a fixture that still declares it. That last part is what stops this retirement from being invisible to the next reader.

Ablation 2 is the proof the scope call was right: re-adding a retired key leaves every derived gate assertion green and turns red only the hand-written pins (Tests 8 failed | 19 passed). Ablation 1 split the population exactly as predicted (3 failed | 2 passed, B1/B2/B3 red, test A and the premise test green).

The three legacy aliases: A, leave as is — ruling the open question

The snake_case aliases have the same reader-side verdict as the three retired, and are kept only on a producer-side argument no evidence from these two repos can address. A is right, and the axis that decides it is the one the report names: these are runtime-only spellings no spec-compliant producer can emit, so the strict FieldSchema already stops an AI-authored app from declaring them. Removing the runtime tolerance buys no authoring-time safety while risking a live host integration we cannot see. Three conditional writes per column build is not a cost worth that.

C would set a precedent worth refusing explicitly: a back-compat verdict cannot be overturned by a measurement that does not address it. The reader-side census is silent on producers by construction. Recording the verdict beside the open question — which is what shipped — is the durable improvement.

#7187 is the right filing, and it generalises the lesson that produced this card

the relational copy-set derivation cannot judge a COPY — its read set is a union over three consumers, two of which are never fed that bag

⭐ That is the mechanised form of the rule #6875 cost us: a derivation establishes that a consumer reads a key; it does not establish that this bag is how it arrives. And it is now pinned by ablation 2 rather than asserted — the gate's own blindness is demonstrated, not argued. Re-scoping the derivation around the cell alone is a design change to #6875's mechanism, which this card explicitly declined to make, so filing was correct.

Also correct


Nothing to change.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-grid): retire the three inert fieldMeta copies the cell never reads - #7186

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7166-inert-fieldmeta-copies
Sep 1, 2026
Merged

fix(plugin-grid): retire the three inert fieldMeta copies the cell never reads#7186
os-warren merged 1 commit into
mainfrom
claude/issue-7166-inert-fieldmeta-copies

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7166

ObjectGrid no longer copies descriptionField, lookupColumns or lookupFilters onto a relational column's fieldMeta. No behaviour change — all three still reach the inline lookup picker, by a different seam.

Copy set: 10 keys to 7. Re-measured on 47035ce79 (the card's own numbers were taken on 51449a043, before PR #7167 landed into these files).

Ruling item 3 — the gating measurement: RELATIONAL_META_KEYS is INTERNAL

Clause 2 is not engaged, measured three ways, each with a live control:

proberelationalMetaKeyscontrol (useRowColor)
re-export in src/index.tsx barrelabsent (no export * either)line 30
built dist/index.d.ts0 occurrences of any of its symbolspresent
built dist/index.js export listnot exportedpresent

Every import of ./relationalMetaKeys in the repo lives inside packages/plugin-grid/src/ — the implementation plus two of its own tests. The cross-package hits are prose in docblocks; plugin-dashboard keeps its own independent CELL_RELATIONAL_META_KEYS literal rather than importing this one. Narrowing the copy set therefore moves no published surface.

What was measured

applyRelationalMeta writes onto the fieldMeta that generateColumns hands to CellRenderer as the field prop — 6 JSX passes across the three column-building paths, and nowhere else (as a whole object it otherwise only reaches applyRelationalMeta, which mutates it).

  • LookupCellRenderer reads exactly reference_to, reference, display_field, displayField, reference_field, plus options.
  • UserCellRenderer has zero occurrences of field — control: LookupCellRenderer has 8.

Stated by receiver rather than by count: packages/fields/src/index.tsx, the file holding every cell renderer, contains zero occurrences of the three retired keys, against a control of 22 occurrences of the display_field / displayField / reference_to spellings the cell does read.

Their only readers off a field meta are LookupField and UserField — the two editor widgets — and the editor is not fed this bag. renderCellEditor looks the field up in the object schema and spreads the whole def into the widget ({ name: ctx.column.accessorKey, ...fieldDef }), so every key a def carries reaches LookupField whether or not it is copied. The copies were dead writes: the #6711 / #6874 class arriving from the opposite direction — those keys had no declaration, these have no reader on this path.

The evidence bar this card exists to raise

Two of the three, descriptionField and lookupColumns, were added by #6875 as its fix, classified from a read-set derivation alone. Its third key displayField is genuinely delivered and stays copied — and it is the one that arrived with a rendering test.

A derivation establishes that a consumer READS a key. It does not establish that a given BAG is how the consumer gets it. Recorded in both docblocks.

So this is pinned by rendering, in both directions — new __tests__/relationalMetaCopySet-7166.test.tsx:

  • The cell. Four lookup columns over one referenced record, differing only in the key under test, all resolve the same text; the displayField control column resolves something else. The control is what makes the three zeros readings rather than a fixture that never reached the lookup path.
  • The editor. Each retired key still takes effect in the inline picker with the copy set no longer carrying it — descriptionField drives the secondary line, lookupColumns shapes the picker columns, lookupFilters scopes the candidates — each against a sibling control column that declares nothing. The referenced schema is deliberately two fields, so deriveLookupColumns derives nothing and the controls stay load-bearing.

Scope: 3 keys, not 6

The card measures six keys with no reader on this bag. They are two populations. The three snake_case legacy-alias spellings — description_field, lookup_filters, id_field — are deliberately kept. They are recorded as legacy aliases precisely because a host DataSource outside these repos may hand-feed them; that is a producer-side argument and this measurement is entirely reader-side. Their reader-side verdict is now recorded on the table with the open producer question stated, so the next pass inherits a measurement instead of a silence.

⛔ Reported, not fixed: the derivation gate cannot see this class

The gate's read set is a union over three consumers, two of which are not fed this bag. Membership means "some consumer reads this key", never "this bag is how that consumer gets it" — and only the second claim justifies a copy. All three retired keys remain in the extracted read set, so every derived assertion passes whichever verdict they carry.

Measured, not asserted (ablation 2 below): re-adding lookupFilters to the copy set leaves every derived assertion in the gate green and turns red only the hand-written pins. Re-scoping the derivation around the cell alone, with the editor widgets classified separately, is a design change to #6875's mechanism and is filed rather than made here.

The gate is strengthened, not weakened: every deferred verdict is now mechanically proved spec-declared against the installed FieldSchema, and the sibling pins turn the retired lookupFilters into a live negative assertion on a fixture that still declares it.

Tests

All runs are the canonical repo-root invocation (the repo's vitest guard refuses a package-cwd run as a false green). Union run on the final commit f4f299e23:

pnpm exec vitest run packages/plugin-grid/src packages/fields/src/__tests__/lookupCellDisplayField.test.tsx
Test Files 106 passed (106)
Tests 956 passed (956)

pnpm --filter @object-ui/plugin-grid run type-check — clean (first attempt reported TS2307: Cannot find module '@object-ui/*', which is an unbuilt dependency closure and NOT a result; re-run after pnpm --filter '@object-ui/plugin-grid^...' build). The new test file is confirmed present in the tsconfig.test.json program via --listFiles, so that green does cover it.

eslint . --no-inline-config was run repo-wide, not narrowed: 4100 files, 75 files with errors, all pre-existing and none in this diff; the changed files are E0.

Ablations

Both targets resolve from source (the root config aliases every @object-ui/* to src/, and both are relative imports), so no rebuild leg applies. Each mutation was proven on disk by marker count and blob hash — never an editor's exit code — and each restore by state: git diff HEAD, git diff --cached and git status --short all empty, blob back to the HEAD blob, marker gone.

  1. Break the editor's schema spread (predicted before running: the editor half goes red, the cell half stays green) — Tests 3 failed | 2 passed. Exactly B1/B2/B3 red; test A and the premise test green. The retirement's safety claim is load-bearing.
  2. Re-add lookupFilters to the copy set (predicted: derived gate assertions stay green, hand-written pins go red) — Tests 8 failed | 19 passed. The only derivation-gate failure is the hand-written finding(plugin-grid): 6 of the 10 keys ObjectGrid copies onto a column's fieldMeta have no reader on that bag — the picker keys reach the editor by a different seam #7166 pin; every derived assertion stayed green. The cell and editor tests also stayed green, which is itself the finding: the copy makes no observable difference.

Generated by Claude Code


Generated by Claude Code

…never reads
`applyRelationalMeta` no longer copies `descriptionField`, `lookupColumns` or
`lookupFilters` onto a relational column's `fieldMeta`. No behaviour change:
all three still reach the inline lookup picker, by a different seam.
`fieldMeta` is handed to `<CellRenderer>` as the `field` prop and nowhere else
— six JSX passes across the three column-building paths. A relational column
resolves to `LookupCellRenderer`, which reads exactly `reference_to`,
`reference`, `display_field`, `displayField`, `reference_field` and `options`;
a `user` column resolves to `UserCellRenderer`, which destructures `{ value }`
and reads no field meta at all. Measured by receiver: `fields/src/index.tsx`,
the file holding every cell renderer, contains zero occurrences of the three
retired keys, against a control of 22 for the spellings the cell does read.
Their only readers off a field meta are `LookupField` and `UserField` — the two
editor widgets — which `renderCellEditor` feeds from the schema def directly,
so every key a def carries reaches them whether or not it is copied.
Two of the three were added by objectui#6875, classified from a read-set
derivation alone; its third key `displayField` is genuinely delivered and stays
copied. A derivation establishes that a consumer reads a key, not that a given
bag is how the consumer gets it — recorded in both docblocks.
- New `relationalMetaCopySet-7166.test.tsx` renders both halves: the cell (four
columns differing only in the key under test resolve the same text, with a
`displayField` control that differs) and the editor (each retired key still
takes effect in the picker, each against a control column declaring nothing).
- The three snake_case `legacy-alias` spellings are deliberately kept: their
retention rests on an open producer-side question this reader-side
measurement does not touch. Their verdict is now recorded on the table.
- The derivation gate cannot enforce this retirement — its read set is a union
over three consumers, two of which are not fed this bag — and now says so.
Absence is pinned by an explicit assertion plus the rendering test.
- Gate strengthened: every `deferred` verdict is now mechanically proved
spec-declared, and the sibling pins turn the retired `lookupFilters` into a
live negative assertion on a fixture that still declares it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.2 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-VEpn3hxK.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)68.45KB19.20KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)207.19KB56.03KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

os-dev-report

{
"issue": 7166,
"status": "done",
"branch": "claude/issue-7166-inert-fieldmeta-copies",
"pr": "https://github.com/objectstack-ai/objectui/pull/7186",
"premise_still_valid": true,
"summary": "RULING ITEM 3 FIRST, and it came back INTERNAL, so I proceeded: `RELATIONAL_META_KEYS` is not re-exported from the plugin-grid barrel (fully enumerated export list, no `export *`), the built `dist/index.d.ts` holds zero occurrences of any of the module's symbols, and it is absent from the built bundle's export list — each with `useRowColor` as a live control that hits in all three probes. Clause 2 not engaged. A2.1 re-derived on 47035ce79 and it still holds: the copy set was 10, six had no reader on the bag. Retired the 3 modern spellings (`descriptionField`, `lookupColumns`, `lookupFilters`) through the derivation table by flipping their verdict to `deferred`, the verdict this repo already uses for the same class; copy set now derives to 7, verified at runtime. Kept the 3 snake_case legacy aliases with their reader-side verdict recorded on the table and the unanswered producer question stated beside it. A2.3, A2.4 and A2.5 all confirmed on head by re-measurement, not inherited: 6 `field={fieldMeta}` JSX passes across the three column-building paths and no other whole-object use except `applyRelationalMeta`; `UserCellRenderer` has zero occurrences of `field` against a control of 8 in `LookupCellRenderer`; and `packages/fields/src/index.tsx`, which holds every cell renderer, has zero occurrences of all six keys against a control of 22 for the spellings the cell does read. The gate was strengthened, not weakened. Note for the PM: the platform appended a second copy of the signature footer to the PR body; I left it, because a PATCH edit would downgrade both footers to the bare form and lose the session URL.",
"tests": "All runs used the canonical repo-root vitest invocation (this repo's guard refuses a package-cwd run as a false green) and went through the shared verify lock. UNION on the final commit f4f299e23: `pnpm exec vitest run packages/plugin-grid/src packages/fields/src/__tests__/lookupCellDisplayField.test.tsx` gave `Test Files 106 passed (106)` / `Tests 956 passed (956)`, VERDICT command-exit 0. Baseline before any edit was 5 files / 27 tests; the relational suites after the change are 6 files / 34 tests. `pnpm --filter @object-ui/plugin-grid run type-check` clean — the FIRST attempt returned TS2307 'Cannot find module @object-ui/*' across files I never touched, which is an unbuilt dependency closure and NOT MEASURED rather than a red; re-ran after `pnpm --filter '@object-ui/plugin-grid^...' build` (13 packages) and it passed with the script name echoed, so it was not a zero-match no-op. Confirmed the new test file is actually IN the tsconfig.test.json program with `--listFiles` (1 hit, control `lookupPickerKeys-7154` also 1, 1770 files). LINT: ran `eslint . --no-inline-config` REPO-WIDE rather than narrowing — 4100 files by eslint's own count, 75 files with errors, every one pre-existing and none in this diff; my files are E0 (one unused import it caught was fixed). Control-character self-scan clean over all 6 changed files, with the pattern itself verified against 0x01 and 0x0b after my first control (a tab) proved nothing because 0x09 is deliberately outside the class. ABLATIONS — both targets resolve from SOURCE (the root config aliases every @object-ui/* to src/, and both are relative imports), so no rebuild leg applies and none was needed; each mutation was proven on disk by marker count AND blob hash versus the HEAD blob, never by an editor's exit code, and each restore proven by STATE with `git diff HEAD`, `git diff --cached` and `git status --short` all empty plus the blob back to the HEAD blob and the marker gone. Both ran against the COMMITTED implementation. (1) Break `renderCellEditor`'s schema spread — direction predicted before running as 'the editor half goes red, the cell half stays green' — result `Tests 3 failed | 2 passed`: exactly B1/B2/B3 red, test A and the premise test green. (2) Re-add `lookupFilters` to the copy set — direction predicted as 'the derived gate assertions stay green, only the hand-written pins go red' — result `Tests 8 failed | 19 passed`, and the ONLY derivation-gate failure was my hand-written pin while every derived assertion stayed green. One flaw recorded rather than hidden: in ablation 2 my `deferred`-count grep was malformed by nested quotes and printed a bogus 0 — the exact 'empty output read as a zero' trap; the mutation was still proven by the injected-marker count and the blob change, and I re-counted cleanly afterwards (7 deferred, control `legacy-alias` 4).",
"mcp_calls": "9 — REST was 403 on every repo-scoped read and `gh` is absent, so the duplicate check for the filed finding used ONE targeted MCP search_issues (3 hits, so a non-empty reading needing no control query). Channel switch declared.",
"open_questions": [
{
"question": "The 3 snake_case legacy aliases (`description_field`, `lookup_filters`, `id_field`) have the SAME reader-side verdict as the three I retired — no reader on this bag — and are kept only on a producer-side argument that no evidence gathered in these two repos can confirm or refute: does any host DataSource outside objectui and objectstack actually hand-feed these spellings onto a field def? Their verdict is now recorded on the table, so this is not blocking, but it is the question that decides whether a future card retires them.",
"options": [
"A. Leave as is. The reader-side verdict is now recorded with the open producer question stated beside it, so the next pass inherits a measurement instead of a silence.",
"B. Commission a producer survey outside these two repos (host DataSource implementations, any published adapter) and retire whichever aliases come back with zero producers.",
"C. Retire all three now on the reader-side measurement alone, treating the legacy-alias verdict as unbacked."
],
"recommendation": "A, and B only if someone independently wants the producer inventory for its own sake. On the four axes: REAL BUSINESS NEED — there is no measured pull for retiring them; they cost three conditional writes per relational column build and nothing else, while the failure mode of being wrong is a silently broken host integration we cannot see from here. LONG-TERM SOUNDNESS — the honest state is now encoded rather than implied; retiring on reader-side evidence would set the precedent that a back-compat verdict can be overturned by a measurement that does not address it. AI-ERROR-RESISTANCE — this is the axis that argues hardest against C: these are runtime-only spellings no spec-compliant producer can emit, so the strict FieldSchema already stops an AI-authored app from declaring them, and removing the runtime tolerance buys no authoring-time safety while risking a live host. STARTUP SCOPE — C spends real risk on tidiness with zero pull, and B spends survey effort on the same. C is explicitly out of scope per the ruling and I did not take it."
}
],
"out_of_scope_findings": [
"filed as #7187: the relational copy-set derivation cannot judge a COPY — its read set is a union over three consumers, two of which are never fed that bag, so read-set membership proves only that some consumer reads a key and not that this bag is how it arrives. Pinned by my ablation 2: re-adding a retired key leaves every DERIVED gate assertion green and turns red only the hand-written pins. Re-scoping it around the cell alone is a design change to #6875's mechanism, which #7166 explicitly declined to make, so it is filed rather than ridden. Unassigned, labels finding + domain:ui.",
"not filed, recorded here: #7165 (the dependsOn uneditable-column bug) was named out of scope by the dispatch and I did not touch it; the `dependsOn` gating behaviour it concerns is still pinned as current behaviour by #7154's suite, which stayed green throughout."
]
}

Generated by Claude Code

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — domain:ui seat (session session_012wwHa4aaFybxXrfmfHioDM), reviewer of record

Verdict: ACCEPT. Arming on green.

Ruling item 3 was answered first and answered properly

I gated this card on one question — is RELATIONAL_META_KEYS public surface? — because a "yes" made the whole thing unlandable at today's tier. It came back internal, and not on one probe but three, each carrying useRowColor as a live control that hit in all three:

  1. not re-exported from the plugin-grid barrel (fully enumerated export list, no export * — which is what makes a negative readable at all);
  2. the built dist/index.d.ts holds zero occurrences of any of the module's symbols;
  3. absent from the built bundle's export list.

⭐ Three independent probes rather than one, and the export * check is the detail that matters: a barrel with a star export would have made probe 1 meaningless while still looking like a clean negative. Clause ② not engaged, correctly established rather than assumed.

⭐ The self-reported flaw is the most valuable line in the report

"in ablation 2 my deferred-count grep was malformed by nested quotes and printed a bogus 0 — the exact 'empty output read as a zero' trap"

Reported rather than buried, with the recovery stated: the mutation was still proven by the injected-marker count and the blob change, so the ablation's validity never depended on the broken probe, and the count was re-taken cleanly afterwards (7 deferred, control legacy-alias 4).

⚠️ This is the third instance of that exact trap in this seat's work today — I hit it verifying #7139 (a malformed control that printed empty beside a subject's 0), and again this morning pointing a pack probe at a barrel file. It is not a rare slip; it is the default failure of any grep written inside nested quoting. A dev who reports it, and who can show the result did not rest on it, is worth more than one who never trips it.

The same instinct shows up on the control-byte scan: the first control was a tab, which proved nothing because 0x09 is deliberately outside the class, so the pattern was re-verified against 0x01 and 0x0b. Noticing that your control is a dud is the whole skill.

The retirement was done through the table, and the gate came out stronger

Copy set 10 → 7, verified at runtime, by flipping the three modern spellings to deferred — the verdict this repo already uses for the class, rather than a new one. And the gate was strengthened, not weakened: every deferred verdict is now mechanically proved spec-declared, and the two sibling pins turn the retired lookupFilters into a live negative assertion on a fixture that still declares it. That last part is what stops this retirement from being invisible to the next reader.

Ablation 2 is the proof the scope call was right: re-adding a retired key leaves every derived gate assertion green and turns red only the hand-written pins (Tests 8 failed | 19 passed). Ablation 1 split the population exactly as predicted (3 failed | 2 passed, B1/B2/B3 red, test A and the premise test green).

The three legacy aliases: A, leave as is — ruling the open question

The snake_case aliases have the same reader-side verdict as the three retired, and are kept only on a producer-side argument no evidence from these two repos can address. A is right, and the axis that decides it is the one the report names: these are runtime-only spellings no spec-compliant producer can emit, so the strict FieldSchema already stops an AI-authored app from declaring them. Removing the runtime tolerance buys no authoring-time safety while risking a live host integration we cannot see. Three conditional writes per column build is not a cost worth that.

C would set a precedent worth refusing explicitly: a back-compat verdict cannot be overturned by a measurement that does not address it. The reader-side census is silent on producers by construction. Recording the verdict beside the open question — which is what shipped — is the durable improvement.

#7187 is the right filing, and it generalises the lesson that produced this card

the relational copy-set derivation cannot judge a COPY — its read set is a union over three consumers, two of which are never fed that bag

⭐ That is the mechanised form of the rule #6875 cost us: a derivation establishes that a consumer reads a key; it does not establish that this bag is how it arrives. And it is now pinned by ablation 2 rather than asserted — the gate's own blindness is demonstrated, not argued. Re-scoping the derivation around the cell alone is a design change to #6875's mechanism, which this card explicitly declined to make, so filing was correct.

Also correct


Nothing to change.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(plugin-grid): retire the three inert fieldMeta copies the cell never reads - #7186

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7166-inert-fieldmeta-copies
Sep 1, 2026
Merged

fix(plugin-grid): retire the three inert fieldMeta copies the cell never reads#7186
os-warren merged 1 commit into
mainfrom
claude/issue-7166-inert-fieldmeta-copies

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7166

ObjectGrid no longer copies descriptionField, lookupColumns or lookupFilters onto a relational column's fieldMeta. No behaviour change — all three still reach the inline lookup picker, by a different seam.

Copy set: 10 keys to 7. Re-measured on 47035ce79 (the card's own numbers were taken on 51449a043, before PR #7167 landed into these files).

Ruling item 3 — the gating measurement: RELATIONAL_META_KEYS is INTERNAL

Clause 2 is not engaged, measured three ways, each with a live control:

proberelationalMetaKeyscontrol (useRowColor)
re-export in src/index.tsx barrelabsent (no export * either)line 30
built dist/index.d.ts0 occurrences of any of its symbolspresent
built dist/index.js export listnot exportedpresent

Every import of ./relationalMetaKeys in the repo lives inside packages/plugin-grid/src/ — the implementation plus two of its own tests. The cross-package hits are prose in docblocks; plugin-dashboard keeps its own independent CELL_RELATIONAL_META_KEYS literal rather than importing this one. Narrowing the copy set therefore moves no published surface.

What was measured

applyRelationalMeta writes onto the fieldMeta that generateColumns hands to CellRenderer as the field prop — 6 JSX passes across the three column-building paths, and nowhere else (as a whole object it otherwise only reaches applyRelationalMeta, which mutates it).

  • LookupCellRenderer reads exactly reference_to, reference, display_field, displayField, reference_field, plus options.
  • UserCellRenderer has zero occurrences of field — control: LookupCellRenderer has 8.

Stated by receiver rather than by count: packages/fields/src/index.tsx, the file holding every cell renderer, contains zero occurrences of the three retired keys, against a control of 22 occurrences of the display_field / displayField / reference_to spellings the cell does read.

Their only readers off a field meta are LookupField and UserField — the two editor widgets — and the editor is not fed this bag. renderCellEditor looks the field up in the object schema and spreads the whole def into the widget ({ name: ctx.column.accessorKey, ...fieldDef }), so every key a def carries reaches LookupField whether or not it is copied. The copies were dead writes: the #6711 / #6874 class arriving from the opposite direction — those keys had no declaration, these have no reader on this path.

The evidence bar this card exists to raise

Two of the three, descriptionField and lookupColumns, were added by #6875 as its fix, classified from a read-set derivation alone. Its third key displayField is genuinely delivered and stays copied — and it is the one that arrived with a rendering test.

A derivation establishes that a consumer READS a key. It does not establish that a given BAG is how the consumer gets it. Recorded in both docblocks.

So this is pinned by rendering, in both directions — new __tests__/relationalMetaCopySet-7166.test.tsx:

  • The cell. Four lookup columns over one referenced record, differing only in the key under test, all resolve the same text; the displayField control column resolves something else. The control is what makes the three zeros readings rather than a fixture that never reached the lookup path.
  • The editor. Each retired key still takes effect in the inline picker with the copy set no longer carrying it — descriptionField drives the secondary line, lookupColumns shapes the picker columns, lookupFilters scopes the candidates — each against a sibling control column that declares nothing. The referenced schema is deliberately two fields, so deriveLookupColumns derives nothing and the controls stay load-bearing.

Scope: 3 keys, not 6

The card measures six keys with no reader on this bag. They are two populations. The three snake_case legacy-alias spellings — description_field, lookup_filters, id_field — are deliberately kept. They are recorded as legacy aliases precisely because a host DataSource outside these repos may hand-feed them; that is a producer-side argument and this measurement is entirely reader-side. Their reader-side verdict is now recorded on the table with the open producer question stated, so the next pass inherits a measurement instead of a silence.

⛔ Reported, not fixed: the derivation gate cannot see this class

The gate's read set is a union over three consumers, two of which are not fed this bag. Membership means "some consumer reads this key", never "this bag is how that consumer gets it" — and only the second claim justifies a copy. All three retired keys remain in the extracted read set, so every derived assertion passes whichever verdict they carry.

Measured, not asserted (ablation 2 below): re-adding lookupFilters to the copy set leaves every derived assertion in the gate green and turns red only the hand-written pins. Re-scoping the derivation around the cell alone, with the editor widgets classified separately, is a design change to #6875's mechanism and is filed rather than made here.

The gate is strengthened, not weakened: every deferred verdict is now mechanically proved spec-declared against the installed FieldSchema, and the sibling pins turn the retired lookupFilters into a live negative assertion on a fixture that still declares it.

Tests

All runs are the canonical repo-root invocation (the repo's vitest guard refuses a package-cwd run as a false green). Union run on the final commit f4f299e23:

pnpm exec vitest run packages/plugin-grid/src packages/fields/src/__tests__/lookupCellDisplayField.test.tsx
Test Files 106 passed (106)
Tests 956 passed (956)

pnpm --filter @object-ui/plugin-grid run type-check — clean (first attempt reported TS2307: Cannot find module '@object-ui/*', which is an unbuilt dependency closure and NOT a result; re-run after pnpm --filter '@object-ui/plugin-grid^...' build). The new test file is confirmed present in the tsconfig.test.json program via --listFiles, so that green does cover it.

eslint . --no-inline-config was run repo-wide, not narrowed: 4100 files, 75 files with errors, all pre-existing and none in this diff; the changed files are E0.

Ablations

Both targets resolve from source (the root config aliases every @object-ui/* to src/, and both are relative imports), so no rebuild leg applies. Each mutation was proven on disk by marker count and blob hash — never an editor's exit code — and each restore by state: git diff HEAD, git diff --cached and git status --short all empty, blob back to the HEAD blob, marker gone.

  1. Break the editor's schema spread (predicted before running: the editor half goes red, the cell half stays green) — Tests 3 failed | 2 passed. Exactly B1/B2/B3 red; test A and the premise test green. The retirement's safety claim is load-bearing.
  2. Re-add lookupFilters to the copy set (predicted: derived gate assertions stay green, hand-written pins go red) — Tests 8 failed | 19 passed. The only derivation-gate failure is the hand-written finding(plugin-grid): 6 of the 10 keys ObjectGrid copies onto a column's fieldMeta have no reader on that bag — the picker keys reach the editor by a different seam #7166 pin; every derived assertion stayed green. The cell and editor tests also stayed green, which is itself the finding: the copy makes no observable difference.

Generated by Claude Code


Generated by Claude Code

…never reads
`applyRelationalMeta` no longer copies `descriptionField`, `lookupColumns` or
`lookupFilters` onto a relational column's `fieldMeta`. No behaviour change:
all three still reach the inline lookup picker, by a different seam.
`fieldMeta` is handed to `<CellRenderer>` as the `field` prop and nowhere else
— six JSX passes across the three column-building paths. A relational column
resolves to `LookupCellRenderer`, which reads exactly `reference_to`,
`reference`, `display_field`, `displayField`, `reference_field` and `options`;
a `user` column resolves to `UserCellRenderer`, which destructures `{ value }`
and reads no field meta at all. Measured by receiver: `fields/src/index.tsx`,
the file holding every cell renderer, contains zero occurrences of the three
retired keys, against a control of 22 for the spellings the cell does read.
Their only readers off a field meta are `LookupField` and `UserField` — the two
editor widgets — which `renderCellEditor` feeds from the schema def directly,
so every key a def carries reaches them whether or not it is copied.
Two of the three were added by objectui#6875, classified from a read-set
derivation alone; its third key `displayField` is genuinely delivered and stays
copied. A derivation establishes that a consumer reads a key, not that a given
bag is how the consumer gets it — recorded in both docblocks.
- New `relationalMetaCopySet-7166.test.tsx` renders both halves: the cell (four
columns differing only in the key under test resolve the same text, with a
`displayField` control that differs) and the editor (each retired key still
takes effect in the picker, each against a control column declaring nothing).
- The three snake_case `legacy-alias` spellings are deliberately kept: their
retention rests on an open producer-side question this reader-side
measurement does not touch. Their verdict is now recorded on the table.
- The derivation gate cannot enforce this retirement — its read set is a union
over three consumers, two of which are not fed this bag — and now says so.
Absence is pinned by an explicit assertion plus the rendering test.
- Gate strengthened: every `deferred` verdict is now mechanically proved
spec-declared, and the sibling pins turn the retired `lookupFilters` into a
live negative assertion on a fixture that still declares it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.2 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-VEpn3hxK.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)68.45KB19.20KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)207.19KB56.03KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

os-dev-report

{
"issue": 7166,
"status": "done",
"branch": "claude/issue-7166-inert-fieldmeta-copies",
"pr": "https://github.com/objectstack-ai/objectui/pull/7186",
"premise_still_valid": true,
"summary": "RULING ITEM 3 FIRST, and it came back INTERNAL, so I proceeded: `RELATIONAL_META_KEYS` is not re-exported from the plugin-grid barrel (fully enumerated export list, no `export *`), the built `dist/index.d.ts` holds zero occurrences of any of the module's symbols, and it is absent from the built bundle's export list — each with `useRowColor` as a live control that hits in all three probes. Clause 2 not engaged. A2.1 re-derived on 47035ce79 and it still holds: the copy set was 10, six had no reader on the bag. Retired the 3 modern spellings (`descriptionField`, `lookupColumns`, `lookupFilters`) through the derivation table by flipping their verdict to `deferred`, the verdict this repo already uses for the same class; copy set now derives to 7, verified at runtime. Kept the 3 snake_case legacy aliases with their reader-side verdict recorded on the table and the unanswered producer question stated beside it. A2.3, A2.4 and A2.5 all confirmed on head by re-measurement, not inherited: 6 `field={fieldMeta}` JSX passes across the three column-building paths and no other whole-object use except `applyRelationalMeta`; `UserCellRenderer` has zero occurrences of `field` against a control of 8 in `LookupCellRenderer`; and `packages/fields/src/index.tsx`, which holds every cell renderer, has zero occurrences of all six keys against a control of 22 for the spellings the cell does read. The gate was strengthened, not weakened. Note for the PM: the platform appended a second copy of the signature footer to the PR body; I left it, because a PATCH edit would downgrade both footers to the bare form and lose the session URL.",
"tests": "All runs used the canonical repo-root vitest invocation (this repo's guard refuses a package-cwd run as a false green) and went through the shared verify lock. UNION on the final commit f4f299e23: `pnpm exec vitest run packages/plugin-grid/src packages/fields/src/__tests__/lookupCellDisplayField.test.tsx` gave `Test Files 106 passed (106)` / `Tests 956 passed (956)`, VERDICT command-exit 0. Baseline before any edit was 5 files / 27 tests; the relational suites after the change are 6 files / 34 tests. `pnpm --filter @object-ui/plugin-grid run type-check` clean — the FIRST attempt returned TS2307 'Cannot find module @object-ui/*' across files I never touched, which is an unbuilt dependency closure and NOT MEASURED rather than a red; re-ran after `pnpm --filter '@object-ui/plugin-grid^...' build` (13 packages) and it passed with the script name echoed, so it was not a zero-match no-op. Confirmed the new test file is actually IN the tsconfig.test.json program with `--listFiles` (1 hit, control `lookupPickerKeys-7154` also 1, 1770 files). LINT: ran `eslint . --no-inline-config` REPO-WIDE rather than narrowing — 4100 files by eslint's own count, 75 files with errors, every one pre-existing and none in this diff; my files are E0 (one unused import it caught was fixed). Control-character self-scan clean over all 6 changed files, with the pattern itself verified against 0x01 and 0x0b after my first control (a tab) proved nothing because 0x09 is deliberately outside the class. ABLATIONS — both targets resolve from SOURCE (the root config aliases every @object-ui/* to src/, and both are relative imports), so no rebuild leg applies and none was needed; each mutation was proven on disk by marker count AND blob hash versus the HEAD blob, never by an editor's exit code, and each restore proven by STATE with `git diff HEAD`, `git diff --cached` and `git status --short` all empty plus the blob back to the HEAD blob and the marker gone. Both ran against the COMMITTED implementation. (1) Break `renderCellEditor`'s schema spread — direction predicted before running as 'the editor half goes red, the cell half stays green' — result `Tests 3 failed | 2 passed`: exactly B1/B2/B3 red, test A and the premise test green. (2) Re-add `lookupFilters` to the copy set — direction predicted as 'the derived gate assertions stay green, only the hand-written pins go red' — result `Tests 8 failed | 19 passed`, and the ONLY derivation-gate failure was my hand-written pin while every derived assertion stayed green. One flaw recorded rather than hidden: in ablation 2 my `deferred`-count grep was malformed by nested quotes and printed a bogus 0 — the exact 'empty output read as a zero' trap; the mutation was still proven by the injected-marker count and the blob change, and I re-counted cleanly afterwards (7 deferred, control `legacy-alias` 4).",
"mcp_calls": "9 — REST was 403 on every repo-scoped read and `gh` is absent, so the duplicate check for the filed finding used ONE targeted MCP search_issues (3 hits, so a non-empty reading needing no control query). Channel switch declared.",
"open_questions": [
{
"question": "The 3 snake_case legacy aliases (`description_field`, `lookup_filters`, `id_field`) have the SAME reader-side verdict as the three I retired — no reader on this bag — and are kept only on a producer-side argument that no evidence gathered in these two repos can confirm or refute: does any host DataSource outside objectui and objectstack actually hand-feed these spellings onto a field def? Their verdict is now recorded on the table, so this is not blocking, but it is the question that decides whether a future card retires them.",
"options": [
"A. Leave as is. The reader-side verdict is now recorded with the open producer question stated beside it, so the next pass inherits a measurement instead of a silence.",
"B. Commission a producer survey outside these two repos (host DataSource implementations, any published adapter) and retire whichever aliases come back with zero producers.",
"C. Retire all three now on the reader-side measurement alone, treating the legacy-alias verdict as unbacked."
],
"recommendation": "A, and B only if someone independently wants the producer inventory for its own sake. On the four axes: REAL BUSINESS NEED — there is no measured pull for retiring them; they cost three conditional writes per relational column build and nothing else, while the failure mode of being wrong is a silently broken host integration we cannot see from here. LONG-TERM SOUNDNESS — the honest state is now encoded rather than implied; retiring on reader-side evidence would set the precedent that a back-compat verdict can be overturned by a measurement that does not address it. AI-ERROR-RESISTANCE — this is the axis that argues hardest against C: these are runtime-only spellings no spec-compliant producer can emit, so the strict FieldSchema already stops an AI-authored app from declaring them, and removing the runtime tolerance buys no authoring-time safety while risking a live host. STARTUP SCOPE — C spends real risk on tidiness with zero pull, and B spends survey effort on the same. C is explicitly out of scope per the ruling and I did not take it."
}
],
"out_of_scope_findings": [
"filed as #7187: the relational copy-set derivation cannot judge a COPY — its read set is a union over three consumers, two of which are never fed that bag, so read-set membership proves only that some consumer reads a key and not that this bag is how it arrives. Pinned by my ablation 2: re-adding a retired key leaves every DERIVED gate assertion green and turns red only the hand-written pins. Re-scoping it around the cell alone is a design change to #6875's mechanism, which #7166 explicitly declined to make, so it is filed rather than ridden. Unassigned, labels finding + domain:ui.",
"not filed, recorded here: #7165 (the dependsOn uneditable-column bug) was named out of scope by the dispatch and I did not touch it; the `dependsOn` gating behaviour it concerns is still pinned as current behaviour by #7154's suite, which stayed green throughout."
]
}

Generated by Claude Code

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — domain:ui seat (session session_012wwHa4aaFybxXrfmfHioDM), reviewer of record

Verdict: ACCEPT. Arming on green.

Ruling item 3 was answered first and answered properly

I gated this card on one question — is RELATIONAL_META_KEYS public surface? — because a "yes" made the whole thing unlandable at today's tier. It came back internal, and not on one probe but three, each carrying useRowColor as a live control that hit in all three:

  1. not re-exported from the plugin-grid barrel (fully enumerated export list, no export * — which is what makes a negative readable at all);
  2. the built dist/index.d.ts holds zero occurrences of any of the module's symbols;
  3. absent from the built bundle's export list.

⭐ Three independent probes rather than one, and the export * check is the detail that matters: a barrel with a star export would have made probe 1 meaningless while still looking like a clean negative. Clause ② not engaged, correctly established rather than assumed.

⭐ The self-reported flaw is the most valuable line in the report

"in ablation 2 my deferred-count grep was malformed by nested quotes and printed a bogus 0 — the exact 'empty output read as a zero' trap"

Reported rather than buried, with the recovery stated: the mutation was still proven by the injected-marker count and the blob change, so the ablation's validity never depended on the broken probe, and the count was re-taken cleanly afterwards (7 deferred, control legacy-alias 4).

⚠️ This is the third instance of that exact trap in this seat's work today — I hit it verifying #7139 (a malformed control that printed empty beside a subject's 0), and again this morning pointing a pack probe at a barrel file. It is not a rare slip; it is the default failure of any grep written inside nested quoting. A dev who reports it, and who can show the result did not rest on it, is worth more than one who never trips it.

The same instinct shows up on the control-byte scan: the first control was a tab, which proved nothing because 0x09 is deliberately outside the class, so the pattern was re-verified against 0x01 and 0x0b. Noticing that your control is a dud is the whole skill.

The retirement was done through the table, and the gate came out stronger

Copy set 10 → 7, verified at runtime, by flipping the three modern spellings to deferred — the verdict this repo already uses for the class, rather than a new one. And the gate was strengthened, not weakened: every deferred verdict is now mechanically proved spec-declared, and the two sibling pins turn the retired lookupFilters into a live negative assertion on a fixture that still declares it. That last part is what stops this retirement from being invisible to the next reader.

Ablation 2 is the proof the scope call was right: re-adding a retired key leaves every derived gate assertion green and turns red only the hand-written pins (Tests 8 failed | 19 passed). Ablation 1 split the population exactly as predicted (3 failed | 2 passed, B1/B2/B3 red, test A and the premise test green).

The three legacy aliases: A, leave as is — ruling the open question

The snake_case aliases have the same reader-side verdict as the three retired, and are kept only on a producer-side argument no evidence from these two repos can address. A is right, and the axis that decides it is the one the report names: these are runtime-only spellings no spec-compliant producer can emit, so the strict FieldSchema already stops an AI-authored app from declaring them. Removing the runtime tolerance buys no authoring-time safety while risking a live host integration we cannot see. Three conditional writes per column build is not a cost worth that.

C would set a precedent worth refusing explicitly: a back-compat verdict cannot be overturned by a measurement that does not address it. The reader-side census is silent on producers by construction. Recording the verdict beside the open question — which is what shipped — is the durable improvement.

#7187 is the right filing, and it generalises the lesson that produced this card

the relational copy-set derivation cannot judge a COPY — its read set is a union over three consumers, two of which are never fed that bag

⭐ That is the mechanised form of the rule #6875 cost us: a derivation establishes that a consumer reads a key; it does not establish that this bag is how it arrives. And it is now pinned by ablation 2 rather than asserted — the gate's own blindness is demonstrated, not argued. Re-scoping the derivation around the cell alone is a design change to #6875's mechanism, which this card explicitly declined to make, so filing was correct.

Also correct


Nothing to change.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-grid): retire the three inert fieldMeta copies the cell never reads - #7186

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7166-inert-fieldmeta-copies
Sep 1, 2026
Merged

fix(plugin-grid): retire the three inert fieldMeta copies the cell never reads#7186
os-warren merged 1 commit into
mainfrom
claude/issue-7166-inert-fieldmeta-copies

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7166

ObjectGrid no longer copies descriptionField, lookupColumns or lookupFilters onto a relational column's fieldMeta. No behaviour change — all three still reach the inline lookup picker, by a different seam.

Copy set: 10 keys to 7. Re-measured on 47035ce79 (the card's own numbers were taken on 51449a043, before PR #7167 landed into these files).

Ruling item 3 — the gating measurement: RELATIONAL_META_KEYS is INTERNAL

Clause 2 is not engaged, measured three ways, each with a live control:

proberelationalMetaKeyscontrol (useRowColor)
re-export in src/index.tsx barrelabsent (no export * either)line 30
built dist/index.d.ts0 occurrences of any of its symbolspresent
built dist/index.js export listnot exportedpresent

Every import of ./relationalMetaKeys in the repo lives inside packages/plugin-grid/src/ — the implementation plus two of its own tests. The cross-package hits are prose in docblocks; plugin-dashboard keeps its own independent CELL_RELATIONAL_META_KEYS literal rather than importing this one. Narrowing the copy set therefore moves no published surface.

What was measured

applyRelationalMeta writes onto the fieldMeta that generateColumns hands to CellRenderer as the field prop — 6 JSX passes across the three column-building paths, and nowhere else (as a whole object it otherwise only reaches applyRelationalMeta, which mutates it).

  • LookupCellRenderer reads exactly reference_to, reference, display_field, displayField, reference_field, plus options.
  • UserCellRenderer has zero occurrences of field — control: LookupCellRenderer has 8.

Stated by receiver rather than by count: packages/fields/src/index.tsx, the file holding every cell renderer, contains zero occurrences of the three retired keys, against a control of 22 occurrences of the display_field / displayField / reference_to spellings the cell does read.

Their only readers off a field meta are LookupField and UserField — the two editor widgets — and the editor is not fed this bag. renderCellEditor looks the field up in the object schema and spreads the whole def into the widget ({ name: ctx.column.accessorKey, ...fieldDef }), so every key a def carries reaches LookupField whether or not it is copied. The copies were dead writes: the #6711 / #6874 class arriving from the opposite direction — those keys had no declaration, these have no reader on this path.

The evidence bar this card exists to raise

Two of the three, descriptionField and lookupColumns, were added by #6875 as its fix, classified from a read-set derivation alone. Its third key displayField is genuinely delivered and stays copied — and it is the one that arrived with a rendering test.

A derivation establishes that a consumer READS a key. It does not establish that a given BAG is how the consumer gets it. Recorded in both docblocks.

So this is pinned by rendering, in both directions — new __tests__/relationalMetaCopySet-7166.test.tsx:

  • The cell. Four lookup columns over one referenced record, differing only in the key under test, all resolve the same text; the displayField control column resolves something else. The control is what makes the three zeros readings rather than a fixture that never reached the lookup path.
  • The editor. Each retired key still takes effect in the inline picker with the copy set no longer carrying it — descriptionField drives the secondary line, lookupColumns shapes the picker columns, lookupFilters scopes the candidates — each against a sibling control column that declares nothing. The referenced schema is deliberately two fields, so deriveLookupColumns derives nothing and the controls stay load-bearing.

Scope: 3 keys, not 6

The card measures six keys with no reader on this bag. They are two populations. The three snake_case legacy-alias spellings — description_field, lookup_filters, id_field — are deliberately kept. They are recorded as legacy aliases precisely because a host DataSource outside these repos may hand-feed them; that is a producer-side argument and this measurement is entirely reader-side. Their reader-side verdict is now recorded on the table with the open producer question stated, so the next pass inherits a measurement instead of a silence.

⛔ Reported, not fixed: the derivation gate cannot see this class

The gate's read set is a union over three consumers, two of which are not fed this bag. Membership means "some consumer reads this key", never "this bag is how that consumer gets it" — and only the second claim justifies a copy. All three retired keys remain in the extracted read set, so every derived assertion passes whichever verdict they carry.

Measured, not asserted (ablation 2 below): re-adding lookupFilters to the copy set leaves every derived assertion in the gate green and turns red only the hand-written pins. Re-scoping the derivation around the cell alone, with the editor widgets classified separately, is a design change to #6875's mechanism and is filed rather than made here.

The gate is strengthened, not weakened: every deferred verdict is now mechanically proved spec-declared against the installed FieldSchema, and the sibling pins turn the retired lookupFilters into a live negative assertion on a fixture that still declares it.

Tests

All runs are the canonical repo-root invocation (the repo's vitest guard refuses a package-cwd run as a false green). Union run on the final commit f4f299e23:

pnpm exec vitest run packages/plugin-grid/src packages/fields/src/__tests__/lookupCellDisplayField.test.tsx
Test Files 106 passed (106)
Tests 956 passed (956)

pnpm --filter @object-ui/plugin-grid run type-check — clean (first attempt reported TS2307: Cannot find module '@object-ui/*', which is an unbuilt dependency closure and NOT a result; re-run after pnpm --filter '@object-ui/plugin-grid^...' build). The new test file is confirmed present in the tsconfig.test.json program via --listFiles, so that green does cover it.

eslint . --no-inline-config was run repo-wide, not narrowed: 4100 files, 75 files with errors, all pre-existing and none in this diff; the changed files are E0.

Ablations

Both targets resolve from source (the root config aliases every @object-ui/* to src/, and both are relative imports), so no rebuild leg applies. Each mutation was proven on disk by marker count and blob hash — never an editor's exit code — and each restore by state: git diff HEAD, git diff --cached and git status --short all empty, blob back to the HEAD blob, marker gone.

  1. Break the editor's schema spread (predicted before running: the editor half goes red, the cell half stays green) — Tests 3 failed | 2 passed. Exactly B1/B2/B3 red; test A and the premise test green. The retirement's safety claim is load-bearing.
  2. Re-add lookupFilters to the copy set (predicted: derived gate assertions stay green, hand-written pins go red) — Tests 8 failed | 19 passed. The only derivation-gate failure is the hand-written finding(plugin-grid): 6 of the 10 keys ObjectGrid copies onto a column's fieldMeta have no reader on that bag — the picker keys reach the editor by a different seam #7166 pin; every derived assertion stayed green. The cell and editor tests also stayed green, which is itself the finding: the copy makes no observable difference.

Generated by Claude Code


Generated by Claude Code

…never reads
`applyRelationalMeta` no longer copies `descriptionField`, `lookupColumns` or
`lookupFilters` onto a relational column's `fieldMeta`. No behaviour change:
all three still reach the inline lookup picker, by a different seam.
`fieldMeta` is handed to `<CellRenderer>` as the `field` prop and nowhere else
— six JSX passes across the three column-building paths. A relational column
resolves to `LookupCellRenderer`, which reads exactly `reference_to`,
`reference`, `display_field`, `displayField`, `reference_field` and `options`;
a `user` column resolves to `UserCellRenderer`, which destructures `{ value }`
and reads no field meta at all. Measured by receiver: `fields/src/index.tsx`,
the file holding every cell renderer, contains zero occurrences of the three
retired keys, against a control of 22 for the spellings the cell does read.
Their only readers off a field meta are `LookupField` and `UserField` — the two
editor widgets — which `renderCellEditor` feeds from the schema def directly,
so every key a def carries reaches them whether or not it is copied.
Two of the three were added by objectui#6875, classified from a read-set
derivation alone; its third key `displayField` is genuinely delivered and stays
copied. A derivation establishes that a consumer reads a key, not that a given
bag is how the consumer gets it — recorded in both docblocks.
- New `relationalMetaCopySet-7166.test.tsx` renders both halves: the cell (four
columns differing only in the key under test resolve the same text, with a
`displayField` control that differs) and the editor (each retired key still
takes effect in the picker, each against a control column declaring nothing).
- The three snake_case `legacy-alias` spellings are deliberately kept: their
retention rests on an open producer-side question this reader-side
measurement does not touch. Their verdict is now recorded on the table.
- The derivation gate cannot enforce this retirement — its read set is a union
over three consumers, two of which are not fed this bag — and now says so.
Absence is pinned by an explicit assertion plus the rendering test.
- Gate strengthened: every `deferred` verdict is now mechanically proved
spec-declared, and the sibling pins turn the retired `lookupFilters` into a
live negative assertion on a fixture that still declares it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.2 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-VEpn3hxK.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)68.45KB19.20KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)207.19KB56.03KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

os-dev-report

{
"issue": 7166,
"status": "done",
"branch": "claude/issue-7166-inert-fieldmeta-copies",
"pr": "https://github.com/objectstack-ai/objectui/pull/7186",
"premise_still_valid": true,
"summary": "RULING ITEM 3 FIRST, and it came back INTERNAL, so I proceeded: `RELATIONAL_META_KEYS` is not re-exported from the plugin-grid barrel (fully enumerated export list, no `export *`), the built `dist/index.d.ts` holds zero occurrences of any of the module's symbols, and it is absent from the built bundle's export list — each with `useRowColor` as a live control that hits in all three probes. Clause 2 not engaged. A2.1 re-derived on 47035ce79 and it still holds: the copy set was 10, six had no reader on the bag. Retired the 3 modern spellings (`descriptionField`, `lookupColumns`, `lookupFilters`) through the derivation table by flipping their verdict to `deferred`, the verdict this repo already uses for the same class; copy set now derives to 7, verified at runtime. Kept the 3 snake_case legacy aliases with their reader-side verdict recorded on the table and the unanswered producer question stated beside it. A2.3, A2.4 and A2.5 all confirmed on head by re-measurement, not inherited: 6 `field={fieldMeta}` JSX passes across the three column-building paths and no other whole-object use except `applyRelationalMeta`; `UserCellRenderer` has zero occurrences of `field` against a control of 8 in `LookupCellRenderer`; and `packages/fields/src/index.tsx`, which holds every cell renderer, has zero occurrences of all six keys against a control of 22 for the spellings the cell does read. The gate was strengthened, not weakened. Note for the PM: the platform appended a second copy of the signature footer to the PR body; I left it, because a PATCH edit would downgrade both footers to the bare form and lose the session URL.",
"tests": "All runs used the canonical repo-root vitest invocation (this repo's guard refuses a package-cwd run as a false green) and went through the shared verify lock. UNION on the final commit f4f299e23: `pnpm exec vitest run packages/plugin-grid/src packages/fields/src/__tests__/lookupCellDisplayField.test.tsx` gave `Test Files 106 passed (106)` / `Tests 956 passed (956)`, VERDICT command-exit 0. Baseline before any edit was 5 files / 27 tests; the relational suites after the change are 6 files / 34 tests. `pnpm --filter @object-ui/plugin-grid run type-check` clean — the FIRST attempt returned TS2307 'Cannot find module @object-ui/*' across files I never touched, which is an unbuilt dependency closure and NOT MEASURED rather than a red; re-ran after `pnpm --filter '@object-ui/plugin-grid^...' build` (13 packages) and it passed with the script name echoed, so it was not a zero-match no-op. Confirmed the new test file is actually IN the tsconfig.test.json program with `--listFiles` (1 hit, control `lookupPickerKeys-7154` also 1, 1770 files). LINT: ran `eslint . --no-inline-config` REPO-WIDE rather than narrowing — 4100 files by eslint's own count, 75 files with errors, every one pre-existing and none in this diff; my files are E0 (one unused import it caught was fixed). Control-character self-scan clean over all 6 changed files, with the pattern itself verified against 0x01 and 0x0b after my first control (a tab) proved nothing because 0x09 is deliberately outside the class. ABLATIONS — both targets resolve from SOURCE (the root config aliases every @object-ui/* to src/, and both are relative imports), so no rebuild leg applies and none was needed; each mutation was proven on disk by marker count AND blob hash versus the HEAD blob, never by an editor's exit code, and each restore proven by STATE with `git diff HEAD`, `git diff --cached` and `git status --short` all empty plus the blob back to the HEAD blob and the marker gone. Both ran against the COMMITTED implementation. (1) Break `renderCellEditor`'s schema spread — direction predicted before running as 'the editor half goes red, the cell half stays green' — result `Tests 3 failed | 2 passed`: exactly B1/B2/B3 red, test A and the premise test green. (2) Re-add `lookupFilters` to the copy set — direction predicted as 'the derived gate assertions stay green, only the hand-written pins go red' — result `Tests 8 failed | 19 passed`, and the ONLY derivation-gate failure was my hand-written pin while every derived assertion stayed green. One flaw recorded rather than hidden: in ablation 2 my `deferred`-count grep was malformed by nested quotes and printed a bogus 0 — the exact 'empty output read as a zero' trap; the mutation was still proven by the injected-marker count and the blob change, and I re-counted cleanly afterwards (7 deferred, control `legacy-alias` 4).",
"mcp_calls": "9 — REST was 403 on every repo-scoped read and `gh` is absent, so the duplicate check for the filed finding used ONE targeted MCP search_issues (3 hits, so a non-empty reading needing no control query). Channel switch declared.",
"open_questions": [
{
"question": "The 3 snake_case legacy aliases (`description_field`, `lookup_filters`, `id_field`) have the SAME reader-side verdict as the three I retired — no reader on this bag — and are kept only on a producer-side argument that no evidence gathered in these two repos can confirm or refute: does any host DataSource outside objectui and objectstack actually hand-feed these spellings onto a field def? Their verdict is now recorded on the table, so this is not blocking, but it is the question that decides whether a future card retires them.",
"options": [
"A. Leave as is. The reader-side verdict is now recorded with the open producer question stated beside it, so the next pass inherits a measurement instead of a silence.",
"B. Commission a producer survey outside these two repos (host DataSource implementations, any published adapter) and retire whichever aliases come back with zero producers.",
"C. Retire all three now on the reader-side measurement alone, treating the legacy-alias verdict as unbacked."
],
"recommendation": "A, and B only if someone independently wants the producer inventory for its own sake. On the four axes: REAL BUSINESS NEED — there is no measured pull for retiring them; they cost three conditional writes per relational column build and nothing else, while the failure mode of being wrong is a silently broken host integration we cannot see from here. LONG-TERM SOUNDNESS — the honest state is now encoded rather than implied; retiring on reader-side evidence would set the precedent that a back-compat verdict can be overturned by a measurement that does not address it. AI-ERROR-RESISTANCE — this is the axis that argues hardest against C: these are runtime-only spellings no spec-compliant producer can emit, so the strict FieldSchema already stops an AI-authored app from declaring them, and removing the runtime tolerance buys no authoring-time safety while risking a live host. STARTUP SCOPE — C spends real risk on tidiness with zero pull, and B spends survey effort on the same. C is explicitly out of scope per the ruling and I did not take it."
}
],
"out_of_scope_findings": [
"filed as #7187: the relational copy-set derivation cannot judge a COPY — its read set is a union over three consumers, two of which are never fed that bag, so read-set membership proves only that some consumer reads a key and not that this bag is how it arrives. Pinned by my ablation 2: re-adding a retired key leaves every DERIVED gate assertion green and turns red only the hand-written pins. Re-scoping it around the cell alone is a design change to #6875's mechanism, which #7166 explicitly declined to make, so it is filed rather than ridden. Unassigned, labels finding + domain:ui.",
"not filed, recorded here: #7165 (the dependsOn uneditable-column bug) was named out of scope by the dispatch and I did not touch it; the `dependsOn` gating behaviour it concerns is still pinned as current behaviour by #7154's suite, which stayed green throughout."
]
}

Generated by Claude Code

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — domain:ui seat (session session_012wwHa4aaFybxXrfmfHioDM), reviewer of record

Verdict: ACCEPT. Arming on green.

Ruling item 3 was answered first and answered properly

I gated this card on one question — is RELATIONAL_META_KEYS public surface? — because a "yes" made the whole thing unlandable at today's tier. It came back internal, and not on one probe but three, each carrying useRowColor as a live control that hit in all three:

  1. not re-exported from the plugin-grid barrel (fully enumerated export list, no export * — which is what makes a negative readable at all);
  2. the built dist/index.d.ts holds zero occurrences of any of the module's symbols;
  3. absent from the built bundle's export list.

⭐ Three independent probes rather than one, and the export * check is the detail that matters: a barrel with a star export would have made probe 1 meaningless while still looking like a clean negative. Clause ② not engaged, correctly established rather than assumed.

⭐ The self-reported flaw is the most valuable line in the report

"in ablation 2 my deferred-count grep was malformed by nested quotes and printed a bogus 0 — the exact 'empty output read as a zero' trap"

Reported rather than buried, with the recovery stated: the mutation was still proven by the injected-marker count and the blob change, so the ablation's validity never depended on the broken probe, and the count was re-taken cleanly afterwards (7 deferred, control legacy-alias 4).

⚠️ This is the third instance of that exact trap in this seat's work today — I hit it verifying #7139 (a malformed control that printed empty beside a subject's 0), and again this morning pointing a pack probe at a barrel file. It is not a rare slip; it is the default failure of any grep written inside nested quoting. A dev who reports it, and who can show the result did not rest on it, is worth more than one who never trips it.

The same instinct shows up on the control-byte scan: the first control was a tab, which proved nothing because 0x09 is deliberately outside the class, so the pattern was re-verified against 0x01 and 0x0b. Noticing that your control is a dud is the whole skill.

The retirement was done through the table, and the gate came out stronger

Copy set 10 → 7, verified at runtime, by flipping the three modern spellings to deferred — the verdict this repo already uses for the class, rather than a new one. And the gate was strengthened, not weakened: every deferred verdict is now mechanically proved spec-declared, and the two sibling pins turn the retired lookupFilters into a live negative assertion on a fixture that still declares it. That last part is what stops this retirement from being invisible to the next reader.

Ablation 2 is the proof the scope call was right: re-adding a retired key leaves every derived gate assertion green and turns red only the hand-written pins (Tests 8 failed | 19 passed). Ablation 1 split the population exactly as predicted (3 failed | 2 passed, B1/B2/B3 red, test A and the premise test green).

The three legacy aliases: A, leave as is — ruling the open question

The snake_case aliases have the same reader-side verdict as the three retired, and are kept only on a producer-side argument no evidence from these two repos can address. A is right, and the axis that decides it is the one the report names: these are runtime-only spellings no spec-compliant producer can emit, so the strict FieldSchema already stops an AI-authored app from declaring them. Removing the runtime tolerance buys no authoring-time safety while risking a live host integration we cannot see. Three conditional writes per column build is not a cost worth that.

C would set a precedent worth refusing explicitly: a back-compat verdict cannot be overturned by a measurement that does not address it. The reader-side census is silent on producers by construction. Recording the verdict beside the open question — which is what shipped — is the durable improvement.

#7187 is the right filing, and it generalises the lesson that produced this card

the relational copy-set derivation cannot judge a COPY — its read set is a union over three consumers, two of which are never fed that bag

⭐ That is the mechanised form of the rule #6875 cost us: a derivation establishes that a consumer reads a key; it does not establish that this bag is how it arrives. And it is now pinned by ablation 2 rather than asserted — the gate's own blindness is demonstrated, not argued. Re-scoping the derivation around the cell alone is a design change to #6875's mechanism, which this card explicitly declined to make, so filing was correct.

Also correct


Nothing to change.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-grid): retire the three inert fieldMeta copies the cell never reads - #7186

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7166-inert-fieldmeta-copies
Sep 1, 2026
Merged

fix(plugin-grid): retire the three inert fieldMeta copies the cell never reads#7186
os-warren merged 1 commit into
mainfrom
claude/issue-7166-inert-fieldmeta-copies

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7166

ObjectGrid no longer copies descriptionField, lookupColumns or lookupFilters onto a relational column's fieldMeta. No behaviour change — all three still reach the inline lookup picker, by a different seam.

Copy set: 10 keys to 7. Re-measured on 47035ce79 (the card's own numbers were taken on 51449a043, before PR #7167 landed into these files).

Ruling item 3 — the gating measurement: RELATIONAL_META_KEYS is INTERNAL

Clause 2 is not engaged, measured three ways, each with a live control:

proberelationalMetaKeyscontrol (useRowColor)
re-export in src/index.tsx barrelabsent (no export * either)line 30
built dist/index.d.ts0 occurrences of any of its symbolspresent
built dist/index.js export listnot exportedpresent

Every import of ./relationalMetaKeys in the repo lives inside packages/plugin-grid/src/ — the implementation plus two of its own tests. The cross-package hits are prose in docblocks; plugin-dashboard keeps its own independent CELL_RELATIONAL_META_KEYS literal rather than importing this one. Narrowing the copy set therefore moves no published surface.

What was measured

applyRelationalMeta writes onto the fieldMeta that generateColumns hands to CellRenderer as the field prop — 6 JSX passes across the three column-building paths, and nowhere else (as a whole object it otherwise only reaches applyRelationalMeta, which mutates it).

  • LookupCellRenderer reads exactly reference_to, reference, display_field, displayField, reference_field, plus options.
  • UserCellRenderer has zero occurrences of field — control: LookupCellRenderer has 8.

Stated by receiver rather than by count: packages/fields/src/index.tsx, the file holding every cell renderer, contains zero occurrences of the three retired keys, against a control of 22 occurrences of the display_field / displayField / reference_to spellings the cell does read.

Their only readers off a field meta are LookupField and UserField — the two editor widgets — and the editor is not fed this bag. renderCellEditor looks the field up in the object schema and spreads the whole def into the widget ({ name: ctx.column.accessorKey, ...fieldDef }), so every key a def carries reaches LookupField whether or not it is copied. The copies were dead writes: the #6711 / #6874 class arriving from the opposite direction — those keys had no declaration, these have no reader on this path.

The evidence bar this card exists to raise

Two of the three, descriptionField and lookupColumns, were added by #6875 as its fix, classified from a read-set derivation alone. Its third key displayField is genuinely delivered and stays copied — and it is the one that arrived with a rendering test.

A derivation establishes that a consumer READS a key. It does not establish that a given BAG is how the consumer gets it. Recorded in both docblocks.

So this is pinned by rendering, in both directions — new __tests__/relationalMetaCopySet-7166.test.tsx:

  • The cell. Four lookup columns over one referenced record, differing only in the key under test, all resolve the same text; the displayField control column resolves something else. The control is what makes the three zeros readings rather than a fixture that never reached the lookup path.
  • The editor. Each retired key still takes effect in the inline picker with the copy set no longer carrying it — descriptionField drives the secondary line, lookupColumns shapes the picker columns, lookupFilters scopes the candidates — each against a sibling control column that declares nothing. The referenced schema is deliberately two fields, so deriveLookupColumns derives nothing and the controls stay load-bearing.

Scope: 3 keys, not 6

The card measures six keys with no reader on this bag. They are two populations. The three snake_case legacy-alias spellings — description_field, lookup_filters, id_field — are deliberately kept. They are recorded as legacy aliases precisely because a host DataSource outside these repos may hand-feed them; that is a producer-side argument and this measurement is entirely reader-side. Their reader-side verdict is now recorded on the table with the open producer question stated, so the next pass inherits a measurement instead of a silence.

⛔ Reported, not fixed: the derivation gate cannot see this class

The gate's read set is a union over three consumers, two of which are not fed this bag. Membership means "some consumer reads this key", never "this bag is how that consumer gets it" — and only the second claim justifies a copy. All three retired keys remain in the extracted read set, so every derived assertion passes whichever verdict they carry.

Measured, not asserted (ablation 2 below): re-adding lookupFilters to the copy set leaves every derived assertion in the gate green and turns red only the hand-written pins. Re-scoping the derivation around the cell alone, with the editor widgets classified separately, is a design change to #6875's mechanism and is filed rather than made here.

The gate is strengthened, not weakened: every deferred verdict is now mechanically proved spec-declared against the installed FieldSchema, and the sibling pins turn the retired lookupFilters into a live negative assertion on a fixture that still declares it.

Tests

All runs are the canonical repo-root invocation (the repo's vitest guard refuses a package-cwd run as a false green). Union run on the final commit f4f299e23:

pnpm exec vitest run packages/plugin-grid/src packages/fields/src/__tests__/lookupCellDisplayField.test.tsx
Test Files 106 passed (106)
Tests 956 passed (956)

pnpm --filter @object-ui/plugin-grid run type-check — clean (first attempt reported TS2307: Cannot find module '@object-ui/*', which is an unbuilt dependency closure and NOT a result; re-run after pnpm --filter '@object-ui/plugin-grid^...' build). The new test file is confirmed present in the tsconfig.test.json program via --listFiles, so that green does cover it.

eslint . --no-inline-config was run repo-wide, not narrowed: 4100 files, 75 files with errors, all pre-existing and none in this diff; the changed files are E0.

Ablations

Both targets resolve from source (the root config aliases every @object-ui/* to src/, and both are relative imports), so no rebuild leg applies. Each mutation was proven on disk by marker count and blob hash — never an editor's exit code — and each restore by state: git diff HEAD, git diff --cached and git status --short all empty, blob back to the HEAD blob, marker gone.

  1. Break the editor's schema spread (predicted before running: the editor half goes red, the cell half stays green) — Tests 3 failed | 2 passed. Exactly B1/B2/B3 red; test A and the premise test green. The retirement's safety claim is load-bearing.
  2. Re-add lookupFilters to the copy set (predicted: derived gate assertions stay green, hand-written pins go red) — Tests 8 failed | 19 passed. The only derivation-gate failure is the hand-written finding(plugin-grid): 6 of the 10 keys ObjectGrid copies onto a column's fieldMeta have no reader on that bag — the picker keys reach the editor by a different seam #7166 pin; every derived assertion stayed green. The cell and editor tests also stayed green, which is itself the finding: the copy makes no observable difference.

Generated by Claude Code


Generated by Claude Code

…never reads
`applyRelationalMeta` no longer copies `descriptionField`, `lookupColumns` or
`lookupFilters` onto a relational column's `fieldMeta`. No behaviour change:
all three still reach the inline lookup picker, by a different seam.
`fieldMeta` is handed to `<CellRenderer>` as the `field` prop and nowhere else
— six JSX passes across the three column-building paths. A relational column
resolves to `LookupCellRenderer`, which reads exactly `reference_to`,
`reference`, `display_field`, `displayField`, `reference_field` and `options`;
a `user` column resolves to `UserCellRenderer`, which destructures `{ value }`
and reads no field meta at all. Measured by receiver: `fields/src/index.tsx`,
the file holding every cell renderer, contains zero occurrences of the three
retired keys, against a control of 22 for the spellings the cell does read.
Their only readers off a field meta are `LookupField` and `UserField` — the two
editor widgets — which `renderCellEditor` feeds from the schema def directly,
so every key a def carries reaches them whether or not it is copied.
Two of the three were added by objectui#6875, classified from a read-set
derivation alone; its third key `displayField` is genuinely delivered and stays
copied. A derivation establishes that a consumer reads a key, not that a given
bag is how the consumer gets it — recorded in both docblocks.
- New `relationalMetaCopySet-7166.test.tsx` renders both halves: the cell (four
columns differing only in the key under test resolve the same text, with a
`displayField` control that differs) and the editor (each retired key still
takes effect in the picker, each against a control column declaring nothing).
- The three snake_case `legacy-alias` spellings are deliberately kept: their
retention rests on an open producer-side question this reader-side
measurement does not touch. Their verdict is now recorded on the table.
- The derivation gate cannot enforce this retirement — its read set is a union
over three consumers, two of which are not fed this bag — and now says so.
Absence is pinned by an explicit assertion plus the rendering test.
- Gate strengthened: every `deferred` verdict is now mechanically proved
spec-declared, and the sibling pins turn the retired `lookupFilters` into a
live negative assertion on a fixture that still declares it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.2 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-VEpn3hxK.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)68.45KB19.20KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)207.19KB56.03KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

os-dev-report

{
"issue": 7166,
"status": "done",
"branch": "claude/issue-7166-inert-fieldmeta-copies",
"pr": "https://github.com/objectstack-ai/objectui/pull/7186",
"premise_still_valid": true,
"summary": "RULING ITEM 3 FIRST, and it came back INTERNAL, so I proceeded: `RELATIONAL_META_KEYS` is not re-exported from the plugin-grid barrel (fully enumerated export list, no `export *`), the built `dist/index.d.ts` holds zero occurrences of any of the module's symbols, and it is absent from the built bundle's export list — each with `useRowColor` as a live control that hits in all three probes. Clause 2 not engaged. A2.1 re-derived on 47035ce79 and it still holds: the copy set was 10, six had no reader on the bag. Retired the 3 modern spellings (`descriptionField`, `lookupColumns`, `lookupFilters`) through the derivation table by flipping their verdict to `deferred`, the verdict this repo already uses for the same class; copy set now derives to 7, verified at runtime. Kept the 3 snake_case legacy aliases with their reader-side verdict recorded on the table and the unanswered producer question stated beside it. A2.3, A2.4 and A2.5 all confirmed on head by re-measurement, not inherited: 6 `field={fieldMeta}` JSX passes across the three column-building paths and no other whole-object use except `applyRelationalMeta`; `UserCellRenderer` has zero occurrences of `field` against a control of 8 in `LookupCellRenderer`; and `packages/fields/src/index.tsx`, which holds every cell renderer, has zero occurrences of all six keys against a control of 22 for the spellings the cell does read. The gate was strengthened, not weakened. Note for the PM: the platform appended a second copy of the signature footer to the PR body; I left it, because a PATCH edit would downgrade both footers to the bare form and lose the session URL.",
"tests": "All runs used the canonical repo-root vitest invocation (this repo's guard refuses a package-cwd run as a false green) and went through the shared verify lock. UNION on the final commit f4f299e23: `pnpm exec vitest run packages/plugin-grid/src packages/fields/src/__tests__/lookupCellDisplayField.test.tsx` gave `Test Files 106 passed (106)` / `Tests 956 passed (956)`, VERDICT command-exit 0. Baseline before any edit was 5 files / 27 tests; the relational suites after the change are 6 files / 34 tests. `pnpm --filter @object-ui/plugin-grid run type-check` clean — the FIRST attempt returned TS2307 'Cannot find module @object-ui/*' across files I never touched, which is an unbuilt dependency closure and NOT MEASURED rather than a red; re-ran after `pnpm --filter '@object-ui/plugin-grid^...' build` (13 packages) and it passed with the script name echoed, so it was not a zero-match no-op. Confirmed the new test file is actually IN the tsconfig.test.json program with `--listFiles` (1 hit, control `lookupPickerKeys-7154` also 1, 1770 files). LINT: ran `eslint . --no-inline-config` REPO-WIDE rather than narrowing — 4100 files by eslint's own count, 75 files with errors, every one pre-existing and none in this diff; my files are E0 (one unused import it caught was fixed). Control-character self-scan clean over all 6 changed files, with the pattern itself verified against 0x01 and 0x0b after my first control (a tab) proved nothing because 0x09 is deliberately outside the class. ABLATIONS — both targets resolve from SOURCE (the root config aliases every @object-ui/* to src/, and both are relative imports), so no rebuild leg applies and none was needed; each mutation was proven on disk by marker count AND blob hash versus the HEAD blob, never by an editor's exit code, and each restore proven by STATE with `git diff HEAD`, `git diff --cached` and `git status --short` all empty plus the blob back to the HEAD blob and the marker gone. Both ran against the COMMITTED implementation. (1) Break `renderCellEditor`'s schema spread — direction predicted before running as 'the editor half goes red, the cell half stays green' — result `Tests 3 failed | 2 passed`: exactly B1/B2/B3 red, test A and the premise test green. (2) Re-add `lookupFilters` to the copy set — direction predicted as 'the derived gate assertions stay green, only the hand-written pins go red' — result `Tests 8 failed | 19 passed`, and the ONLY derivation-gate failure was my hand-written pin while every derived assertion stayed green. One flaw recorded rather than hidden: in ablation 2 my `deferred`-count grep was malformed by nested quotes and printed a bogus 0 — the exact 'empty output read as a zero' trap; the mutation was still proven by the injected-marker count and the blob change, and I re-counted cleanly afterwards (7 deferred, control `legacy-alias` 4).",
"mcp_calls": "9 — REST was 403 on every repo-scoped read and `gh` is absent, so the duplicate check for the filed finding used ONE targeted MCP search_issues (3 hits, so a non-empty reading needing no control query). Channel switch declared.",
"open_questions": [
{
"question": "The 3 snake_case legacy aliases (`description_field`, `lookup_filters`, `id_field`) have the SAME reader-side verdict as the three I retired — no reader on this bag — and are kept only on a producer-side argument that no evidence gathered in these two repos can confirm or refute: does any host DataSource outside objectui and objectstack actually hand-feed these spellings onto a field def? Their verdict is now recorded on the table, so this is not blocking, but it is the question that decides whether a future card retires them.",
"options": [
"A. Leave as is. The reader-side verdict is now recorded with the open producer question stated beside it, so the next pass inherits a measurement instead of a silence.",
"B. Commission a producer survey outside these two repos (host DataSource implementations, any published adapter) and retire whichever aliases come back with zero producers.",
"C. Retire all three now on the reader-side measurement alone, treating the legacy-alias verdict as unbacked."
],
"recommendation": "A, and B only if someone independently wants the producer inventory for its own sake. On the four axes: REAL BUSINESS NEED — there is no measured pull for retiring them; they cost three conditional writes per relational column build and nothing else, while the failure mode of being wrong is a silently broken host integration we cannot see from here. LONG-TERM SOUNDNESS — the honest state is now encoded rather than implied; retiring on reader-side evidence would set the precedent that a back-compat verdict can be overturned by a measurement that does not address it. AI-ERROR-RESISTANCE — this is the axis that argues hardest against C: these are runtime-only spellings no spec-compliant producer can emit, so the strict FieldSchema already stops an AI-authored app from declaring them, and removing the runtime tolerance buys no authoring-time safety while risking a live host. STARTUP SCOPE — C spends real risk on tidiness with zero pull, and B spends survey effort on the same. C is explicitly out of scope per the ruling and I did not take it."
}
],
"out_of_scope_findings": [
"filed as #7187: the relational copy-set derivation cannot judge a COPY — its read set is a union over three consumers, two of which are never fed that bag, so read-set membership proves only that some consumer reads a key and not that this bag is how it arrives. Pinned by my ablation 2: re-adding a retired key leaves every DERIVED gate assertion green and turns red only the hand-written pins. Re-scoping it around the cell alone is a design change to #6875's mechanism, which #7166 explicitly declined to make, so it is filed rather than ridden. Unassigned, labels finding + domain:ui.",
"not filed, recorded here: #7165 (the dependsOn uneditable-column bug) was named out of scope by the dispatch and I did not touch it; the `dependsOn` gating behaviour it concerns is still pinned as current behaviour by #7154's suite, which stayed green throughout."
]
}

Generated by Claude Code

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — domain:ui seat (session session_012wwHa4aaFybxXrfmfHioDM), reviewer of record

Verdict: ACCEPT. Arming on green.

Ruling item 3 was answered first and answered properly

I gated this card on one question — is RELATIONAL_META_KEYS public surface? — because a "yes" made the whole thing unlandable at today's tier. It came back internal, and not on one probe but three, each carrying useRowColor as a live control that hit in all three:

  1. not re-exported from the plugin-grid barrel (fully enumerated export list, no export * — which is what makes a negative readable at all);
  2. the built dist/index.d.ts holds zero occurrences of any of the module's symbols;
  3. absent from the built bundle's export list.

⭐ Three independent probes rather than one, and the export * check is the detail that matters: a barrel with a star export would have made probe 1 meaningless while still looking like a clean negative. Clause ② not engaged, correctly established rather than assumed.

⭐ The self-reported flaw is the most valuable line in the report

"in ablation 2 my deferred-count grep was malformed by nested quotes and printed a bogus 0 — the exact 'empty output read as a zero' trap"

Reported rather than buried, with the recovery stated: the mutation was still proven by the injected-marker count and the blob change, so the ablation's validity never depended on the broken probe, and the count was re-taken cleanly afterwards (7 deferred, control legacy-alias 4).

⚠️ This is the third instance of that exact trap in this seat's work today — I hit it verifying #7139 (a malformed control that printed empty beside a subject's 0), and again this morning pointing a pack probe at a barrel file. It is not a rare slip; it is the default failure of any grep written inside nested quoting. A dev who reports it, and who can show the result did not rest on it, is worth more than one who never trips it.

The same instinct shows up on the control-byte scan: the first control was a tab, which proved nothing because 0x09 is deliberately outside the class, so the pattern was re-verified against 0x01 and 0x0b. Noticing that your control is a dud is the whole skill.

The retirement was done through the table, and the gate came out stronger

Copy set 10 → 7, verified at runtime, by flipping the three modern spellings to deferred — the verdict this repo already uses for the class, rather than a new one. And the gate was strengthened, not weakened: every deferred verdict is now mechanically proved spec-declared, and the two sibling pins turn the retired lookupFilters into a live negative assertion on a fixture that still declares it. That last part is what stops this retirement from being invisible to the next reader.

Ablation 2 is the proof the scope call was right: re-adding a retired key leaves every derived gate assertion green and turns red only the hand-written pins (Tests 8 failed | 19 passed). Ablation 1 split the population exactly as predicted (3 failed | 2 passed, B1/B2/B3 red, test A and the premise test green).

The three legacy aliases: A, leave as is — ruling the open question

The snake_case aliases have the same reader-side verdict as the three retired, and are kept only on a producer-side argument no evidence from these two repos can address. A is right, and the axis that decides it is the one the report names: these are runtime-only spellings no spec-compliant producer can emit, so the strict FieldSchema already stops an AI-authored app from declaring them. Removing the runtime tolerance buys no authoring-time safety while risking a live host integration we cannot see. Three conditional writes per column build is not a cost worth that.

C would set a precedent worth refusing explicitly: a back-compat verdict cannot be overturned by a measurement that does not address it. The reader-side census is silent on producers by construction. Recording the verdict beside the open question — which is what shipped — is the durable improvement.

#7187 is the right filing, and it generalises the lesson that produced this card

the relational copy-set derivation cannot judge a COPY — its read set is a union over three consumers, two of which are never fed that bag

⭐ That is the mechanised form of the rule #6875 cost us: a derivation establishes that a consumer reads a key; it does not establish that this bag is how it arrives. And it is now pinned by ablation 2 rather than asserted — the gate's own blindness is demonstrated, not argued. Re-scoping the derivation around the cell alone is a design change to #6875's mechanism, which this card explicitly declined to make, so filing was correct.

Also correct


Nothing to change.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-warren@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(plugin-grid): retire the three inert fieldMeta copies the cell never reads - #7186

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-7166-inert-fieldmeta-copies
Sep 1, 2026
Merged

fix(plugin-grid): retire the three inert fieldMeta copies the cell never reads#7186
os-warren merged 1 commit into
mainfrom
claude/issue-7166-inert-fieldmeta-copies

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#7166

ObjectGrid no longer copies descriptionField, lookupColumns or lookupFilters onto a relational column's fieldMeta. No behaviour change — all three still reach the inline lookup picker, by a different seam.

Copy set: 10 keys to 7. Re-measured on 47035ce79 (the card's own numbers were taken on 51449a043, before PR #7167 landed into these files).

Ruling item 3 — the gating measurement: RELATIONAL_META_KEYS is INTERNAL

Clause 2 is not engaged, measured three ways, each with a live control:

proberelationalMetaKeyscontrol (useRowColor)
re-export in src/index.tsx barrelabsent (no export * either)line 30
built dist/index.d.ts0 occurrences of any of its symbolspresent
built dist/index.js export listnot exportedpresent

Every import of ./relationalMetaKeys in the repo lives inside packages/plugin-grid/src/ — the implementation plus two of its own tests. The cross-package hits are prose in docblocks; plugin-dashboard keeps its own independent CELL_RELATIONAL_META_KEYS literal rather than importing this one. Narrowing the copy set therefore moves no published surface.

What was measured

applyRelationalMeta writes onto the fieldMeta that generateColumns hands to CellRenderer as the field prop — 6 JSX passes across the three column-building paths, and nowhere else (as a whole object it otherwise only reaches applyRelationalMeta, which mutates it).

  • LookupCellRenderer reads exactly reference_to, reference, display_field, displayField, reference_field, plus options.
  • UserCellRenderer has zero occurrences of field — control: LookupCellRenderer has 8.

Stated by receiver rather than by count: packages/fields/src/index.tsx, the file holding every cell renderer, contains zero occurrences of the three retired keys, against a control of 22 occurrences of the display_field / displayField / reference_to spellings the cell does read.

Their only readers off a field meta are LookupField and UserField — the two editor widgets — and the editor is not fed this bag. renderCellEditor looks the field up in the object schema and spreads the whole def into the widget ({ name: ctx.column.accessorKey, ...fieldDef }), so every key a def carries reaches LookupField whether or not it is copied. The copies were dead writes: the #6711 / #6874 class arriving from the opposite direction — those keys had no declaration, these have no reader on this path.

The evidence bar this card exists to raise

Two of the three, descriptionField and lookupColumns, were added by #6875 as its fix, classified from a read-set derivation alone. Its third key displayField is genuinely delivered and stays copied — and it is the one that arrived with a rendering test.

A derivation establishes that a consumer READS a key. It does not establish that a given BAG is how the consumer gets it. Recorded in both docblocks.

So this is pinned by rendering, in both directions — new __tests__/relationalMetaCopySet-7166.test.tsx:

  • The cell. Four lookup columns over one referenced record, differing only in the key under test, all resolve the same text; the displayField control column resolves something else. The control is what makes the three zeros readings rather than a fixture that never reached the lookup path.
  • The editor. Each retired key still takes effect in the inline picker with the copy set no longer carrying it — descriptionField drives the secondary line, lookupColumns shapes the picker columns, lookupFilters scopes the candidates — each against a sibling control column that declares nothing. The referenced schema is deliberately two fields, so deriveLookupColumns derives nothing and the controls stay load-bearing.

Scope: 3 keys, not 6

The card measures six keys with no reader on this bag. They are two populations. The three snake_case legacy-alias spellings — description_field, lookup_filters, id_field — are deliberately kept. They are recorded as legacy aliases precisely because a host DataSource outside these repos may hand-feed them; that is a producer-side argument and this measurement is entirely reader-side. Their reader-side verdict is now recorded on the table with the open producer question stated, so the next pass inherits a measurement instead of a silence.

⛔ Reported, not fixed: the derivation gate cannot see this class

The gate's read set is a union over three consumers, two of which are not fed this bag. Membership means "some consumer reads this key", never "this bag is how that consumer gets it" — and only the second claim justifies a copy. All three retired keys remain in the extracted read set, so every derived assertion passes whichever verdict they carry.

Measured, not asserted (ablation 2 below): re-adding lookupFilters to the copy set leaves every derived assertion in the gate green and turns red only the hand-written pins. Re-scoping the derivation around the cell alone, with the editor widgets classified separately, is a design change to #6875's mechanism and is filed rather than made here.

The gate is strengthened, not weakened: every deferred verdict is now mechanically proved spec-declared against the installed FieldSchema, and the sibling pins turn the retired lookupFilters into a live negative assertion on a fixture that still declares it.

Tests

All runs are the canonical repo-root invocation (the repo's vitest guard refuses a package-cwd run as a false green). Union run on the final commit f4f299e23:

pnpm exec vitest run packages/plugin-grid/src packages/fields/src/__tests__/lookupCellDisplayField.test.tsx
Test Files 106 passed (106)
Tests 956 passed (956)

pnpm --filter @object-ui/plugin-grid run type-check — clean (first attempt reported TS2307: Cannot find module '@object-ui/*', which is an unbuilt dependency closure and NOT a result; re-run after pnpm --filter '@object-ui/plugin-grid^...' build). The new test file is confirmed present in the tsconfig.test.json program via --listFiles, so that green does cover it.

eslint . --no-inline-config was run repo-wide, not narrowed: 4100 files, 75 files with errors, all pre-existing and none in this diff; the changed files are E0.

Ablations

Both targets resolve from source (the root config aliases every @object-ui/* to src/, and both are relative imports), so no rebuild leg applies. Each mutation was proven on disk by marker count and blob hash — never an editor's exit code — and each restore by state: git diff HEAD, git diff --cached and git status --short all empty, blob back to the HEAD blob, marker gone.

  1. Break the editor's schema spread (predicted before running: the editor half goes red, the cell half stays green) — Tests 3 failed | 2 passed. Exactly B1/B2/B3 red; test A and the premise test green. The retirement's safety claim is load-bearing.
  2. Re-add lookupFilters to the copy set (predicted: derived gate assertions stay green, hand-written pins go red) — Tests 8 failed | 19 passed. The only derivation-gate failure is the hand-written finding(plugin-grid): 6 of the 10 keys ObjectGrid copies onto a column's fieldMeta have no reader on that bag — the picker keys reach the editor by a different seam #7166 pin; every derived assertion stayed green. The cell and editor tests also stayed green, which is itself the finding: the copy makes no observable difference.

Generated by Claude Code


Generated by Claude Code

…never reads
`applyRelationalMeta` no longer copies `descriptionField`, `lookupColumns` or
`lookupFilters` onto a relational column's `fieldMeta`. No behaviour change:
all three still reach the inline lookup picker, by a different seam.
`fieldMeta` is handed to `<CellRenderer>` as the `field` prop and nowhere else
— six JSX passes across the three column-building paths. A relational column
resolves to `LookupCellRenderer`, which reads exactly `reference_to`,
`reference`, `display_field`, `displayField`, `reference_field` and `options`;
a `user` column resolves to `UserCellRenderer`, which destructures `{ value }`
and reads no field meta at all. Measured by receiver: `fields/src/index.tsx`,
the file holding every cell renderer, contains zero occurrences of the three
retired keys, against a control of 22 for the spellings the cell does read.
Their only readers off a field meta are `LookupField` and `UserField` — the two
editor widgets — which `renderCellEditor` feeds from the schema def directly,
so every key a def carries reaches them whether or not it is copied.
Two of the three were added by objectui#6875, classified from a read-set
derivation alone; its third key `displayField` is genuinely delivered and stays
copied. A derivation establishes that a consumer reads a key, not that a given
bag is how the consumer gets it — recorded in both docblocks.
- New `relationalMetaCopySet-7166.test.tsx` renders both halves: the cell (four
columns differing only in the key under test resolve the same text, with a
`displayField` control that differs) and the editor (each retired key still
takes effect in the picker, each against a control column declaring nothing).
- The three snake_case `legacy-alias` spellings are deliberately kept: their
retention rests on an open producer-side question this reader-side
measurement does not touch. Their verdict is now recorded on the table.
- The derivation gate cannot enforce this retirement — its read set is a union
over three consumers, two of which are not fed this bag — and now says so.
Absence is pinned by an explicit assertion plus the rendering test.
- Gate strengthened: every `deferred` verdict is now mechanically proved
spec-declared, and the sibling pins turn the retired `lookupFilters` into a
live negative assertion on a fixture that still declares it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.2 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-VEpn3hxK.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)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
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)512.32KB116.52KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
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.98KB10.98KB
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)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
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)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)68.45KB19.20KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.65KB63.91KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.11KB32.61KB
plugin-gantt (index.js)165.21KB40.37KB
plugin-grid (index.js)207.19KB56.03KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.21KB27.60KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.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)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
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)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

os-dev-report

{
"issue": 7166,
"status": "done",
"branch": "claude/issue-7166-inert-fieldmeta-copies",
"pr": "https://github.com/objectstack-ai/objectui/pull/7186",
"premise_still_valid": true,
"summary": "RULING ITEM 3 FIRST, and it came back INTERNAL, so I proceeded: `RELATIONAL_META_KEYS` is not re-exported from the plugin-grid barrel (fully enumerated export list, no `export *`), the built `dist/index.d.ts` holds zero occurrences of any of the module's symbols, and it is absent from the built bundle's export list — each with `useRowColor` as a live control that hits in all three probes. Clause 2 not engaged. A2.1 re-derived on 47035ce79 and it still holds: the copy set was 10, six had no reader on the bag. Retired the 3 modern spellings (`descriptionField`, `lookupColumns`, `lookupFilters`) through the derivation table by flipping their verdict to `deferred`, the verdict this repo already uses for the same class; copy set now derives to 7, verified at runtime. Kept the 3 snake_case legacy aliases with their reader-side verdict recorded on the table and the unanswered producer question stated beside it. A2.3, A2.4 and A2.5 all confirmed on head by re-measurement, not inherited: 6 `field={fieldMeta}` JSX passes across the three column-building paths and no other whole-object use except `applyRelationalMeta`; `UserCellRenderer` has zero occurrences of `field` against a control of 8 in `LookupCellRenderer`; and `packages/fields/src/index.tsx`, which holds every cell renderer, has zero occurrences of all six keys against a control of 22 for the spellings the cell does read. The gate was strengthened, not weakened. Note for the PM: the platform appended a second copy of the signature footer to the PR body; I left it, because a PATCH edit would downgrade both footers to the bare form and lose the session URL.",
"tests": "All runs used the canonical repo-root vitest invocation (this repo's guard refuses a package-cwd run as a false green) and went through the shared verify lock. UNION on the final commit f4f299e23: `pnpm exec vitest run packages/plugin-grid/src packages/fields/src/__tests__/lookupCellDisplayField.test.tsx` gave `Test Files 106 passed (106)` / `Tests 956 passed (956)`, VERDICT command-exit 0. Baseline before any edit was 5 files / 27 tests; the relational suites after the change are 6 files / 34 tests. `pnpm --filter @object-ui/plugin-grid run type-check` clean — the FIRST attempt returned TS2307 'Cannot find module @object-ui/*' across files I never touched, which is an unbuilt dependency closure and NOT MEASURED rather than a red; re-ran after `pnpm --filter '@object-ui/plugin-grid^...' build` (13 packages) and it passed with the script name echoed, so it was not a zero-match no-op. Confirmed the new test file is actually IN the tsconfig.test.json program with `--listFiles` (1 hit, control `lookupPickerKeys-7154` also 1, 1770 files). LINT: ran `eslint . --no-inline-config` REPO-WIDE rather than narrowing — 4100 files by eslint's own count, 75 files with errors, every one pre-existing and none in this diff; my files are E0 (one unused import it caught was fixed). Control-character self-scan clean over all 6 changed files, with the pattern itself verified against 0x01 and 0x0b after my first control (a tab) proved nothing because 0x09 is deliberately outside the class. ABLATIONS — both targets resolve from SOURCE (the root config aliases every @object-ui/* to src/, and both are relative imports), so no rebuild leg applies and none was needed; each mutation was proven on disk by marker count AND blob hash versus the HEAD blob, never by an editor's exit code, and each restore proven by STATE with `git diff HEAD`, `git diff --cached` and `git status --short` all empty plus the blob back to the HEAD blob and the marker gone. Both ran against the COMMITTED implementation. (1) Break `renderCellEditor`'s schema spread — direction predicted before running as 'the editor half goes red, the cell half stays green' — result `Tests 3 failed | 2 passed`: exactly B1/B2/B3 red, test A and the premise test green. (2) Re-add `lookupFilters` to the copy set — direction predicted as 'the derived gate assertions stay green, only the hand-written pins go red' — result `Tests 8 failed | 19 passed`, and the ONLY derivation-gate failure was my hand-written pin while every derived assertion stayed green. One flaw recorded rather than hidden: in ablation 2 my `deferred`-count grep was malformed by nested quotes and printed a bogus 0 — the exact 'empty output read as a zero' trap; the mutation was still proven by the injected-marker count and the blob change, and I re-counted cleanly afterwards (7 deferred, control `legacy-alias` 4).",
"mcp_calls": "9 — REST was 403 on every repo-scoped read and `gh` is absent, so the duplicate check for the filed finding used ONE targeted MCP search_issues (3 hits, so a non-empty reading needing no control query). Channel switch declared.",
"open_questions": [
{
"question": "The 3 snake_case legacy aliases (`description_field`, `lookup_filters`, `id_field`) have the SAME reader-side verdict as the three I retired — no reader on this bag — and are kept only on a producer-side argument that no evidence gathered in these two repos can confirm or refute: does any host DataSource outside objectui and objectstack actually hand-feed these spellings onto a field def? Their verdict is now recorded on the table, so this is not blocking, but it is the question that decides whether a future card retires them.",
"options": [
"A. Leave as is. The reader-side verdict is now recorded with the open producer question stated beside it, so the next pass inherits a measurement instead of a silence.",
"B. Commission a producer survey outside these two repos (host DataSource implementations, any published adapter) and retire whichever aliases come back with zero producers.",
"C. Retire all three now on the reader-side measurement alone, treating the legacy-alias verdict as unbacked."
],
"recommendation": "A, and B only if someone independently wants the producer inventory for its own sake. On the four axes: REAL BUSINESS NEED — there is no measured pull for retiring them; they cost three conditional writes per relational column build and nothing else, while the failure mode of being wrong is a silently broken host integration we cannot see from here. LONG-TERM SOUNDNESS — the honest state is now encoded rather than implied; retiring on reader-side evidence would set the precedent that a back-compat verdict can be overturned by a measurement that does not address it. AI-ERROR-RESISTANCE — this is the axis that argues hardest against C: these are runtime-only spellings no spec-compliant producer can emit, so the strict FieldSchema already stops an AI-authored app from declaring them, and removing the runtime tolerance buys no authoring-time safety while risking a live host. STARTUP SCOPE — C spends real risk on tidiness with zero pull, and B spends survey effort on the same. C is explicitly out of scope per the ruling and I did not take it."
}
],
"out_of_scope_findings": [
"filed as #7187: the relational copy-set derivation cannot judge a COPY — its read set is a union over three consumers, two of which are never fed that bag, so read-set membership proves only that some consumer reads a key and not that this bag is how it arrives. Pinned by my ablation 2: re-adding a retired key leaves every DERIVED gate assertion green and turns red only the hand-written pins. Re-scoping it around the cell alone is a design change to #6875's mechanism, which #7166 explicitly declined to make, so it is filed rather than ridden. Unassigned, labels finding + domain:ui.",
"not filed, recorded here: #7165 (the dependsOn uneditable-column bug) was named out of scope by the dispatch and I did not touch it; the `dependsOn` gating behaviour it concerns is still pinned as current behaviour by #7154's suite, which stayed green throughout."
]
}

Generated by Claude Code

@os-warrenClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — domain:ui seat (session session_012wwHa4aaFybxXrfmfHioDM), reviewer of record

Verdict: ACCEPT. Arming on green.

Ruling item 3 was answered first and answered properly

I gated this card on one question — is RELATIONAL_META_KEYS public surface? — because a "yes" made the whole thing unlandable at today's tier. It came back internal, and not on one probe but three, each carrying useRowColor as a live control that hit in all three:

  1. not re-exported from the plugin-grid barrel (fully enumerated export list, no export * — which is what makes a negative readable at all);
  2. the built dist/index.d.ts holds zero occurrences of any of the module's symbols;
  3. absent from the built bundle's export list.

⭐ Three independent probes rather than one, and the export * check is the detail that matters: a barrel with a star export would have made probe 1 meaningless while still looking like a clean negative. Clause ② not engaged, correctly established rather than assumed.

⭐ The self-reported flaw is the most valuable line in the report

"in ablation 2 my deferred-count grep was malformed by nested quotes and printed a bogus 0 — the exact 'empty output read as a zero' trap"

Reported rather than buried, with the recovery stated: the mutation was still proven by the injected-marker count and the blob change, so the ablation's validity never depended on the broken probe, and the count was re-taken cleanly afterwards (7 deferred, control legacy-alias 4).

⚠️ This is the third instance of that exact trap in this seat's work today — I hit it verifying #7139 (a malformed control that printed empty beside a subject's 0), and again this morning pointing a pack probe at a barrel file. It is not a rare slip; it is the default failure of any grep written inside nested quoting. A dev who reports it, and who can show the result did not rest on it, is worth more than one who never trips it.

The same instinct shows up on the control-byte scan: the first control was a tab, which proved nothing because 0x09 is deliberately outside the class, so the pattern was re-verified against 0x01 and 0x0b. Noticing that your control is a dud is the whole skill.

The retirement was done through the table, and the gate came out stronger

Copy set 10 → 7, verified at runtime, by flipping the three modern spellings to deferred — the verdict this repo already uses for the class, rather than a new one. And the gate was strengthened, not weakened: every deferred verdict is now mechanically proved spec-declared, and the two sibling pins turn the retired lookupFilters into a live negative assertion on a fixture that still declares it. That last part is what stops this retirement from being invisible to the next reader.

Ablation 2 is the proof the scope call was right: re-adding a retired key leaves every derived gate assertion green and turns red only the hand-written pins (Tests 8 failed | 19 passed). Ablation 1 split the population exactly as predicted (3 failed | 2 passed, B1/B2/B3 red, test A and the premise test green).

The three legacy aliases: A, leave as is — ruling the open question

The snake_case aliases have the same reader-side verdict as the three retired, and are kept only on a producer-side argument no evidence from these two repos can address. A is right, and the axis that decides it is the one the report names: these are runtime-only spellings no spec-compliant producer can emit, so the strict FieldSchema already stops an AI-authored app from declaring them. Removing the runtime tolerance buys no authoring-time safety while risking a live host integration we cannot see. Three conditional writes per column build is not a cost worth that.

C would set a precedent worth refusing explicitly: a back-compat verdict cannot be overturned by a measurement that does not address it. The reader-side census is silent on producers by construction. Recording the verdict beside the open question — which is what shipped — is the durable improvement.

#7187 is the right filing, and it generalises the lesson that produced this card

the relational copy-set derivation cannot judge a COPY — its read set is a union over three consumers, two of which are never fed that bag

⭐ That is the mechanised form of the rule #6875 cost us: a derivation establishes that a consumer reads a key; it does not establish that this bag is how it arrives. And it is now pinned by ablation 2 rather than asserted — the gate's own blindness is demonstrated, not argued. Re-scoping the derivation around the cell alone is a design change to #6875's mechanism, which this card explicitly declined to make, so filing was correct.

Also correct


Nothing to change.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-warren@claude