Skip to content

fix(plugin-grid): scope the relational copy-set derivation to the cell that is fed the bag - #7584

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-7187-relational-copy-set-derivation
Sep 4, 2026
Merged

fix(plugin-grid): scope the relational copy-set derivation to the cell that is fed the bag#7584
os-project-manager merged 1 commit into
mainfrom
claude/issue-7187-relational-copy-set-derivation

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes #7187

Clause-②: no — the whole diff is two files inside packages/plugin-grid/src plus one changeset. relationalMetaKeys.ts is not re-exported from the package entry (src/index.tsx, the vite.config.ts lib entry), so none of RELATIONAL_META_READ_SET, RelationalMetaVerdict, RelationalMetaEntry or RELATIONAL_META_KEYS is in dist/index.d.ts. Measured: zero hits for relationalMetaKeys|RELATIONAL_META|RelationalMeta in src/index.tsx, lit control ObjectGrid in the same query shape returns 5. The copy set itself is byte-identical — the same seven keys, in the same order — so no runtime surface moves either.

The defect

The gate re-extracted a read set from three consumers and collapsed it into a union, then licensed a copy-set entry by membership in it. Only one of the three is fed the bag: applyRelationalMeta writes onto the fieldMeta that generateColumns hands to CellRenderer, while ObjectGrid.renderCellEditor builds the two inline editor widgets' props from the object schema ({ name: ctx.column.accessorKey, ...fieldDef }). So membership meant "some consumer reads this key" and never "this bag is how that consumer gets it" — and a copy-set entry asserts the second.

Measured on this base, which is why the union could not judge a copy:

consumer keys read fed this bag
index.tsx#LookupCellRenderer 6 yes
widgets/LookupField.tsx 25 no
widgets/UserField.tsx 11 no
union (what the gate used) 25

19 of the 25 union members are read by no consumer that is fed the bag.

What landed

  1. The reader axis is recorded per consumer. Every entry carries readers, and the gate checks each declared list against that consumer's own source in both directions — a new spelling in a chain is unclassified, a hand-widened declaration is an orphan, both red.
  2. The copy set is derived from CONSUMERS_FED_THIS_BAG — the cell alone — on two conditions: the verdict licenses a copy at all (producer half), and a consumer fed the bag reads the key (reader half). The one deliberate exception, the three snake_case spellings kept on an unanswered producer-side question, must name itself per key (copiedWithoutCellReader), and the gate confines that exit to keys FieldSchema does not declare — so no authorable key can take it. This is not a widening of the union; it is a strict narrowing plus an exception that has to state its own reason.
  3. The premise is measured, not asserted. The gate bounds renderCellEditor in ObjectGrid.tsx and requires it to spread the schema def and to never name fieldMeta. Three positive controls bound the region (...fieldDef, objectSchema, FieldEditWidget) and a lit control proves the instrument can see the identifier (fieldMeta occurs more than 10 times in the file, 0 times in that region).

The three open questions, decided

1. Split the extractor into cell / editor sets? Yes — but the split lands in the table, not only in the extractor. The extractor already computed per-consumer sets; the defect was that only the union crossed into the table. Making the per-consumer fact cross into the table (readers) is what lets the copy set be derived from the cell alone, and it is what makes the split checkable in both directions instead of being an internal detail of one function.

2. Does deferred survive? No. It meant "spec-declared and read only by an editor widget" — a reader-axis fact, hand-written, inside an enum that otherwise records the producer axis. Both halves are now mechanically known: spec-declaredness from FieldSchema, editor-only-ness from the extractor. A hand-written verdict restating two measured facts is precisely the shape this table's history warns about — read-set membership was taken for a licence to copy and had to be undone. The seven keys are spec now, which is what they are, and they stay off the copy set because no consumer fed the bag reads them. handled-elsewhere is kept: it records a decision (options is written by translateOptions; a raw copy would undo that) that neither axis can derive.

3. How is UserField's forwarding restated? As a checked result, with UserField still swept. The gate asserts UserField's extracted set is a subset of LookupField's and names any key UserField reads that LookupField does not — the false zero the sweep exists to catch. Dropping it because "it forwards anyway" is exactly the assumption that would hide such a key. Under the split the subset relation is no longer load-bearing for copying, because both are editors and neither licenses a copy either way; that is asserted too.

None of the three moves a published surface, so none of them became a stop.

Re-running the load-bearing ablation

The card's central measurement is nine rounds old and triage did not re-run it. Re-run on ccb3ad78a62b before any design work: it reproduces exactly. Re-adding lookupFilters to the copy set (deferred back to spec), across the four relationalMetaCopySet* suites:

 Test Files  4 failed (4)
      Tests  8 failed | 19 passed (27)

8 failed | 19 passed is the card's number to the digit. And the only failure inside the derivation gate was the hand-written pin, exactly as filed — the nine other tests in that file stayed green, including "no unclassified key", "no orphan", the spec proofs, and "copy set equals the copied verdicts". No finding to report; the reading was still good.

Mutation proven on disk before any result was read (anchor lookupFilters: { verdict: 'deferred' 1 to 0, injected anchor 0 to 1, blob 000988fa to de6af77c). Restore proven by state under trap ... EXIT INT TERM with absolute paths, via git checkout HEAD -- ABS_PATH: blob back to 000988fa and git diff HEAD empty.

Discrimination — the gate now moves on the FACT

Four legs, each mutating a fact rather than an assertion, each with the mutation proven on disk (anchored fixed-string counts plus git hash-object against the HEAD blob) before any result was read, and each restore proven by state (git diff HEAD empty and on-disk blob equal to the HEAD blob) under trap ... EXIT INT TERM with absolute paths. Baseline at this commit: Tests 12 passed (12).

leg mutation result derived assertions that went red
A2 lookupFilters claims copiedWithoutCellReader while spec 2 failed | 10 passed the exit is confined to legacy-alias
A3 lookupFilters to legacy-alias and claims the exit 3 failed | 9 passed legacy-alias non-authorability; the exit's own spec check
A4 the fact: a lookupFilters cast read added to LookupCellRenderer in packages/fields/src/index.tsx 3 failed | 9 passed "no unclassified spelling" for the cell; the copy-set derivation
A5 the declaration hand-widened: readers to ALL_THREE with no source support 3 failed | 9 passed "no orphan"; the copy-set derivation

A2 and A3 are the two routes by which the three retired keys could be re-added; A4 is the fact the derivation now rests on; A5 is the attempt to fake that fact in the table.

The copy-set assertion is deliberately derived from the extracted cell set rather than from the table's readers, so RELATIONAL_META_KEYS and the gate's expectation reach the same list by two independent routes and a hand-edited readers cannot carry both. That is why A5 turns it red.

The hand-written pin is now redundant as a guard — shown, not claimed

In every one of the four legs at least one derived assertion went red on its own; deleting the named-key pin would leave the gate red in all four. That answers the card's condition directly: the retirement is no longer held by the pin.

It is kept anyway, restated. It no longer says "the derivation cannot enforce that" — it now asserts the derived fact for the three keys by name (the cell does not read them, and their editor readers are non-empty), so a regression is reported by name instead of only as an equality mismatch. relationalMetaCopySet-7166.test.tsx is untouched and still renders both halves.

Verification

All runs from the repo root, exit codes captured by redirect-then-read (never through a pipe), heavy runs serialized through the container's shared verify lock. Union re-run after the final commit, at c70a04570:

  • pnpm exec vitest run over the four relationalMetaCopySet* suites plus lookupPickerKeys-7154, lookupDisplayFieldSpelling-6875, and scripts/__tests__/one-authority-per-exported-name-6273 (the new exported names): Test Files 7 passed (7), Tests 47 passed (47). Baseline on ccb3ad78a62b for the six relational suites was Tests 34 passed (34); it is 36 now, the gate having grown from 10 tests to 12. The five behavioural suites are unchanged and green, which is the copy set being byte-identical.
  • pnpm --filter @object-ui/plugin-grid type-check — exit 0, tsc --noEmit && tsc -p tsconfig.test.json. Both changed files are in the checked set, not assumed: --listFiles returns 1 hit each out of 1799 listed, so "typecheck is clean" is a statement about them.
  • node scripts/check-changeset-presence.mjs — exit 0: "2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s) ... Every one of them has an EMPTY frontmatter — declared as releasing nothing, which is the explicit exemption and a complete answer to this gate."
  • node scripts/check-control-bytes.mjs — exit 0: "check-control-bytes: OK (scanned 6245 tracked text file(s); skipped 85 binary)." Plus a self-scan of the three changed files for raw control bytes: no hits.
  • node scripts/check-changeset-fixed.mjs / check-changeset-no-major.mjs / check-changeset-overwrite.mjs / check-lint-coverage.mjs — all exit 0 ("No changeset declares a major bump.", "No pre-existing changeset was modified or deleted.", "lint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).").
  • Lint was not narrowed. eslint . ran over its own full population — 4273 files by its --format json count — with --no-inline-config, the stricter form. My two files: 0 errors. The 77 files carrying errors are all untouched by this diff and all report react-hooks/static-components / no-console suppressed by inline comments the strict form disables. With the real config the two files give 0 errors and 3 no-explicit-any warnings, all on lines that predate this change ((FieldSchema as any).shape, and applyRelationalMeta's two Record of string to any parameters). This repo's eslint is not type-aware (tseslint.configs.recommended, no project / projectService in eslint.config.js), so this diff cannot move any untouched file's verdict.

Narrowings declared

  • Test selection. Ran the relational suites and the exported-name authority gate, not the repo's whole vitest run. A wider run could have caught a consumer of RELATIONAL_META_* outside these files; a repo-wide grep finds none — the only other references are plugin-dashboard's prose comments and its own independent CELL_RELATIONAL_META_KEYS literal, neither of which imports from here. CI runs the full farm regardless.
  • Gate selection. Derived by hand from package.json and the workflows rather than from scripts/pm/dispatch-gates.mjs — that script lives only in objectstack and answers about the tree it sits in, so pointing it at objectui paths would return a well-formed wrong list. Ran the changeset family, control-bytes, lint-coverage, lint, type-check, and the exported-name authority gate (implicated by the two new exported names). No gate under scripts/ was edited, so no gate's own test suite is owed.
  • The ablations ran against source, not dist. No rebuild is owed: the repo-root vitest.config.mts aliases @object-ui/fields and @object-ui/plugin-grid to their src directories (lines 417-419), and the mutated module is reached by a relative import (../relationalMetaKeys), so no exports-to-dist resolution is in the path. The dist-preflight failure mode — a mutation that never reaches the running code and leaves the ablation silently green — cannot occur here.

What is NOT measured

Whether any host DataSource outside these two repos feeds fieldMeta to something other than CellRenderer. Every sweep behind this bounds packages/ and apps/ in this repo, and CONSUMERS_FED_THIS_BAG is a statement about this repo's own wiring. The producer-side question the three snake_case spellings rest on is likewise untouched — it needs a producer survey, not another reader sweep, and copiedWithoutCellReader now records that per key instead of leaving it to a docblock.


Generated by Claude Code

…l that is fed the bag

The derivation gate extracted a read set from three consumers and collapsed it
into a UNION, then licensed a copy-set entry by membership in it. Only one of
the three — `LookupCellRenderer` — is handed the bag `applyRelationalMeta`
writes; `ObjectGrid.renderCellEditor` feeds the two inline editor widgets
`{ name, ...fieldDef }` off the object schema instead. So membership meant
"some consumer reads this key" and never "this bag is how that consumer gets
it", while a copy-set entry asserts the second. That conflation shipped two
wrong verdicts (objectui#6875) which objectui#7166 then had to undo, and the
gate was green through both.

- Record the reader axis PER CONSUMER on every entry (`readers`), checked
  against that consumer's own source in both directions.
- Derive the copy set from `CONSUMERS_FED_THIS_BAG` — the cell alone — with one
  self-naming exception (`copiedWithoutCellReader`) confined to keys
  `FieldSchema` does not declare, so no authorable key can take it.
- Assert that bound on `ObjectGrid.tsx` itself: `renderCellEditor` must spread
  the schema def and must never name `fieldMeta`.
- Remove the `deferred` verdict — it stated a fact that is now measured. Its
  seven keys are `spec` and stay off the copy set because no consumer fed the
  bag reads them.

The copy set is unchanged: the same seven keys, in the same order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3182.7 KB 3191.4 KB
Main entry chunk (gzip) 143.2 KB 350 KB
Entry file index-CHM8wzfg.js
Status PASS

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 516.21KB 117.82KB
core (index.js) 6.12KB 2.42KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 180.00KB 50.20KB
fields (index.js) 242.40KB 61.26KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.98KB 10.98KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.55KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 48.09KB 13.34KB
plugin-charts (index.js) 70.92KB 19.75KB
plugin-chatbot (index.js) 196.19KB 46.43KB
plugin-dashboard (index.js) 132.89KB 34.68KB
plugin-designer (index.js) 212.87KB 43.19KB
plugin-detail (index.js) 250.93KB 64.09KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.87KB 32.66KB
plugin-gantt (index.js) 167.44KB 41.05KB
plugin-grid (index.js) 210.75KB 56.95KB
plugin-kanban (index.js) 52.71KB 14.55KB
plugin-list (index.js) 113.28KB 27.59KB
plugin-map (index.js) 20.55KB 6.80KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.57KB 11.96KB
plugin-timeline (index.js) 30.84KB 8.85KB
plugin-tree (index.js) 9.38KB 3.22KB
plugin-view (index.js) 85.24KB 20.94KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.58KB 2.23KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@os-project-manager
os-project-manager marked this pull request as ready for review September 4, 2026 02:29
@os-project-manager
os-project-manager added this pull request to the merge queue Sep 4, 2026
Merged via the queue into main with commit 2099df4 Sep 4, 2026
34 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-7187-relational-copy-set-derivation branch September 4, 2026 02:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants