fix(plugin-grid): feed the inline cell editor the row as dependent values, so a dependsOn lookup column is no longer permanently uneditable - #7193

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-7165-grid-dependent-values
Sep 1, 2026
Merged

fix(plugin-grid): feed the inline cell editor the row as dependent values, so a dependsOn lookup column is no longer permanently uneditable#7193
os-warren merged 3 commits into
mainfrom
claude/issue-7165-grid-dependent-values

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Part of #7165

#7165 must stay open. This ships option A, the interim, and is deliberately not a closing reference to that card — the card stays open for option B, filed as #7188. See "Measurement 1" below — that is the fact that determined this.

Measurement 1 — B is Clause ②-engaged, so A ships as the labelled interim

The PM ruling on #7165 made the A/B choice depend on one measurement: is renderCellEditor's editor context type published surface, or internal to plugin-grid? Measured on 899730e0a, both disjuncts of that test are true:

  • Reachable from @object-ui/typespackages/types/src/data-display.ts declares renderCellEditor?: (ctx: { column; row; value; stage; commit; cancel }) => React.ReactNode on DataTableSchema, with a Zod mirror in packages/types/src/zod/data-display.zod.ts.

  • Part of DataTableSchema's declared editor-context contract — declared by maintainer ruling 2026-08-30 ([Decision] Declare renderCellEditor and schema-level cellClassName on DataTableSchema? — the two live undeclared keys the #6459 census measured #6882), replacing the (schema as any) cast that preceded it. The shape is pinned by exact type equality, not extends, in packages/types/src/__tests__/data-table-declared-keys-6882.test.ts:

    type _RenderCellEditorShape = Expect[ Equal[
    Declared[DataTableSchema]['renderCellEditor'],
    ((ctx: CellEditorContext) => ReactNode) | undefined ] ];
    

    (angle brackets transcribed as square brackets — GitHub's body sanitizer eats
    tag-shaped fragments, including inside a fence. The source uses angle brackets.)

    with the stated reason: "Declaring the key with any other shape is a different (and false) statement about the renderer, so the shape is pinned, not just the membership."

packages/plugin-grid/src/ObjectGrid.tsx states the same fence in its own words: "⛔ Do not 'fix' either hold by declaring the key on DataTableSchema as a rider — that package is published surface with its own review floor."

⇒ B needs a seventh member on that declared context. That edits @object-ui/types, its Zod mirror, and a maintainer-ruled exact-shape pin. Per the ruling, A ships under all three of its conditions: labelled in code, this card not closed, and B filed (#7188).

⚠️A is not the conclusion. A field that can never be filled is a hard defect with no workaround; a field scoped by the persisted parent instead of the staged one is wrong only where the parent was edited in the same unsaved session. Strictly better, strictly not finished.

The defect

LookupField reads the record it gates on as dependentValues ?? ctx.formValues ?? ctx.data ?? {}. The grid's inline cell editor supplied none of the three — renderCellEditor rendered FieldEditWidget with field / value / onChange only, SchemaRendererContext has no formValues, and the grid sets no ctx.data per row. The record was therefore {} for every row, dependenciesMissing was permanently true, and a column declaring dependsOn rendered a disabled trigger reading "Select region first" even when the row carried the parent value.

PR #2216 was the two-half answer to #2215. Half 2 — every picker taking the dependsOn chain as a hard baseFilter — is host-independent and was already live on the grid path, which is why the gate fired at all (verified: A2.5 holds, see LookupField's dependentFilter memo). Half 1 is per-host and the grid never got it. This PR supplies that missing input and re-implements no cascade.

The change

One line of behaviour in ObjectGrid.renderCellEditordependentValues={ctx.row} — plus row: any added to the local ctx annotation, which had omitted it (the declared contract always carried row; only this local annotation did not). FieldEditWidget already forwards dependentValues to the widget, since #7008.

The interim is labelled in the code, naming this card and #7188, and stating precisely what is stale: a parent edited but not yet saved in the same row does not re-scope the child.

Evidence

Rendering proof, packages/plugin-grid/src/__tests__/gridDependentValues-7165.test.tsx (4 tests, each with a live control in the same render):

  1. the dependsOn column opens (was lookup-trigger-gated + disabled; now lookup-trigger-regional_owner + enabled) — control: a no-dependsOn column in the same render is also enabled, so the picker path is proven reached.
  2. the picker is scoped by the row: region: 'north' reaches the query as a hard $filter and Person 07 (south) is absent — control: the no-dependsOn sibling over the same records does offer Person 07, so the absence is scoping and not a dead picker.
  3. negative control: a row whose saved parent is empty still gates, so the gate was not simply disabled — control: the filled row in the same render opens.
  4. ⚠️the interim's staleness, pinned as current behaviour: staging a new parent without saving does not re-scope the child — with its own proof that the staging actually landed (the region cell renders the pending 'south' while the saved record still reads 'north'), because without that the assertion would pass for the trivial reason that nothing was ever staged. The grid's inline cell editor scopes a dependsOn lookup by the SAVED row, not the staged one — carry the pending record across the renderCellEditor seam (option B of #7165) #7188 flips this test.

The #7154 pin was UPDATED, not deleted. The dependsOn case in lookupPickerKeys-7154.test.tsx pinned the gated behaviour, i.e. it encoded the defect. A deleted pin is indistinguishable from a pin that never existed, so it is rewritten in place to assert the fixed behaviour plus the scoping, and its header prose is rewritten to say so.

Ablation — direction predicted before running: removing the single dependentValues={ctx.row} line turns red the 4 new tests plus the updated #7154 case, and leaves the other 4 #7154 cases green.

  • No rebuild required, and this is measured rather than assumed: the root vitest.config.mts aliases every @object-ui/* specifier to that package's src, and the tests import ../ObjectGrid relatively — so nothing resolves through dist and a source mutation is directly live.
  • Mutation confirmed on disk by marker count (dependentValues={ctx.row}: 1 → 0) and blob hash (e8ab332e…f219c865…), not by the editor's exit code.
  • Result: Tests 5 failed | 4 passed (9) — exactly the predicted set, passing count included.
  • Restore proven by state, with HEAD named explicitly (git checkout HEAD -- ..., never the bare form, which restores from the index): git diff HEAD, git diff --cached and git status --short all empty, and the blob hash back to e8ab332e…. The script carried a trap ... EXIT INT TERM with absolute paths.

Gates

⚠️Two heads, stated separately so the provenance is checkable. The full set ran at f17b08047; main then advanced (#7180 landed), so it was merged in and the affected-package suite plus the cheap ratchet gates were re-run at the current head aba0c7499. Nothing below is quoted from a tree that is no longer this branch's head.

Re-run at aba0c7499 (current head, post-merge):

gateresult
packages/plugin-grid/ full suite105 files, 949 tests passed
check:control-bytesOK — 5958 tracked text files scanned
check:phantom-depsOK
check:changeset-no-majorOK

Run at f17b08047 (same tree for every path this PR touches — the merge brought in only packages/components/src/renderers/layout/containers.tsx, a .changeset, a docs page, a components test and one vitest.config.mts line, none of which this PR edits):

gateresult
type-check (plugin-grid + closure)14 tasks successful
lint (affected package, full, not narrowed)2 tasks, 0 errors (751 pre-existing warnings)
check:self-importOK
check:vi-mock-specifiers / check:vi-mock-inheritOK (4106 files, 525 with mocks)
check-governed-queue-guard --testNOT GOVERNED — 4 paths vs 5 surfaces, none matched

The typecheck genuinely covers the new tests: tsc -p tsconfig.test.json --listFiles lists all three edited files among its 1770 (this package typechecks tests via a dedicated project, so "typecheck green" is not silently excluding them).

⚠️#7166 — the concurrent lane in this same package — had not landed as of aba0c7499. It touches relationalMetaKeys.ts and the applyRelationalMeta copy set; this PR touches ObjectGrid.renderCellEditor's context construction. Disjoint symbols, but this branch will need a fresh merge if #7166 lands first.

Incidence — measured, and ⛔ not "no impact"

Two corpora, each instrument run with a live control:

  • this repo: 3 files match dependsOn|depends_on, and none is this defect — the gantt hit is depends_on as task predecessor links (a different key entirely) and the other is a select inside a form. Control: the same instrument returns 88 files across packages/.
  • hotcrm (a real shipped app, anonymous shallow clone): 6 dependsOn declarations on lookup fields across 5 objects (crm_contract ×2, crm_case, crm_quote ×2, crm_opportunity), all scoping contacts to crm_account. But 0 editable grids in its views (control: the views directory exists and was read).

⇒ Neither measured corpus reaches the defect today, because none combines dependsOn with an editable grid — but hotcrm is one metadata key away: editable: true on any of those five objects' list views hits it immediately. Customer apps are unmeasured, and the parent pattern is demonstrably real in shipped metadata. This is a bounded reading, not an all-clear.

Filed, not ridden

⚠️ Still live, per the PM's fence and not settled by this PR: should the user be told the scope came from the saved row? The interim's staleness is the same silent-wrongness class as the three refusals this seat landed in #7146 / #7147 / #7148. It is recorded on #7188 rather than left to disappear into a code comment.


Session: session_012wwHa4aaFybxXrfmfHioDM (https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM) — kept in prose because a body edit rewrites the footer below to its bare form.

Generated by Claude Code

…lues
A `dependsOn` lookup column in an editable ObjectGrid was permanently
uneditable. `LookupField` resolves
`dependentValues ?? ctx.formValues ?? ctx.data ?? {}` and the grid's
`renderCellEditor` supplied none of the three, so the resolved record was
`{}` for every row, `dependenciesMissing` stayed `true`, and the trigger
rendered disabled ("Select region first") even when the row carried the
parent value.
PR #2216closed#2215 in two halves: the form renderer injects its live
watched record, and every picker takes the `dependsOn` chain as a hard
`baseFilter`. Half 2 is host-independent and was already live here; half 1
is per-host and the grid never got it. This supplies that missing input and
re-implements no cascade.
INTERIM: `ctx.row` is the saved record, so a parent edited but not yet
saved in the same row does not re-scope the child. Carrying the staged
record needs a seventh member on `renderCellEditor`'s published context
type (declared in `@object-ui/types`, #6882, and pinned by exact type
equality), which is a contract change tracked separately.
The `dependsOn` case in `lookupPickerKeys-7154.test.tsx` pinned the gated
behaviour and is UPDATED, not deleted.
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.6 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-C6RhwKZM.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)514.13KB117.19KB
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)206.14KB55.77KB
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

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.6 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-C6RhwKZM.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)514.13KB117.19KB
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)206.14KB55.77KB
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": 7165,
"status": "done",
"branch": "claude/issue-7165-grid-dependent-values",
"pr": "https://github.com/objectstack-ai/objectui/pull/7193",
"premise_still_valid": true,
"summary": "MEASUREMENT 1: B IS Clause 2-engaged — so option A shipped as the labelled interim and ⛔ #7165 MUST STAY OPEN (the PR says 'Part of #7165', never a closing keyword). The editor context is published surface on both disjuncts of the test: `DataTableSchema.renderCellEditor` is declared in packages/types/src/data-display.ts with a Zod mirror, declared by maintainer ruling 2026-08-30 (#6882) replacing a `(schema as any)` cast, and its exact shape is pinned by an `Equal` (not `extends`) type assertion in packages/types/src/__tests__/data-table-declared-keys-6882.test.ts whose comment states the shape is pinned deliberately; ObjectGrid.tsx independently fences that package as 'published surface with its own review floor'. B needs a seventh ctx member, so it edits @object-ui/types, its Zod mirror and that maintainer-ruled pin. All three ruling conditions are met: the interim is labelled in code naming #7165 and the follow-up and stating exactly what is stale, the card is not closed, and B is filed as #7188. Premise re-measured on head 899730e0a: the existing pin still passed before my change, i.e. the defect was still live. A2.1/A2.3/A2.4/A2.5 all confirmed; A2.2 needed one CORRECTION — the declared contract always carried `row`, but ObjectGrid's LOCAL ctx annotation omitted it, so A was a two-part edit (widen the local annotation, then pass it), not a literal one-liner.",
"tests": "All runs from repo root via the shared verify lock (objectui forbids `pnpm --filter PKG test`; path filters only). BASELINE on 899730e0a before any edit: `pnpm exec vitest run packages/plugin-grid/src/__tests__/lookupPickerKeys-7154.test.tsx` -> 'Test Files 1 passed (1) / Tests 5 passed (5)' — the gated pin still passed, so the defect was live on head. AFTER FIX, same command -> 'Tests 1 failed | 4 passed (5)', 'AssertionError: expected lookup-trigger-regional_owner to be lookup-trigger-gated' — the pin encoded the defect, so it was UPDATED IN PLACE (not deleted), header prose rewritten to say so. NEW FILE packages/plugin-grid/src/__tests__/gridDependentValues-7165.test.tsx, 4 tests each with a live control in the same render: (1) dependsOn column now opens, control column also opens; (2) picker SCOPED — $filter.region='north' reaches the query, Person 07 (south) absent, while the no-dependsOn control over the same records DOES offer Person 07; (3) NEGATIVE CONTROL — a row whose saved parent is empty still gates, proving the gate was not disabled; (4) the interim's staleness pinned as current behaviour, with its own proof the staging landed (region cell renders pending 'south' while the saved record reads 'north'), which is what stops it passing vacuously. Both files together: 'Test Files 2 passed (2) / Tests 9 passed (9)'. ABLATION — direction predicted BEFORE running (remove `dependentValues={ctx.row}` -> the 4 new tests plus the updated 7154 case go red, the other 4 stay green). NO REBUILD NEEDED, and that is measured not assumed: root vitest.config.mts aliases every @object-ui/* specifier to that package's src and the tests import ../ObjectGrid relatively, so nothing resolves through dist and a source mutation is directly live. Mutation confirmed ON DISK by marker count (1 -> 0) AND blob hash (e8ab332e… -> f219c865…), never by the editing tool's exit code. Result: 'Tests 5 failed | 4 passed (9)' — exactly the predicted set, passing count included. Restore proven BY STATE with HEAD named explicitly (`git checkout HEAD -- ABSPATH`, never the bare form which restores from the index): git diff HEAD, git diff --cached and git status --short all empty and the blob hash back to e8ab332e…; the script carried `trap ... EXIT INT TERM` with absolute paths. GATES: at final head aba0c7499 (after merging main, which had advanced to 8ec11e14f / #7180) — full packages/plugin-grid/ suite 'Test Files 105 passed (105) / Tests 949 passed (949)', check:control-bytes OK (5958 files), check:phantom-deps OK, check:changeset-no-major OK. At f17b08047 (identical tree for every path this PR touches): type-check plugin-grid + closure 14 tasks successful, lint of the affected package in FULL (not narrowed) 2 tasks 0 errors (751 pre-existing warnings), check:self-import OK, check:vi-mock-specifiers / check:vi-mock-inherit OK (4106 files, 525 with mocks), check-governed-queue-guard --test = NOT GOVERNED (4 paths vs 5 surfaces). Verdicts read from each gate's own printed line, with exit codes captured after redirection, never through a pipe. NOT-MEASURED check cleared: `tsc -p tsconfig.test.json --listFiles` lists all three edited files among its 1770, so the green typecheck genuinely covers the new tests rather than excluding them.",
"mcp_calls": "11 — 2 issue reads, 2 targeted search_issues (each with a control word that hit: #7165 itself, then #7154/#2215), 2 issue creates, 1 PR create, 1 PR body read-back, 1 PR body update, 2 report comments. REST was 403 for repo-scoped reads and `gh` is absent, both measured with a control before switching channels.",
"open_questions": [
{
"question": "Should the grid TELL the user that a dependsOn picker is scoped by the SAVED parent while an unsaved edit to that parent is staged in the same row? The PM's ruling explicitly left this live rather than settled, and asked that it not disappear into a code comment.",
"options": [
"A. Say nothing (what this PR ships). The picker silently uses the persisted parent. Zero new surface, but it is the same silent-wrongness class as the three refusals this seat landed in #7146 / #7147 / #7148.",
"B. Surface a hint on the trigger/popover when the parent has a staged-but-unsaved value that differs from the saved one — e.g. 'scoped by the saved Region; save the row to re-scope'. Needs the staged record, i.e. it is gated behind exactly the same seam change as option B of this card.",
"C. Do not build a message at all: land #7188, which removes the staleness and therefore removes the question."
],
"recommendation": "C, and treat B as dead. Real business need: the staleness only exists because the interim exists, so a message would be UI built to describe a defect we have already scheduled to delete. Long-term soundness: contract-first — the honest fix is the declared seam member, not a renderer-side apology for a wrong input, and a warning would fossilize the interim as if it were a supported mode. Making AI-authored metadata hard to get wrong: an author declaring dependsOn should get correct cascading, not a caption explaining why it is stale — a warning trains authors to accept a wrong scope. Startup scope discipline: option B costs the SAME seam change as #7188 while delivering strictly less, which is the worst trade of the three. ⚠️ The one thing that would flip this to B: if #7188 is declined or deferred long, the silent staleness stops being temporary and then it must be surfaced. So this question should be closed by #7188 landing, not by a ruling that silence is acceptable."
}
],
"out_of_scope_findings": [
"filed as #7188: option B — carry the pending/staged record across the renderCellEditor seam so a dependsOn lookup is scoped by the in-flight parent (the seam change is spelled out: @object-ui/types ctx member + Zod mirror + the #6882 exact-shape pin + data-table.tsx passing `{ ...row, ...rowChanges }` + ObjectGrid reading it). Clause 2-engaged; #7165 stays open for it.",
"filed as #7190 (label `finding`): plugin-detail's InlineFieldInput supplies no dependentValues either (measured: zero non-test occurrences; control: 88 files across packages/), and plugin-detail never provides SchemaRendererContext `data` itself. ⚠️ Deliberately NOT graded a bug — a detail page renders ONE record, so ctx.data may legitimately rescue the cascade there, and no rendering measurement was made. The single probe that settles it is written into the card.",
"NOT filed, reported here as an observation for PM triage: ObjectGrid.tsx still lists `renderCellEditor` in its `ObjectGridDataTableSchemaHolds` type as an undeclared-but-live HELD key, with prose saying whether DataTableSchema should declare it is a pending packages/types ruling. That ruling HAPPENED — #6882 declared it on 2026-08-30 — so the hold entry and its surrounding census comment are stale. Harmless today (the intersection with the declared member is identical), but it is documentation that now states the opposite of the tree. Left untouched because it is outside this card and inside the file #7166 is adjacent to."
]
}

Generated by Claude Code


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. The open question is ruled below.

⭐ The pre-flight scan that saved the card — and a trap nobody had written down

a first draft of the PR body contained the phrase "does NOT close #7165", which a pre-flight scan caught — GitHub's parser ignores negation and it would have closed the card on merge.

GitHub's closing-keyword parser does not read negation."does NOT close #7165" contains close #7165, and on merge it closes the card. This PR's entire correctness depends on #7165 staying open — A is an interim, B is the real fix — so the most natural sentence a careful author would write to protect the card is the one that would have destroyed it.

⚠️ And note the shape: the failure would have been silent and delayed. The PR merges, the card closes, and the interim becomes permanent because nothing is left tracking B. Carrying this to #6970 as a fourth mangling class, since that card enumerates exactly this family. The safe form is what shipped: Part of #7165, and never the keyword at all.

Measurement 1 answered decisively, on both disjuncts

B is Clause ②-engaged, and it was established rather than assumed:

⇒ B needs a seventh ctx member, so it edits @object-ui/types, its Zod mirror, and a maintainer-ruled exact-shape pin. That is Clause ② three times over. ⭐ The Equal-not-extends detail is what makes it airtight — an extends pin would have tolerated an added member, and the answer would have been different.

All three of my ruling conditions are met: the interim is labelled in code naming #7165 and #7188 and stating exactly what is stale; the card is not closed; B is filed.

A2.2 corrected, and the correction matters

I said A was "one line: dependentValues: ctx.row". Wrong: the declared contract always carried row, but ObjectGrid's local ctx annotation omitted it — so A was a two-part edit (widen the local annotation, then pass it). ⚠️ A declared member that a local annotation silently narrows is its own small hazard: the type says the data is there and the call site cannot see it.

The test discipline is the strongest part

  • Baseline before any edit: the existing bug(plugin-grid): ObjectGrid's inline lookup picker ignores four more spec-declared field props — multiple, allowCreate, lookupPageSize, dependsOn are read on its own path and never copied #7154 pin still passed on head — so the defect was live, not already fixed by something else. Establishing that first is what makes the rest meaningful.
  • The pin encoded the defect and was UPDATED IN PLACE, not deleted, with its header prose rewritten to say so. A deleted pin is indistinguishable from a pin that never existed.
  • A negative control: a row whose saved parent is empty still gates — proving the fix supplied a value rather than disabling the gate. That is the assertion that separates "cascade works" from "cascade removed", and it is the one most likely to be skipped.
  • The staleness pin carries its own proof it is not vacuous: the region cell renders the pending south while the saved record reads north, so the test proves the staging actually landed before asserting the picker ignored it. A staleness test without that proof passes for the wrong reason.
  • Ablation predicted before running, Tests 5 failed | 4 passed (9) exactly; mutation by marker count and blob hash; restore by state with git checkout HEAD -- ABSPATHnever the bare form, which restores from the index.

⚖️ Ruling on the open question: C — close it by landing #7188, not by ruling that silence is acceptable

I left this live at dispatch and asked that it not disappear into a code comment. The answer is C, and the argument that decides it is the one I did not have when I asked:

Option B costs the same seam change as #7188 while delivering strictly less. A warning that says "scoped by the saved Region" needs the staged record — i.e. exactly the ctx member #7188 adds. Having paid that price, you would ship a caption explaining the wrong scope instead of the right scope. That is the worst of the three trades.

And a warning would fossilise the interim as a supported mode. An author who declares dependsOn should get correct cascading, not a caption explaining why it is stale — a message trains authors to accept a wrong scope as normal.

⚠️I am adopting the flip condition verbatim, because it is what keeps C honest: if #7188 is declined, or deferred long enough that the interim stops being temporary, the silent staleness must be surfaced. So this question is closed by #7188 landing, not by a ruling that silence is fine. Recorded on #7165.

Incidence: carried, not closed — exactly as the dispatch required

Two corpora, each with a live control:

  • this repo: 3 files match dependsOn|depends_on, none is this defect (the gantt hit is depends_on as task predecessor links — a different key entirely); control: 88 files across packages/.
  • hotcrm: 6 dependsOn declarations on lookup fields across 5 objects, all scoping contacts to crm_account — but 0 editable grids in its views (control: the views dir exists and was read).

⇒ ⭐ "Neither measured corpus reaches the defect today, but hotcrm is ONE metadata key away"editable: true on any of those list views. That is a far more useful answer than either "no impact" or "unmeasured", and it is the sentence a maintainer needs to prioritise #7188. Customer apps remain unmeasured, and that is stated rather than glossed.

#7190 filed with the right restraint

InlineFieldInput supplies no dependentValues either (measured: zero non-test occurrences; control: 88 files) — and it is deliberately NOT graded a bug, because a detail page renders one record so ctx.data may legitimately rescue the cascade there, and no rendering measurement was made. The single probe that would settle it is written into the card. Declining to grade on a structural analogy, and leaving the probe behind, is better than a confident grade either way.


Nothing to change.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 10:48
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 84ffdbcSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7165-grid-dependent-values branch September 1, 2026 11:02
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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): feed the inline cell editor the row as dependent values, so a dependsOn lookup column is no longer permanently uneditable - #7193

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-7165-grid-dependent-values
Sep 1, 2026
Merged

fix(plugin-grid): feed the inline cell editor the row as dependent values, so a dependsOn lookup column is no longer permanently uneditable#7193
os-warren merged 3 commits into
mainfrom
claude/issue-7165-grid-dependent-values

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Part of #7165

#7165 must stay open. This ships option A, the interim, and is deliberately not a closing reference to that card — the card stays open for option B, filed as #7188. See "Measurement 1" below — that is the fact that determined this.

Measurement 1 — B is Clause ②-engaged, so A ships as the labelled interim

The PM ruling on #7165 made the A/B choice depend on one measurement: is renderCellEditor's editor context type published surface, or internal to plugin-grid? Measured on 899730e0a, both disjuncts of that test are true:

  • Reachable from @object-ui/typespackages/types/src/data-display.ts declares renderCellEditor?: (ctx: { column; row; value; stage; commit; cancel }) => React.ReactNode on DataTableSchema, with a Zod mirror in packages/types/src/zod/data-display.zod.ts.

  • Part of DataTableSchema's declared editor-context contract — declared by maintainer ruling 2026-08-30 ([Decision] Declare renderCellEditor and schema-level cellClassName on DataTableSchema? — the two live undeclared keys the #6459 census measured #6882), replacing the (schema as any) cast that preceded it. The shape is pinned by exact type equality, not extends, in packages/types/src/__tests__/data-table-declared-keys-6882.test.ts:

    type _RenderCellEditorShape = Expect[ Equal[
    Declared[DataTableSchema]['renderCellEditor'],
    ((ctx: CellEditorContext) => ReactNode) | undefined ] ];
    

    (angle brackets transcribed as square brackets — GitHub's body sanitizer eats
    tag-shaped fragments, including inside a fence. The source uses angle brackets.)

    with the stated reason: "Declaring the key with any other shape is a different (and false) statement about the renderer, so the shape is pinned, not just the membership."

packages/plugin-grid/src/ObjectGrid.tsx states the same fence in its own words: "⛔ Do not 'fix' either hold by declaring the key on DataTableSchema as a rider — that package is published surface with its own review floor."

⇒ B needs a seventh member on that declared context. That edits @object-ui/types, its Zod mirror, and a maintainer-ruled exact-shape pin. Per the ruling, A ships under all three of its conditions: labelled in code, this card not closed, and B filed (#7188).

⚠️A is not the conclusion. A field that can never be filled is a hard defect with no workaround; a field scoped by the persisted parent instead of the staged one is wrong only where the parent was edited in the same unsaved session. Strictly better, strictly not finished.

The defect

LookupField reads the record it gates on as dependentValues ?? ctx.formValues ?? ctx.data ?? {}. The grid's inline cell editor supplied none of the three — renderCellEditor rendered FieldEditWidget with field / value / onChange only, SchemaRendererContext has no formValues, and the grid sets no ctx.data per row. The record was therefore {} for every row, dependenciesMissing was permanently true, and a column declaring dependsOn rendered a disabled trigger reading "Select region first" even when the row carried the parent value.

PR #2216 was the two-half answer to #2215. Half 2 — every picker taking the dependsOn chain as a hard baseFilter — is host-independent and was already live on the grid path, which is why the gate fired at all (verified: A2.5 holds, see LookupField's dependentFilter memo). Half 1 is per-host and the grid never got it. This PR supplies that missing input and re-implements no cascade.

The change

One line of behaviour in ObjectGrid.renderCellEditordependentValues={ctx.row} — plus row: any added to the local ctx annotation, which had omitted it (the declared contract always carried row; only this local annotation did not). FieldEditWidget already forwards dependentValues to the widget, since #7008.

The interim is labelled in the code, naming this card and #7188, and stating precisely what is stale: a parent edited but not yet saved in the same row does not re-scope the child.

Evidence

Rendering proof, packages/plugin-grid/src/__tests__/gridDependentValues-7165.test.tsx (4 tests, each with a live control in the same render):

  1. the dependsOn column opens (was lookup-trigger-gated + disabled; now lookup-trigger-regional_owner + enabled) — control: a no-dependsOn column in the same render is also enabled, so the picker path is proven reached.
  2. the picker is scoped by the row: region: 'north' reaches the query as a hard $filter and Person 07 (south) is absent — control: the no-dependsOn sibling over the same records does offer Person 07, so the absence is scoping and not a dead picker.
  3. negative control: a row whose saved parent is empty still gates, so the gate was not simply disabled — control: the filled row in the same render opens.
  4. ⚠️the interim's staleness, pinned as current behaviour: staging a new parent without saving does not re-scope the child — with its own proof that the staging actually landed (the region cell renders the pending 'south' while the saved record still reads 'north'), because without that the assertion would pass for the trivial reason that nothing was ever staged. The grid's inline cell editor scopes a dependsOn lookup by the SAVED row, not the staged one — carry the pending record across the renderCellEditor seam (option B of #7165) #7188 flips this test.

The #7154 pin was UPDATED, not deleted. The dependsOn case in lookupPickerKeys-7154.test.tsx pinned the gated behaviour, i.e. it encoded the defect. A deleted pin is indistinguishable from a pin that never existed, so it is rewritten in place to assert the fixed behaviour plus the scoping, and its header prose is rewritten to say so.

Ablation — direction predicted before running: removing the single dependentValues={ctx.row} line turns red the 4 new tests plus the updated #7154 case, and leaves the other 4 #7154 cases green.

  • No rebuild required, and this is measured rather than assumed: the root vitest.config.mts aliases every @object-ui/* specifier to that package's src, and the tests import ../ObjectGrid relatively — so nothing resolves through dist and a source mutation is directly live.
  • Mutation confirmed on disk by marker count (dependentValues={ctx.row}: 1 → 0) and blob hash (e8ab332e…f219c865…), not by the editor's exit code.
  • Result: Tests 5 failed | 4 passed (9) — exactly the predicted set, passing count included.
  • Restore proven by state, with HEAD named explicitly (git checkout HEAD -- ..., never the bare form, which restores from the index): git diff HEAD, git diff --cached and git status --short all empty, and the blob hash back to e8ab332e…. The script carried a trap ... EXIT INT TERM with absolute paths.

Gates

⚠️Two heads, stated separately so the provenance is checkable. The full set ran at f17b08047; main then advanced (#7180 landed), so it was merged in and the affected-package suite plus the cheap ratchet gates were re-run at the current head aba0c7499. Nothing below is quoted from a tree that is no longer this branch's head.

Re-run at aba0c7499 (current head, post-merge):

gateresult
packages/plugin-grid/ full suite105 files, 949 tests passed
check:control-bytesOK — 5958 tracked text files scanned
check:phantom-depsOK
check:changeset-no-majorOK

Run at f17b08047 (same tree for every path this PR touches — the merge brought in only packages/components/src/renderers/layout/containers.tsx, a .changeset, a docs page, a components test and one vitest.config.mts line, none of which this PR edits):

gateresult
type-check (plugin-grid + closure)14 tasks successful
lint (affected package, full, not narrowed)2 tasks, 0 errors (751 pre-existing warnings)
check:self-importOK
check:vi-mock-specifiers / check:vi-mock-inheritOK (4106 files, 525 with mocks)
check-governed-queue-guard --testNOT GOVERNED — 4 paths vs 5 surfaces, none matched

The typecheck genuinely covers the new tests: tsc -p tsconfig.test.json --listFiles lists all three edited files among its 1770 (this package typechecks tests via a dedicated project, so "typecheck green" is not silently excluding them).

⚠️#7166 — the concurrent lane in this same package — had not landed as of aba0c7499. It touches relationalMetaKeys.ts and the applyRelationalMeta copy set; this PR touches ObjectGrid.renderCellEditor's context construction. Disjoint symbols, but this branch will need a fresh merge if #7166 lands first.

Incidence — measured, and ⛔ not "no impact"

Two corpora, each instrument run with a live control:

  • this repo: 3 files match dependsOn|depends_on, and none is this defect — the gantt hit is depends_on as task predecessor links (a different key entirely) and the other is a select inside a form. Control: the same instrument returns 88 files across packages/.
  • hotcrm (a real shipped app, anonymous shallow clone): 6 dependsOn declarations on lookup fields across 5 objects (crm_contract ×2, crm_case, crm_quote ×2, crm_opportunity), all scoping contacts to crm_account. But 0 editable grids in its views (control: the views directory exists and was read).

⇒ Neither measured corpus reaches the defect today, because none combines dependsOn with an editable grid — but hotcrm is one metadata key away: editable: true on any of those five objects' list views hits it immediately. Customer apps are unmeasured, and the parent pattern is demonstrably real in shipped metadata. This is a bounded reading, not an all-clear.

Filed, not ridden

⚠️ Still live, per the PM's fence and not settled by this PR: should the user be told the scope came from the saved row? The interim's staleness is the same silent-wrongness class as the three refusals this seat landed in #7146 / #7147 / #7148. It is recorded on #7188 rather than left to disappear into a code comment.


Session: session_012wwHa4aaFybxXrfmfHioDM (https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM) — kept in prose because a body edit rewrites the footer below to its bare form.

Generated by Claude Code

…lues
A `dependsOn` lookup column in an editable ObjectGrid was permanently
uneditable. `LookupField` resolves
`dependentValues ?? ctx.formValues ?? ctx.data ?? {}` and the grid's
`renderCellEditor` supplied none of the three, so the resolved record was
`{}` for every row, `dependenciesMissing` stayed `true`, and the trigger
rendered disabled ("Select region first") even when the row carried the
parent value.
PR #2216closed#2215 in two halves: the form renderer injects its live
watched record, and every picker takes the `dependsOn` chain as a hard
`baseFilter`. Half 2 is host-independent and was already live here; half 1
is per-host and the grid never got it. This supplies that missing input and
re-implements no cascade.
INTERIM: `ctx.row` is the saved record, so a parent edited but not yet
saved in the same row does not re-scope the child. Carrying the staged
record needs a seventh member on `renderCellEditor`'s published context
type (declared in `@object-ui/types`, #6882, and pinned by exact type
equality), which is a contract change tracked separately.
The `dependsOn` case in `lookupPickerKeys-7154.test.tsx` pinned the gated
behaviour and is UPDATED, not deleted.
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.6 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-C6RhwKZM.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)514.13KB117.19KB
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)206.14KB55.77KB
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

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.6 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-C6RhwKZM.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)514.13KB117.19KB
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)206.14KB55.77KB
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": 7165,
"status": "done",
"branch": "claude/issue-7165-grid-dependent-values",
"pr": "https://github.com/objectstack-ai/objectui/pull/7193",
"premise_still_valid": true,
"summary": "MEASUREMENT 1: B IS Clause 2-engaged — so option A shipped as the labelled interim and ⛔ #7165 MUST STAY OPEN (the PR says 'Part of #7165', never a closing keyword). The editor context is published surface on both disjuncts of the test: `DataTableSchema.renderCellEditor` is declared in packages/types/src/data-display.ts with a Zod mirror, declared by maintainer ruling 2026-08-30 (#6882) replacing a `(schema as any)` cast, and its exact shape is pinned by an `Equal` (not `extends`) type assertion in packages/types/src/__tests__/data-table-declared-keys-6882.test.ts whose comment states the shape is pinned deliberately; ObjectGrid.tsx independently fences that package as 'published surface with its own review floor'. B needs a seventh ctx member, so it edits @object-ui/types, its Zod mirror and that maintainer-ruled pin. All three ruling conditions are met: the interim is labelled in code naming #7165 and the follow-up and stating exactly what is stale, the card is not closed, and B is filed as #7188. Premise re-measured on head 899730e0a: the existing pin still passed before my change, i.e. the defect was still live. A2.1/A2.3/A2.4/A2.5 all confirmed; A2.2 needed one CORRECTION — the declared contract always carried `row`, but ObjectGrid's LOCAL ctx annotation omitted it, so A was a two-part edit (widen the local annotation, then pass it), not a literal one-liner.",
"tests": "All runs from repo root via the shared verify lock (objectui forbids `pnpm --filter PKG test`; path filters only). BASELINE on 899730e0a before any edit: `pnpm exec vitest run packages/plugin-grid/src/__tests__/lookupPickerKeys-7154.test.tsx` -> 'Test Files 1 passed (1) / Tests 5 passed (5)' — the gated pin still passed, so the defect was live on head. AFTER FIX, same command -> 'Tests 1 failed | 4 passed (5)', 'AssertionError: expected lookup-trigger-regional_owner to be lookup-trigger-gated' — the pin encoded the defect, so it was UPDATED IN PLACE (not deleted), header prose rewritten to say so. NEW FILE packages/plugin-grid/src/__tests__/gridDependentValues-7165.test.tsx, 4 tests each with a live control in the same render: (1) dependsOn column now opens, control column also opens; (2) picker SCOPED — $filter.region='north' reaches the query, Person 07 (south) absent, while the no-dependsOn control over the same records DOES offer Person 07; (3) NEGATIVE CONTROL — a row whose saved parent is empty still gates, proving the gate was not disabled; (4) the interim's staleness pinned as current behaviour, with its own proof the staging landed (region cell renders pending 'south' while the saved record reads 'north'), which is what stops it passing vacuously. Both files together: 'Test Files 2 passed (2) / Tests 9 passed (9)'. ABLATION — direction predicted BEFORE running (remove `dependentValues={ctx.row}` -> the 4 new tests plus the updated 7154 case go red, the other 4 stay green). NO REBUILD NEEDED, and that is measured not assumed: root vitest.config.mts aliases every @object-ui/* specifier to that package's src and the tests import ../ObjectGrid relatively, so nothing resolves through dist and a source mutation is directly live. Mutation confirmed ON DISK by marker count (1 -> 0) AND blob hash (e8ab332e… -> f219c865…), never by the editing tool's exit code. Result: 'Tests 5 failed | 4 passed (9)' — exactly the predicted set, passing count included. Restore proven BY STATE with HEAD named explicitly (`git checkout HEAD -- ABSPATH`, never the bare form which restores from the index): git diff HEAD, git diff --cached and git status --short all empty and the blob hash back to e8ab332e…; the script carried `trap ... EXIT INT TERM` with absolute paths. GATES: at final head aba0c7499 (after merging main, which had advanced to 8ec11e14f / #7180) — full packages/plugin-grid/ suite 'Test Files 105 passed (105) / Tests 949 passed (949)', check:control-bytes OK (5958 files), check:phantom-deps OK, check:changeset-no-major OK. At f17b08047 (identical tree for every path this PR touches): type-check plugin-grid + closure 14 tasks successful, lint of the affected package in FULL (not narrowed) 2 tasks 0 errors (751 pre-existing warnings), check:self-import OK, check:vi-mock-specifiers / check:vi-mock-inherit OK (4106 files, 525 with mocks), check-governed-queue-guard --test = NOT GOVERNED (4 paths vs 5 surfaces). Verdicts read from each gate's own printed line, with exit codes captured after redirection, never through a pipe. NOT-MEASURED check cleared: `tsc -p tsconfig.test.json --listFiles` lists all three edited files among its 1770, so the green typecheck genuinely covers the new tests rather than excluding them.",
"mcp_calls": "11 — 2 issue reads, 2 targeted search_issues (each with a control word that hit: #7165 itself, then #7154/#2215), 2 issue creates, 1 PR create, 1 PR body read-back, 1 PR body update, 2 report comments. REST was 403 for repo-scoped reads and `gh` is absent, both measured with a control before switching channels.",
"open_questions": [
{
"question": "Should the grid TELL the user that a dependsOn picker is scoped by the SAVED parent while an unsaved edit to that parent is staged in the same row? The PM's ruling explicitly left this live rather than settled, and asked that it not disappear into a code comment.",
"options": [
"A. Say nothing (what this PR ships). The picker silently uses the persisted parent. Zero new surface, but it is the same silent-wrongness class as the three refusals this seat landed in #7146 / #7147 / #7148.",
"B. Surface a hint on the trigger/popover when the parent has a staged-but-unsaved value that differs from the saved one — e.g. 'scoped by the saved Region; save the row to re-scope'. Needs the staged record, i.e. it is gated behind exactly the same seam change as option B of this card.",
"C. Do not build a message at all: land #7188, which removes the staleness and therefore removes the question."
],
"recommendation": "C, and treat B as dead. Real business need: the staleness only exists because the interim exists, so a message would be UI built to describe a defect we have already scheduled to delete. Long-term soundness: contract-first — the honest fix is the declared seam member, not a renderer-side apology for a wrong input, and a warning would fossilize the interim as if it were a supported mode. Making AI-authored metadata hard to get wrong: an author declaring dependsOn should get correct cascading, not a caption explaining why it is stale — a warning trains authors to accept a wrong scope. Startup scope discipline: option B costs the SAME seam change as #7188 while delivering strictly less, which is the worst trade of the three. ⚠️ The one thing that would flip this to B: if #7188 is declined or deferred long, the silent staleness stops being temporary and then it must be surfaced. So this question should be closed by #7188 landing, not by a ruling that silence is acceptable."
}
],
"out_of_scope_findings": [
"filed as #7188: option B — carry the pending/staged record across the renderCellEditor seam so a dependsOn lookup is scoped by the in-flight parent (the seam change is spelled out: @object-ui/types ctx member + Zod mirror + the #6882 exact-shape pin + data-table.tsx passing `{ ...row, ...rowChanges }` + ObjectGrid reading it). Clause 2-engaged; #7165 stays open for it.",
"filed as #7190 (label `finding`): plugin-detail's InlineFieldInput supplies no dependentValues either (measured: zero non-test occurrences; control: 88 files across packages/), and plugin-detail never provides SchemaRendererContext `data` itself. ⚠️ Deliberately NOT graded a bug — a detail page renders ONE record, so ctx.data may legitimately rescue the cascade there, and no rendering measurement was made. The single probe that settles it is written into the card.",
"NOT filed, reported here as an observation for PM triage: ObjectGrid.tsx still lists `renderCellEditor` in its `ObjectGridDataTableSchemaHolds` type as an undeclared-but-live HELD key, with prose saying whether DataTableSchema should declare it is a pending packages/types ruling. That ruling HAPPENED — #6882 declared it on 2026-08-30 — so the hold entry and its surrounding census comment are stale. Harmless today (the intersection with the declared member is identical), but it is documentation that now states the opposite of the tree. Left untouched because it is outside this card and inside the file #7166 is adjacent to."
]
}

Generated by Claude Code


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. The open question is ruled below.

⭐ The pre-flight scan that saved the card — and a trap nobody had written down

a first draft of the PR body contained the phrase "does NOT close #7165", which a pre-flight scan caught — GitHub's parser ignores negation and it would have closed the card on merge.

GitHub's closing-keyword parser does not read negation."does NOT close #7165" contains close #7165, and on merge it closes the card. This PR's entire correctness depends on #7165 staying open — A is an interim, B is the real fix — so the most natural sentence a careful author would write to protect the card is the one that would have destroyed it.

⚠️ And note the shape: the failure would have been silent and delayed. The PR merges, the card closes, and the interim becomes permanent because nothing is left tracking B. Carrying this to #6970 as a fourth mangling class, since that card enumerates exactly this family. The safe form is what shipped: Part of #7165, and never the keyword at all.

Measurement 1 answered decisively, on both disjuncts

B is Clause ②-engaged, and it was established rather than assumed:

⇒ B needs a seventh ctx member, so it edits @object-ui/types, its Zod mirror, and a maintainer-ruled exact-shape pin. That is Clause ② three times over. ⭐ The Equal-not-extends detail is what makes it airtight — an extends pin would have tolerated an added member, and the answer would have been different.

All three of my ruling conditions are met: the interim is labelled in code naming #7165 and #7188 and stating exactly what is stale; the card is not closed; B is filed.

A2.2 corrected, and the correction matters

I said A was "one line: dependentValues: ctx.row". Wrong: the declared contract always carried row, but ObjectGrid's local ctx annotation omitted it — so A was a two-part edit (widen the local annotation, then pass it). ⚠️ A declared member that a local annotation silently narrows is its own small hazard: the type says the data is there and the call site cannot see it.

The test discipline is the strongest part

  • Baseline before any edit: the existing bug(plugin-grid): ObjectGrid's inline lookup picker ignores four more spec-declared field props — multiple, allowCreate, lookupPageSize, dependsOn are read on its own path and never copied #7154 pin still passed on head — so the defect was live, not already fixed by something else. Establishing that first is what makes the rest meaningful.
  • The pin encoded the defect and was UPDATED IN PLACE, not deleted, with its header prose rewritten to say so. A deleted pin is indistinguishable from a pin that never existed.
  • A negative control: a row whose saved parent is empty still gates — proving the fix supplied a value rather than disabling the gate. That is the assertion that separates "cascade works" from "cascade removed", and it is the one most likely to be skipped.
  • The staleness pin carries its own proof it is not vacuous: the region cell renders the pending south while the saved record reads north, so the test proves the staging actually landed before asserting the picker ignored it. A staleness test without that proof passes for the wrong reason.
  • Ablation predicted before running, Tests 5 failed | 4 passed (9) exactly; mutation by marker count and blob hash; restore by state with git checkout HEAD -- ABSPATHnever the bare form, which restores from the index.

⚖️ Ruling on the open question: C — close it by landing #7188, not by ruling that silence is acceptable

I left this live at dispatch and asked that it not disappear into a code comment. The answer is C, and the argument that decides it is the one I did not have when I asked:

Option B costs the same seam change as #7188 while delivering strictly less. A warning that says "scoped by the saved Region" needs the staged record — i.e. exactly the ctx member #7188 adds. Having paid that price, you would ship a caption explaining the wrong scope instead of the right scope. That is the worst of the three trades.

And a warning would fossilise the interim as a supported mode. An author who declares dependsOn should get correct cascading, not a caption explaining why it is stale — a message trains authors to accept a wrong scope as normal.

⚠️I am adopting the flip condition verbatim, because it is what keeps C honest: if #7188 is declined, or deferred long enough that the interim stops being temporary, the silent staleness must be surfaced. So this question is closed by #7188 landing, not by a ruling that silence is fine. Recorded on #7165.

Incidence: carried, not closed — exactly as the dispatch required

Two corpora, each with a live control:

  • this repo: 3 files match dependsOn|depends_on, none is this defect (the gantt hit is depends_on as task predecessor links — a different key entirely); control: 88 files across packages/.
  • hotcrm: 6 dependsOn declarations on lookup fields across 5 objects, all scoping contacts to crm_account — but 0 editable grids in its views (control: the views dir exists and was read).

⇒ ⭐ "Neither measured corpus reaches the defect today, but hotcrm is ONE metadata key away"editable: true on any of those list views. That is a far more useful answer than either "no impact" or "unmeasured", and it is the sentence a maintainer needs to prioritise #7188. Customer apps remain unmeasured, and that is stated rather than glossed.

#7190 filed with the right restraint

InlineFieldInput supplies no dependentValues either (measured: zero non-test occurrences; control: 88 files) — and it is deliberately NOT graded a bug, because a detail page renders one record so ctx.data may legitimately rescue the cascade there, and no rendering measurement was made. The single probe that would settle it is written into the card. Declining to grade on a structural analogy, and leaving the probe behind, is better than a confident grade either way.


Nothing to change.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 10:48
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 84ffdbcSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7165-grid-dependent-values branch September 1, 2026 11:02
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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): feed the inline cell editor the row as dependent values, so a dependsOn lookup column is no longer permanently uneditable - #7193

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-7165-grid-dependent-values
Sep 1, 2026
Merged

fix(plugin-grid): feed the inline cell editor the row as dependent values, so a dependsOn lookup column is no longer permanently uneditable#7193
os-warren merged 3 commits into
mainfrom
claude/issue-7165-grid-dependent-values

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Part of #7165

#7165 must stay open. This ships option A, the interim, and is deliberately not a closing reference to that card — the card stays open for option B, filed as #7188. See "Measurement 1" below — that is the fact that determined this.

Measurement 1 — B is Clause ②-engaged, so A ships as the labelled interim

The PM ruling on #7165 made the A/B choice depend on one measurement: is renderCellEditor's editor context type published surface, or internal to plugin-grid? Measured on 899730e0a, both disjuncts of that test are true:

  • Reachable from @object-ui/typespackages/types/src/data-display.ts declares renderCellEditor?: (ctx: { column; row; value; stage; commit; cancel }) => React.ReactNode on DataTableSchema, with a Zod mirror in packages/types/src/zod/data-display.zod.ts.

  • Part of DataTableSchema's declared editor-context contract — declared by maintainer ruling 2026-08-30 ([Decision] Declare renderCellEditor and schema-level cellClassName on DataTableSchema? — the two live undeclared keys the #6459 census measured #6882), replacing the (schema as any) cast that preceded it. The shape is pinned by exact type equality, not extends, in packages/types/src/__tests__/data-table-declared-keys-6882.test.ts:

    type _RenderCellEditorShape = Expect[ Equal[
    Declared[DataTableSchema]['renderCellEditor'],
    ((ctx: CellEditorContext) => ReactNode) | undefined ] ];
    

    (angle brackets transcribed as square brackets — GitHub's body sanitizer eats
    tag-shaped fragments, including inside a fence. The source uses angle brackets.)

    with the stated reason: "Declaring the key with any other shape is a different (and false) statement about the renderer, so the shape is pinned, not just the membership."

packages/plugin-grid/src/ObjectGrid.tsx states the same fence in its own words: "⛔ Do not 'fix' either hold by declaring the key on DataTableSchema as a rider — that package is published surface with its own review floor."

⇒ B needs a seventh member on that declared context. That edits @object-ui/types, its Zod mirror, and a maintainer-ruled exact-shape pin. Per the ruling, A ships under all three of its conditions: labelled in code, this card not closed, and B filed (#7188).

⚠️A is not the conclusion. A field that can never be filled is a hard defect with no workaround; a field scoped by the persisted parent instead of the staged one is wrong only where the parent was edited in the same unsaved session. Strictly better, strictly not finished.

The defect

LookupField reads the record it gates on as dependentValues ?? ctx.formValues ?? ctx.data ?? {}. The grid's inline cell editor supplied none of the three — renderCellEditor rendered FieldEditWidget with field / value / onChange only, SchemaRendererContext has no formValues, and the grid sets no ctx.data per row. The record was therefore {} for every row, dependenciesMissing was permanently true, and a column declaring dependsOn rendered a disabled trigger reading "Select region first" even when the row carried the parent value.

PR #2216 was the two-half answer to #2215. Half 2 — every picker taking the dependsOn chain as a hard baseFilter — is host-independent and was already live on the grid path, which is why the gate fired at all (verified: A2.5 holds, see LookupField's dependentFilter memo). Half 1 is per-host and the grid never got it. This PR supplies that missing input and re-implements no cascade.

The change

One line of behaviour in ObjectGrid.renderCellEditordependentValues={ctx.row} — plus row: any added to the local ctx annotation, which had omitted it (the declared contract always carried row; only this local annotation did not). FieldEditWidget already forwards dependentValues to the widget, since #7008.

The interim is labelled in the code, naming this card and #7188, and stating precisely what is stale: a parent edited but not yet saved in the same row does not re-scope the child.

Evidence

Rendering proof, packages/plugin-grid/src/__tests__/gridDependentValues-7165.test.tsx (4 tests, each with a live control in the same render):

  1. the dependsOn column opens (was lookup-trigger-gated + disabled; now lookup-trigger-regional_owner + enabled) — control: a no-dependsOn column in the same render is also enabled, so the picker path is proven reached.
  2. the picker is scoped by the row: region: 'north' reaches the query as a hard $filter and Person 07 (south) is absent — control: the no-dependsOn sibling over the same records does offer Person 07, so the absence is scoping and not a dead picker.
  3. negative control: a row whose saved parent is empty still gates, so the gate was not simply disabled — control: the filled row in the same render opens.
  4. ⚠️the interim's staleness, pinned as current behaviour: staging a new parent without saving does not re-scope the child — with its own proof that the staging actually landed (the region cell renders the pending 'south' while the saved record still reads 'north'), because without that the assertion would pass for the trivial reason that nothing was ever staged. The grid's inline cell editor scopes a dependsOn lookup by the SAVED row, not the staged one — carry the pending record across the renderCellEditor seam (option B of #7165) #7188 flips this test.

The #7154 pin was UPDATED, not deleted. The dependsOn case in lookupPickerKeys-7154.test.tsx pinned the gated behaviour, i.e. it encoded the defect. A deleted pin is indistinguishable from a pin that never existed, so it is rewritten in place to assert the fixed behaviour plus the scoping, and its header prose is rewritten to say so.

Ablation — direction predicted before running: removing the single dependentValues={ctx.row} line turns red the 4 new tests plus the updated #7154 case, and leaves the other 4 #7154 cases green.

  • No rebuild required, and this is measured rather than assumed: the root vitest.config.mts aliases every @object-ui/* specifier to that package's src, and the tests import ../ObjectGrid relatively — so nothing resolves through dist and a source mutation is directly live.
  • Mutation confirmed on disk by marker count (dependentValues={ctx.row}: 1 → 0) and blob hash (e8ab332e…f219c865…), not by the editor's exit code.
  • Result: Tests 5 failed | 4 passed (9) — exactly the predicted set, passing count included.
  • Restore proven by state, with HEAD named explicitly (git checkout HEAD -- ..., never the bare form, which restores from the index): git diff HEAD, git diff --cached and git status --short all empty, and the blob hash back to e8ab332e…. The script carried a trap ... EXIT INT TERM with absolute paths.

Gates

⚠️Two heads, stated separately so the provenance is checkable. The full set ran at f17b08047; main then advanced (#7180 landed), so it was merged in and the affected-package suite plus the cheap ratchet gates were re-run at the current head aba0c7499. Nothing below is quoted from a tree that is no longer this branch's head.

Re-run at aba0c7499 (current head, post-merge):

gateresult
packages/plugin-grid/ full suite105 files, 949 tests passed
check:control-bytesOK — 5958 tracked text files scanned
check:phantom-depsOK
check:changeset-no-majorOK

Run at f17b08047 (same tree for every path this PR touches — the merge brought in only packages/components/src/renderers/layout/containers.tsx, a .changeset, a docs page, a components test and one vitest.config.mts line, none of which this PR edits):

gateresult
type-check (plugin-grid + closure)14 tasks successful
lint (affected package, full, not narrowed)2 tasks, 0 errors (751 pre-existing warnings)
check:self-importOK
check:vi-mock-specifiers / check:vi-mock-inheritOK (4106 files, 525 with mocks)
check-governed-queue-guard --testNOT GOVERNED — 4 paths vs 5 surfaces, none matched

The typecheck genuinely covers the new tests: tsc -p tsconfig.test.json --listFiles lists all three edited files among its 1770 (this package typechecks tests via a dedicated project, so "typecheck green" is not silently excluding them).

⚠️#7166 — the concurrent lane in this same package — had not landed as of aba0c7499. It touches relationalMetaKeys.ts and the applyRelationalMeta copy set; this PR touches ObjectGrid.renderCellEditor's context construction. Disjoint symbols, but this branch will need a fresh merge if #7166 lands first.

Incidence — measured, and ⛔ not "no impact"

Two corpora, each instrument run with a live control:

  • this repo: 3 files match dependsOn|depends_on, and none is this defect — the gantt hit is depends_on as task predecessor links (a different key entirely) and the other is a select inside a form. Control: the same instrument returns 88 files across packages/.
  • hotcrm (a real shipped app, anonymous shallow clone): 6 dependsOn declarations on lookup fields across 5 objects (crm_contract ×2, crm_case, crm_quote ×2, crm_opportunity), all scoping contacts to crm_account. But 0 editable grids in its views (control: the views directory exists and was read).

⇒ Neither measured corpus reaches the defect today, because none combines dependsOn with an editable grid — but hotcrm is one metadata key away: editable: true on any of those five objects' list views hits it immediately. Customer apps are unmeasured, and the parent pattern is demonstrably real in shipped metadata. This is a bounded reading, not an all-clear.

Filed, not ridden

⚠️ Still live, per the PM's fence and not settled by this PR: should the user be told the scope came from the saved row? The interim's staleness is the same silent-wrongness class as the three refusals this seat landed in #7146 / #7147 / #7148. It is recorded on #7188 rather than left to disappear into a code comment.


Session: session_012wwHa4aaFybxXrfmfHioDM (https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM) — kept in prose because a body edit rewrites the footer below to its bare form.

Generated by Claude Code

…lues
A `dependsOn` lookup column in an editable ObjectGrid was permanently
uneditable. `LookupField` resolves
`dependentValues ?? ctx.formValues ?? ctx.data ?? {}` and the grid's
`renderCellEditor` supplied none of the three, so the resolved record was
`{}` for every row, `dependenciesMissing` stayed `true`, and the trigger
rendered disabled ("Select region first") even when the row carried the
parent value.
PR #2216closed#2215 in two halves: the form renderer injects its live
watched record, and every picker takes the `dependsOn` chain as a hard
`baseFilter`. Half 2 is host-independent and was already live here; half 1
is per-host and the grid never got it. This supplies that missing input and
re-implements no cascade.
INTERIM: `ctx.row` is the saved record, so a parent edited but not yet
saved in the same row does not re-scope the child. Carrying the staged
record needs a seventh member on `renderCellEditor`'s published context
type (declared in `@object-ui/types`, #6882, and pinned by exact type
equality), which is a contract change tracked separately.
The `dependsOn` case in `lookupPickerKeys-7154.test.tsx` pinned the gated
behaviour and is UPDATED, not deleted.
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.6 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-C6RhwKZM.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)514.13KB117.19KB
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)206.14KB55.77KB
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

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.6 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-C6RhwKZM.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)514.13KB117.19KB
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)206.14KB55.77KB
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": 7165,
"status": "done",
"branch": "claude/issue-7165-grid-dependent-values",
"pr": "https://github.com/objectstack-ai/objectui/pull/7193",
"premise_still_valid": true,
"summary": "MEASUREMENT 1: B IS Clause 2-engaged — so option A shipped as the labelled interim and ⛔ #7165 MUST STAY OPEN (the PR says 'Part of #7165', never a closing keyword). The editor context is published surface on both disjuncts of the test: `DataTableSchema.renderCellEditor` is declared in packages/types/src/data-display.ts with a Zod mirror, declared by maintainer ruling 2026-08-30 (#6882) replacing a `(schema as any)` cast, and its exact shape is pinned by an `Equal` (not `extends`) type assertion in packages/types/src/__tests__/data-table-declared-keys-6882.test.ts whose comment states the shape is pinned deliberately; ObjectGrid.tsx independently fences that package as 'published surface with its own review floor'. B needs a seventh ctx member, so it edits @object-ui/types, its Zod mirror and that maintainer-ruled pin. All three ruling conditions are met: the interim is labelled in code naming #7165 and the follow-up and stating exactly what is stale, the card is not closed, and B is filed as #7188. Premise re-measured on head 899730e0a: the existing pin still passed before my change, i.e. the defect was still live. A2.1/A2.3/A2.4/A2.5 all confirmed; A2.2 needed one CORRECTION — the declared contract always carried `row`, but ObjectGrid's LOCAL ctx annotation omitted it, so A was a two-part edit (widen the local annotation, then pass it), not a literal one-liner.",
"tests": "All runs from repo root via the shared verify lock (objectui forbids `pnpm --filter PKG test`; path filters only). BASELINE on 899730e0a before any edit: `pnpm exec vitest run packages/plugin-grid/src/__tests__/lookupPickerKeys-7154.test.tsx` -> 'Test Files 1 passed (1) / Tests 5 passed (5)' — the gated pin still passed, so the defect was live on head. AFTER FIX, same command -> 'Tests 1 failed | 4 passed (5)', 'AssertionError: expected lookup-trigger-regional_owner to be lookup-trigger-gated' — the pin encoded the defect, so it was UPDATED IN PLACE (not deleted), header prose rewritten to say so. NEW FILE packages/plugin-grid/src/__tests__/gridDependentValues-7165.test.tsx, 4 tests each with a live control in the same render: (1) dependsOn column now opens, control column also opens; (2) picker SCOPED — $filter.region='north' reaches the query, Person 07 (south) absent, while the no-dependsOn control over the same records DOES offer Person 07; (3) NEGATIVE CONTROL — a row whose saved parent is empty still gates, proving the gate was not disabled; (4) the interim's staleness pinned as current behaviour, with its own proof the staging landed (region cell renders pending 'south' while the saved record reads 'north'), which is what stops it passing vacuously. Both files together: 'Test Files 2 passed (2) / Tests 9 passed (9)'. ABLATION — direction predicted BEFORE running (remove `dependentValues={ctx.row}` -> the 4 new tests plus the updated 7154 case go red, the other 4 stay green). NO REBUILD NEEDED, and that is measured not assumed: root vitest.config.mts aliases every @object-ui/* specifier to that package's src and the tests import ../ObjectGrid relatively, so nothing resolves through dist and a source mutation is directly live. Mutation confirmed ON DISK by marker count (1 -> 0) AND blob hash (e8ab332e… -> f219c865…), never by the editing tool's exit code. Result: 'Tests 5 failed | 4 passed (9)' — exactly the predicted set, passing count included. Restore proven BY STATE with HEAD named explicitly (`git checkout HEAD -- ABSPATH`, never the bare form which restores from the index): git diff HEAD, git diff --cached and git status --short all empty and the blob hash back to e8ab332e…; the script carried `trap ... EXIT INT TERM` with absolute paths. GATES: at final head aba0c7499 (after merging main, which had advanced to 8ec11e14f / #7180) — full packages/plugin-grid/ suite 'Test Files 105 passed (105) / Tests 949 passed (949)', check:control-bytes OK (5958 files), check:phantom-deps OK, check:changeset-no-major OK. At f17b08047 (identical tree for every path this PR touches): type-check plugin-grid + closure 14 tasks successful, lint of the affected package in FULL (not narrowed) 2 tasks 0 errors (751 pre-existing warnings), check:self-import OK, check:vi-mock-specifiers / check:vi-mock-inherit OK (4106 files, 525 with mocks), check-governed-queue-guard --test = NOT GOVERNED (4 paths vs 5 surfaces). Verdicts read from each gate's own printed line, with exit codes captured after redirection, never through a pipe. NOT-MEASURED check cleared: `tsc -p tsconfig.test.json --listFiles` lists all three edited files among its 1770, so the green typecheck genuinely covers the new tests rather than excluding them.",
"mcp_calls": "11 — 2 issue reads, 2 targeted search_issues (each with a control word that hit: #7165 itself, then #7154/#2215), 2 issue creates, 1 PR create, 1 PR body read-back, 1 PR body update, 2 report comments. REST was 403 for repo-scoped reads and `gh` is absent, both measured with a control before switching channels.",
"open_questions": [
{
"question": "Should the grid TELL the user that a dependsOn picker is scoped by the SAVED parent while an unsaved edit to that parent is staged in the same row? The PM's ruling explicitly left this live rather than settled, and asked that it not disappear into a code comment.",
"options": [
"A. Say nothing (what this PR ships). The picker silently uses the persisted parent. Zero new surface, but it is the same silent-wrongness class as the three refusals this seat landed in #7146 / #7147 / #7148.",
"B. Surface a hint on the trigger/popover when the parent has a staged-but-unsaved value that differs from the saved one — e.g. 'scoped by the saved Region; save the row to re-scope'. Needs the staged record, i.e. it is gated behind exactly the same seam change as option B of this card.",
"C. Do not build a message at all: land #7188, which removes the staleness and therefore removes the question."
],
"recommendation": "C, and treat B as dead. Real business need: the staleness only exists because the interim exists, so a message would be UI built to describe a defect we have already scheduled to delete. Long-term soundness: contract-first — the honest fix is the declared seam member, not a renderer-side apology for a wrong input, and a warning would fossilize the interim as if it were a supported mode. Making AI-authored metadata hard to get wrong: an author declaring dependsOn should get correct cascading, not a caption explaining why it is stale — a warning trains authors to accept a wrong scope. Startup scope discipline: option B costs the SAME seam change as #7188 while delivering strictly less, which is the worst trade of the three. ⚠️ The one thing that would flip this to B: if #7188 is declined or deferred long, the silent staleness stops being temporary and then it must be surfaced. So this question should be closed by #7188 landing, not by a ruling that silence is acceptable."
}
],
"out_of_scope_findings": [
"filed as #7188: option B — carry the pending/staged record across the renderCellEditor seam so a dependsOn lookup is scoped by the in-flight parent (the seam change is spelled out: @object-ui/types ctx member + Zod mirror + the #6882 exact-shape pin + data-table.tsx passing `{ ...row, ...rowChanges }` + ObjectGrid reading it). Clause 2-engaged; #7165 stays open for it.",
"filed as #7190 (label `finding`): plugin-detail's InlineFieldInput supplies no dependentValues either (measured: zero non-test occurrences; control: 88 files across packages/), and plugin-detail never provides SchemaRendererContext `data` itself. ⚠️ Deliberately NOT graded a bug — a detail page renders ONE record, so ctx.data may legitimately rescue the cascade there, and no rendering measurement was made. The single probe that settles it is written into the card.",
"NOT filed, reported here as an observation for PM triage: ObjectGrid.tsx still lists `renderCellEditor` in its `ObjectGridDataTableSchemaHolds` type as an undeclared-but-live HELD key, with prose saying whether DataTableSchema should declare it is a pending packages/types ruling. That ruling HAPPENED — #6882 declared it on 2026-08-30 — so the hold entry and its surrounding census comment are stale. Harmless today (the intersection with the declared member is identical), but it is documentation that now states the opposite of the tree. Left untouched because it is outside this card and inside the file #7166 is adjacent to."
]
}

Generated by Claude Code


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. The open question is ruled below.

⭐ The pre-flight scan that saved the card — and a trap nobody had written down

a first draft of the PR body contained the phrase "does NOT close #7165", which a pre-flight scan caught — GitHub's parser ignores negation and it would have closed the card on merge.

GitHub's closing-keyword parser does not read negation."does NOT close #7165" contains close #7165, and on merge it closes the card. This PR's entire correctness depends on #7165 staying open — A is an interim, B is the real fix — so the most natural sentence a careful author would write to protect the card is the one that would have destroyed it.

⚠️ And note the shape: the failure would have been silent and delayed. The PR merges, the card closes, and the interim becomes permanent because nothing is left tracking B. Carrying this to #6970 as a fourth mangling class, since that card enumerates exactly this family. The safe form is what shipped: Part of #7165, and never the keyword at all.

Measurement 1 answered decisively, on both disjuncts

B is Clause ②-engaged, and it was established rather than assumed:

⇒ B needs a seventh ctx member, so it edits @object-ui/types, its Zod mirror, and a maintainer-ruled exact-shape pin. That is Clause ② three times over. ⭐ The Equal-not-extends detail is what makes it airtight — an extends pin would have tolerated an added member, and the answer would have been different.

All three of my ruling conditions are met: the interim is labelled in code naming #7165 and #7188 and stating exactly what is stale; the card is not closed; B is filed.

A2.2 corrected, and the correction matters

I said A was "one line: dependentValues: ctx.row". Wrong: the declared contract always carried row, but ObjectGrid's local ctx annotation omitted it — so A was a two-part edit (widen the local annotation, then pass it). ⚠️ A declared member that a local annotation silently narrows is its own small hazard: the type says the data is there and the call site cannot see it.

The test discipline is the strongest part

  • Baseline before any edit: the existing bug(plugin-grid): ObjectGrid's inline lookup picker ignores four more spec-declared field props — multiple, allowCreate, lookupPageSize, dependsOn are read on its own path and never copied #7154 pin still passed on head — so the defect was live, not already fixed by something else. Establishing that first is what makes the rest meaningful.
  • The pin encoded the defect and was UPDATED IN PLACE, not deleted, with its header prose rewritten to say so. A deleted pin is indistinguishable from a pin that never existed.
  • A negative control: a row whose saved parent is empty still gates — proving the fix supplied a value rather than disabling the gate. That is the assertion that separates "cascade works" from "cascade removed", and it is the one most likely to be skipped.
  • The staleness pin carries its own proof it is not vacuous: the region cell renders the pending south while the saved record reads north, so the test proves the staging actually landed before asserting the picker ignored it. A staleness test without that proof passes for the wrong reason.
  • Ablation predicted before running, Tests 5 failed | 4 passed (9) exactly; mutation by marker count and blob hash; restore by state with git checkout HEAD -- ABSPATHnever the bare form, which restores from the index.

⚖️ Ruling on the open question: C — close it by landing #7188, not by ruling that silence is acceptable

I left this live at dispatch and asked that it not disappear into a code comment. The answer is C, and the argument that decides it is the one I did not have when I asked:

Option B costs the same seam change as #7188 while delivering strictly less. A warning that says "scoped by the saved Region" needs the staged record — i.e. exactly the ctx member #7188 adds. Having paid that price, you would ship a caption explaining the wrong scope instead of the right scope. That is the worst of the three trades.

And a warning would fossilise the interim as a supported mode. An author who declares dependsOn should get correct cascading, not a caption explaining why it is stale — a message trains authors to accept a wrong scope as normal.

⚠️I am adopting the flip condition verbatim, because it is what keeps C honest: if #7188 is declined, or deferred long enough that the interim stops being temporary, the silent staleness must be surfaced. So this question is closed by #7188 landing, not by a ruling that silence is fine. Recorded on #7165.

Incidence: carried, not closed — exactly as the dispatch required

Two corpora, each with a live control:

  • this repo: 3 files match dependsOn|depends_on, none is this defect (the gantt hit is depends_on as task predecessor links — a different key entirely); control: 88 files across packages/.
  • hotcrm: 6 dependsOn declarations on lookup fields across 5 objects, all scoping contacts to crm_account — but 0 editable grids in its views (control: the views dir exists and was read).

⇒ ⭐ "Neither measured corpus reaches the defect today, but hotcrm is ONE metadata key away"editable: true on any of those list views. That is a far more useful answer than either "no impact" or "unmeasured", and it is the sentence a maintainer needs to prioritise #7188. Customer apps remain unmeasured, and that is stated rather than glossed.

#7190 filed with the right restraint

InlineFieldInput supplies no dependentValues either (measured: zero non-test occurrences; control: 88 files) — and it is deliberately NOT graded a bug, because a detail page renders one record so ctx.data may legitimately rescue the cascade there, and no rendering measurement was made. The single probe that would settle it is written into the card. Declining to grade on a structural analogy, and leaving the probe behind, is better than a confident grade either way.


Nothing to change.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 10:48
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 84ffdbcSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7165-grid-dependent-values branch September 1, 2026 11:02
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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): feed the inline cell editor the row as dependent values, so a dependsOn lookup column is no longer permanently uneditable - #7193

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-7165-grid-dependent-values
Sep 1, 2026
Merged

fix(plugin-grid): feed the inline cell editor the row as dependent values, so a dependsOn lookup column is no longer permanently uneditable#7193
os-warren merged 3 commits into
mainfrom
claude/issue-7165-grid-dependent-values

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Part of #7165

#7165 must stay open. This ships option A, the interim, and is deliberately not a closing reference to that card — the card stays open for option B, filed as #7188. See "Measurement 1" below — that is the fact that determined this.

Measurement 1 — B is Clause ②-engaged, so A ships as the labelled interim

The PM ruling on #7165 made the A/B choice depend on one measurement: is renderCellEditor's editor context type published surface, or internal to plugin-grid? Measured on 899730e0a, both disjuncts of that test are true:

  • Reachable from @object-ui/typespackages/types/src/data-display.ts declares renderCellEditor?: (ctx: { column; row; value; stage; commit; cancel }) => React.ReactNode on DataTableSchema, with a Zod mirror in packages/types/src/zod/data-display.zod.ts.

  • Part of DataTableSchema's declared editor-context contract — declared by maintainer ruling 2026-08-30 ([Decision] Declare renderCellEditor and schema-level cellClassName on DataTableSchema? — the two live undeclared keys the #6459 census measured #6882), replacing the (schema as any) cast that preceded it. The shape is pinned by exact type equality, not extends, in packages/types/src/__tests__/data-table-declared-keys-6882.test.ts:

    type _RenderCellEditorShape = Expect[ Equal[
    Declared[DataTableSchema]['renderCellEditor'],
    ((ctx: CellEditorContext) => ReactNode) | undefined ] ];
    

    (angle brackets transcribed as square brackets — GitHub's body sanitizer eats
    tag-shaped fragments, including inside a fence. The source uses angle brackets.)

    with the stated reason: "Declaring the key with any other shape is a different (and false) statement about the renderer, so the shape is pinned, not just the membership."

packages/plugin-grid/src/ObjectGrid.tsx states the same fence in its own words: "⛔ Do not 'fix' either hold by declaring the key on DataTableSchema as a rider — that package is published surface with its own review floor."

⇒ B needs a seventh member on that declared context. That edits @object-ui/types, its Zod mirror, and a maintainer-ruled exact-shape pin. Per the ruling, A ships under all three of its conditions: labelled in code, this card not closed, and B filed (#7188).

⚠️A is not the conclusion. A field that can never be filled is a hard defect with no workaround; a field scoped by the persisted parent instead of the staged one is wrong only where the parent was edited in the same unsaved session. Strictly better, strictly not finished.

The defect

LookupField reads the record it gates on as dependentValues ?? ctx.formValues ?? ctx.data ?? {}. The grid's inline cell editor supplied none of the three — renderCellEditor rendered FieldEditWidget with field / value / onChange only, SchemaRendererContext has no formValues, and the grid sets no ctx.data per row. The record was therefore {} for every row, dependenciesMissing was permanently true, and a column declaring dependsOn rendered a disabled trigger reading "Select region first" even when the row carried the parent value.

PR #2216 was the two-half answer to #2215. Half 2 — every picker taking the dependsOn chain as a hard baseFilter — is host-independent and was already live on the grid path, which is why the gate fired at all (verified: A2.5 holds, see LookupField's dependentFilter memo). Half 1 is per-host and the grid never got it. This PR supplies that missing input and re-implements no cascade.

The change

One line of behaviour in ObjectGrid.renderCellEditordependentValues={ctx.row} — plus row: any added to the local ctx annotation, which had omitted it (the declared contract always carried row; only this local annotation did not). FieldEditWidget already forwards dependentValues to the widget, since #7008.

The interim is labelled in the code, naming this card and #7188, and stating precisely what is stale: a parent edited but not yet saved in the same row does not re-scope the child.

Evidence

Rendering proof, packages/plugin-grid/src/__tests__/gridDependentValues-7165.test.tsx (4 tests, each with a live control in the same render):

  1. the dependsOn column opens (was lookup-trigger-gated + disabled; now lookup-trigger-regional_owner + enabled) — control: a no-dependsOn column in the same render is also enabled, so the picker path is proven reached.
  2. the picker is scoped by the row: region: 'north' reaches the query as a hard $filter and Person 07 (south) is absent — control: the no-dependsOn sibling over the same records does offer Person 07, so the absence is scoping and not a dead picker.
  3. negative control: a row whose saved parent is empty still gates, so the gate was not simply disabled — control: the filled row in the same render opens.
  4. ⚠️the interim's staleness, pinned as current behaviour: staging a new parent without saving does not re-scope the child — with its own proof that the staging actually landed (the region cell renders the pending 'south' while the saved record still reads 'north'), because without that the assertion would pass for the trivial reason that nothing was ever staged. The grid's inline cell editor scopes a dependsOn lookup by the SAVED row, not the staged one — carry the pending record across the renderCellEditor seam (option B of #7165) #7188 flips this test.

The #7154 pin was UPDATED, not deleted. The dependsOn case in lookupPickerKeys-7154.test.tsx pinned the gated behaviour, i.e. it encoded the defect. A deleted pin is indistinguishable from a pin that never existed, so it is rewritten in place to assert the fixed behaviour plus the scoping, and its header prose is rewritten to say so.

Ablation — direction predicted before running: removing the single dependentValues={ctx.row} line turns red the 4 new tests plus the updated #7154 case, and leaves the other 4 #7154 cases green.

  • No rebuild required, and this is measured rather than assumed: the root vitest.config.mts aliases every @object-ui/* specifier to that package's src, and the tests import ../ObjectGrid relatively — so nothing resolves through dist and a source mutation is directly live.
  • Mutation confirmed on disk by marker count (dependentValues={ctx.row}: 1 → 0) and blob hash (e8ab332e…f219c865…), not by the editor's exit code.
  • Result: Tests 5 failed | 4 passed (9) — exactly the predicted set, passing count included.
  • Restore proven by state, with HEAD named explicitly (git checkout HEAD -- ..., never the bare form, which restores from the index): git diff HEAD, git diff --cached and git status --short all empty, and the blob hash back to e8ab332e…. The script carried a trap ... EXIT INT TERM with absolute paths.

Gates

⚠️Two heads, stated separately so the provenance is checkable. The full set ran at f17b08047; main then advanced (#7180 landed), so it was merged in and the affected-package suite plus the cheap ratchet gates were re-run at the current head aba0c7499. Nothing below is quoted from a tree that is no longer this branch's head.

Re-run at aba0c7499 (current head, post-merge):

gateresult
packages/plugin-grid/ full suite105 files, 949 tests passed
check:control-bytesOK — 5958 tracked text files scanned
check:phantom-depsOK
check:changeset-no-majorOK

Run at f17b08047 (same tree for every path this PR touches — the merge brought in only packages/components/src/renderers/layout/containers.tsx, a .changeset, a docs page, a components test and one vitest.config.mts line, none of which this PR edits):

gateresult
type-check (plugin-grid + closure)14 tasks successful
lint (affected package, full, not narrowed)2 tasks, 0 errors (751 pre-existing warnings)
check:self-importOK
check:vi-mock-specifiers / check:vi-mock-inheritOK (4106 files, 525 with mocks)
check-governed-queue-guard --testNOT GOVERNED — 4 paths vs 5 surfaces, none matched

The typecheck genuinely covers the new tests: tsc -p tsconfig.test.json --listFiles lists all three edited files among its 1770 (this package typechecks tests via a dedicated project, so "typecheck green" is not silently excluding them).

⚠️#7166 — the concurrent lane in this same package — had not landed as of aba0c7499. It touches relationalMetaKeys.ts and the applyRelationalMeta copy set; this PR touches ObjectGrid.renderCellEditor's context construction. Disjoint symbols, but this branch will need a fresh merge if #7166 lands first.

Incidence — measured, and ⛔ not "no impact"

Two corpora, each instrument run with a live control:

  • this repo: 3 files match dependsOn|depends_on, and none is this defect — the gantt hit is depends_on as task predecessor links (a different key entirely) and the other is a select inside a form. Control: the same instrument returns 88 files across packages/.
  • hotcrm (a real shipped app, anonymous shallow clone): 6 dependsOn declarations on lookup fields across 5 objects (crm_contract ×2, crm_case, crm_quote ×2, crm_opportunity), all scoping contacts to crm_account. But 0 editable grids in its views (control: the views directory exists and was read).

⇒ Neither measured corpus reaches the defect today, because none combines dependsOn with an editable grid — but hotcrm is one metadata key away: editable: true on any of those five objects' list views hits it immediately. Customer apps are unmeasured, and the parent pattern is demonstrably real in shipped metadata. This is a bounded reading, not an all-clear.

Filed, not ridden

⚠️ Still live, per the PM's fence and not settled by this PR: should the user be told the scope came from the saved row? The interim's staleness is the same silent-wrongness class as the three refusals this seat landed in #7146 / #7147 / #7148. It is recorded on #7188 rather than left to disappear into a code comment.


Session: session_012wwHa4aaFybxXrfmfHioDM (https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM) — kept in prose because a body edit rewrites the footer below to its bare form.

Generated by Claude Code

…lues
A `dependsOn` lookup column in an editable ObjectGrid was permanently
uneditable. `LookupField` resolves
`dependentValues ?? ctx.formValues ?? ctx.data ?? {}` and the grid's
`renderCellEditor` supplied none of the three, so the resolved record was
`{}` for every row, `dependenciesMissing` stayed `true`, and the trigger
rendered disabled ("Select region first") even when the row carried the
parent value.
PR #2216closed#2215 in two halves: the form renderer injects its live
watched record, and every picker takes the `dependsOn` chain as a hard
`baseFilter`. Half 2 is host-independent and was already live here; half 1
is per-host and the grid never got it. This supplies that missing input and
re-implements no cascade.
INTERIM: `ctx.row` is the saved record, so a parent edited but not yet
saved in the same row does not re-scope the child. Carrying the staged
record needs a seventh member on `renderCellEditor`'s published context
type (declared in `@object-ui/types`, #6882, and pinned by exact type
equality), which is a contract change tracked separately.
The `dependsOn` case in `lookupPickerKeys-7154.test.tsx` pinned the gated
behaviour and is UPDATED, not deleted.
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.6 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-C6RhwKZM.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)514.13KB117.19KB
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)206.14KB55.77KB
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

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.6 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-C6RhwKZM.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)514.13KB117.19KB
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)206.14KB55.77KB
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": 7165,
"status": "done",
"branch": "claude/issue-7165-grid-dependent-values",
"pr": "https://github.com/objectstack-ai/objectui/pull/7193",
"premise_still_valid": true,
"summary": "MEASUREMENT 1: B IS Clause 2-engaged — so option A shipped as the labelled interim and ⛔ #7165 MUST STAY OPEN (the PR says 'Part of #7165', never a closing keyword). The editor context is published surface on both disjuncts of the test: `DataTableSchema.renderCellEditor` is declared in packages/types/src/data-display.ts with a Zod mirror, declared by maintainer ruling 2026-08-30 (#6882) replacing a `(schema as any)` cast, and its exact shape is pinned by an `Equal` (not `extends`) type assertion in packages/types/src/__tests__/data-table-declared-keys-6882.test.ts whose comment states the shape is pinned deliberately; ObjectGrid.tsx independently fences that package as 'published surface with its own review floor'. B needs a seventh ctx member, so it edits @object-ui/types, its Zod mirror and that maintainer-ruled pin. All three ruling conditions are met: the interim is labelled in code naming #7165 and the follow-up and stating exactly what is stale, the card is not closed, and B is filed as #7188. Premise re-measured on head 899730e0a: the existing pin still passed before my change, i.e. the defect was still live. A2.1/A2.3/A2.4/A2.5 all confirmed; A2.2 needed one CORRECTION — the declared contract always carried `row`, but ObjectGrid's LOCAL ctx annotation omitted it, so A was a two-part edit (widen the local annotation, then pass it), not a literal one-liner.",
"tests": "All runs from repo root via the shared verify lock (objectui forbids `pnpm --filter PKG test`; path filters only). BASELINE on 899730e0a before any edit: `pnpm exec vitest run packages/plugin-grid/src/__tests__/lookupPickerKeys-7154.test.tsx` -> 'Test Files 1 passed (1) / Tests 5 passed (5)' — the gated pin still passed, so the defect was live on head. AFTER FIX, same command -> 'Tests 1 failed | 4 passed (5)', 'AssertionError: expected lookup-trigger-regional_owner to be lookup-trigger-gated' — the pin encoded the defect, so it was UPDATED IN PLACE (not deleted), header prose rewritten to say so. NEW FILE packages/plugin-grid/src/__tests__/gridDependentValues-7165.test.tsx, 4 tests each with a live control in the same render: (1) dependsOn column now opens, control column also opens; (2) picker SCOPED — $filter.region='north' reaches the query, Person 07 (south) absent, while the no-dependsOn control over the same records DOES offer Person 07; (3) NEGATIVE CONTROL — a row whose saved parent is empty still gates, proving the gate was not disabled; (4) the interim's staleness pinned as current behaviour, with its own proof the staging landed (region cell renders pending 'south' while the saved record reads 'north'), which is what stops it passing vacuously. Both files together: 'Test Files 2 passed (2) / Tests 9 passed (9)'. ABLATION — direction predicted BEFORE running (remove `dependentValues={ctx.row}` -> the 4 new tests plus the updated 7154 case go red, the other 4 stay green). NO REBUILD NEEDED, and that is measured not assumed: root vitest.config.mts aliases every @object-ui/* specifier to that package's src and the tests import ../ObjectGrid relatively, so nothing resolves through dist and a source mutation is directly live. Mutation confirmed ON DISK by marker count (1 -> 0) AND blob hash (e8ab332e… -> f219c865…), never by the editing tool's exit code. Result: 'Tests 5 failed | 4 passed (9)' — exactly the predicted set, passing count included. Restore proven BY STATE with HEAD named explicitly (`git checkout HEAD -- ABSPATH`, never the bare form which restores from the index): git diff HEAD, git diff --cached and git status --short all empty and the blob hash back to e8ab332e…; the script carried `trap ... EXIT INT TERM` with absolute paths. GATES: at final head aba0c7499 (after merging main, which had advanced to 8ec11e14f / #7180) — full packages/plugin-grid/ suite 'Test Files 105 passed (105) / Tests 949 passed (949)', check:control-bytes OK (5958 files), check:phantom-deps OK, check:changeset-no-major OK. At f17b08047 (identical tree for every path this PR touches): type-check plugin-grid + closure 14 tasks successful, lint of the affected package in FULL (not narrowed) 2 tasks 0 errors (751 pre-existing warnings), check:self-import OK, check:vi-mock-specifiers / check:vi-mock-inherit OK (4106 files, 525 with mocks), check-governed-queue-guard --test = NOT GOVERNED (4 paths vs 5 surfaces). Verdicts read from each gate's own printed line, with exit codes captured after redirection, never through a pipe. NOT-MEASURED check cleared: `tsc -p tsconfig.test.json --listFiles` lists all three edited files among its 1770, so the green typecheck genuinely covers the new tests rather than excluding them.",
"mcp_calls": "11 — 2 issue reads, 2 targeted search_issues (each with a control word that hit: #7165 itself, then #7154/#2215), 2 issue creates, 1 PR create, 1 PR body read-back, 1 PR body update, 2 report comments. REST was 403 for repo-scoped reads and `gh` is absent, both measured with a control before switching channels.",
"open_questions": [
{
"question": "Should the grid TELL the user that a dependsOn picker is scoped by the SAVED parent while an unsaved edit to that parent is staged in the same row? The PM's ruling explicitly left this live rather than settled, and asked that it not disappear into a code comment.",
"options": [
"A. Say nothing (what this PR ships). The picker silently uses the persisted parent. Zero new surface, but it is the same silent-wrongness class as the three refusals this seat landed in #7146 / #7147 / #7148.",
"B. Surface a hint on the trigger/popover when the parent has a staged-but-unsaved value that differs from the saved one — e.g. 'scoped by the saved Region; save the row to re-scope'. Needs the staged record, i.e. it is gated behind exactly the same seam change as option B of this card.",
"C. Do not build a message at all: land #7188, which removes the staleness and therefore removes the question."
],
"recommendation": "C, and treat B as dead. Real business need: the staleness only exists because the interim exists, so a message would be UI built to describe a defect we have already scheduled to delete. Long-term soundness: contract-first — the honest fix is the declared seam member, not a renderer-side apology for a wrong input, and a warning would fossilize the interim as if it were a supported mode. Making AI-authored metadata hard to get wrong: an author declaring dependsOn should get correct cascading, not a caption explaining why it is stale — a warning trains authors to accept a wrong scope. Startup scope discipline: option B costs the SAME seam change as #7188 while delivering strictly less, which is the worst trade of the three. ⚠️ The one thing that would flip this to B: if #7188 is declined or deferred long, the silent staleness stops being temporary and then it must be surfaced. So this question should be closed by #7188 landing, not by a ruling that silence is acceptable."
}
],
"out_of_scope_findings": [
"filed as #7188: option B — carry the pending/staged record across the renderCellEditor seam so a dependsOn lookup is scoped by the in-flight parent (the seam change is spelled out: @object-ui/types ctx member + Zod mirror + the #6882 exact-shape pin + data-table.tsx passing `{ ...row, ...rowChanges }` + ObjectGrid reading it). Clause 2-engaged; #7165 stays open for it.",
"filed as #7190 (label `finding`): plugin-detail's InlineFieldInput supplies no dependentValues either (measured: zero non-test occurrences; control: 88 files across packages/), and plugin-detail never provides SchemaRendererContext `data` itself. ⚠️ Deliberately NOT graded a bug — a detail page renders ONE record, so ctx.data may legitimately rescue the cascade there, and no rendering measurement was made. The single probe that settles it is written into the card.",
"NOT filed, reported here as an observation for PM triage: ObjectGrid.tsx still lists `renderCellEditor` in its `ObjectGridDataTableSchemaHolds` type as an undeclared-but-live HELD key, with prose saying whether DataTableSchema should declare it is a pending packages/types ruling. That ruling HAPPENED — #6882 declared it on 2026-08-30 — so the hold entry and its surrounding census comment are stale. Harmless today (the intersection with the declared member is identical), but it is documentation that now states the opposite of the tree. Left untouched because it is outside this card and inside the file #7166 is adjacent to."
]
}

Generated by Claude Code


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. The open question is ruled below.

⭐ The pre-flight scan that saved the card — and a trap nobody had written down

a first draft of the PR body contained the phrase "does NOT close #7165", which a pre-flight scan caught — GitHub's parser ignores negation and it would have closed the card on merge.

GitHub's closing-keyword parser does not read negation."does NOT close #7165" contains close #7165, and on merge it closes the card. This PR's entire correctness depends on #7165 staying open — A is an interim, B is the real fix — so the most natural sentence a careful author would write to protect the card is the one that would have destroyed it.

⚠️ And note the shape: the failure would have been silent and delayed. The PR merges, the card closes, and the interim becomes permanent because nothing is left tracking B. Carrying this to #6970 as a fourth mangling class, since that card enumerates exactly this family. The safe form is what shipped: Part of #7165, and never the keyword at all.

Measurement 1 answered decisively, on both disjuncts

B is Clause ②-engaged, and it was established rather than assumed:

⇒ B needs a seventh ctx member, so it edits @object-ui/types, its Zod mirror, and a maintainer-ruled exact-shape pin. That is Clause ② three times over. ⭐ The Equal-not-extends detail is what makes it airtight — an extends pin would have tolerated an added member, and the answer would have been different.

All three of my ruling conditions are met: the interim is labelled in code naming #7165 and #7188 and stating exactly what is stale; the card is not closed; B is filed.

A2.2 corrected, and the correction matters

I said A was "one line: dependentValues: ctx.row". Wrong: the declared contract always carried row, but ObjectGrid's local ctx annotation omitted it — so A was a two-part edit (widen the local annotation, then pass it). ⚠️ A declared member that a local annotation silently narrows is its own small hazard: the type says the data is there and the call site cannot see it.

The test discipline is the strongest part

  • Baseline before any edit: the existing bug(plugin-grid): ObjectGrid's inline lookup picker ignores four more spec-declared field props — multiple, allowCreate, lookupPageSize, dependsOn are read on its own path and never copied #7154 pin still passed on head — so the defect was live, not already fixed by something else. Establishing that first is what makes the rest meaningful.
  • The pin encoded the defect and was UPDATED IN PLACE, not deleted, with its header prose rewritten to say so. A deleted pin is indistinguishable from a pin that never existed.
  • A negative control: a row whose saved parent is empty still gates — proving the fix supplied a value rather than disabling the gate. That is the assertion that separates "cascade works" from "cascade removed", and it is the one most likely to be skipped.
  • The staleness pin carries its own proof it is not vacuous: the region cell renders the pending south while the saved record reads north, so the test proves the staging actually landed before asserting the picker ignored it. A staleness test without that proof passes for the wrong reason.
  • Ablation predicted before running, Tests 5 failed | 4 passed (9) exactly; mutation by marker count and blob hash; restore by state with git checkout HEAD -- ABSPATHnever the bare form, which restores from the index.

⚖️ Ruling on the open question: C — close it by landing #7188, not by ruling that silence is acceptable

I left this live at dispatch and asked that it not disappear into a code comment. The answer is C, and the argument that decides it is the one I did not have when I asked:

Option B costs the same seam change as #7188 while delivering strictly less. A warning that says "scoped by the saved Region" needs the staged record — i.e. exactly the ctx member #7188 adds. Having paid that price, you would ship a caption explaining the wrong scope instead of the right scope. That is the worst of the three trades.

And a warning would fossilise the interim as a supported mode. An author who declares dependsOn should get correct cascading, not a caption explaining why it is stale — a message trains authors to accept a wrong scope as normal.

⚠️I am adopting the flip condition verbatim, because it is what keeps C honest: if #7188 is declined, or deferred long enough that the interim stops being temporary, the silent staleness must be surfaced. So this question is closed by #7188 landing, not by a ruling that silence is fine. Recorded on #7165.

Incidence: carried, not closed — exactly as the dispatch required

Two corpora, each with a live control:

  • this repo: 3 files match dependsOn|depends_on, none is this defect (the gantt hit is depends_on as task predecessor links — a different key entirely); control: 88 files across packages/.
  • hotcrm: 6 dependsOn declarations on lookup fields across 5 objects, all scoping contacts to crm_account — but 0 editable grids in its views (control: the views dir exists and was read).

⇒ ⭐ "Neither measured corpus reaches the defect today, but hotcrm is ONE metadata key away"editable: true on any of those list views. That is a far more useful answer than either "no impact" or "unmeasured", and it is the sentence a maintainer needs to prioritise #7188. Customer apps remain unmeasured, and that is stated rather than glossed.

#7190 filed with the right restraint

InlineFieldInput supplies no dependentValues either (measured: zero non-test occurrences; control: 88 files) — and it is deliberately NOT graded a bug, because a detail page renders one record so ctx.data may legitimately rescue the cascade there, and no rendering measurement was made. The single probe that would settle it is written into the card. Declining to grade on a structural analogy, and leaving the probe behind, is better than a confident grade either way.


Nothing to change.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 10:48
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 84ffdbcSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7165-grid-dependent-values branch September 1, 2026 11:02
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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): feed the inline cell editor the row as dependent values, so a dependsOn lookup column is no longer permanently uneditable - #7193

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-7165-grid-dependent-values
Sep 1, 2026
Merged

fix(plugin-grid): feed the inline cell editor the row as dependent values, so a dependsOn lookup column is no longer permanently uneditable#7193
os-warren merged 3 commits into
mainfrom
claude/issue-7165-grid-dependent-values

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Part of #7165

#7165 must stay open. This ships option A, the interim, and is deliberately not a closing reference to that card — the card stays open for option B, filed as #7188. See "Measurement 1" below — that is the fact that determined this.

Measurement 1 — B is Clause ②-engaged, so A ships as the labelled interim

The PM ruling on #7165 made the A/B choice depend on one measurement: is renderCellEditor's editor context type published surface, or internal to plugin-grid? Measured on 899730e0a, both disjuncts of that test are true:

  • Reachable from @object-ui/typespackages/types/src/data-display.ts declares renderCellEditor?: (ctx: { column; row; value; stage; commit; cancel }) => React.ReactNode on DataTableSchema, with a Zod mirror in packages/types/src/zod/data-display.zod.ts.

  • Part of DataTableSchema's declared editor-context contract — declared by maintainer ruling 2026-08-30 ([Decision] Declare renderCellEditor and schema-level cellClassName on DataTableSchema? — the two live undeclared keys the #6459 census measured #6882), replacing the (schema as any) cast that preceded it. The shape is pinned by exact type equality, not extends, in packages/types/src/__tests__/data-table-declared-keys-6882.test.ts:

    type _RenderCellEditorShape = Expect[ Equal[
    Declared[DataTableSchema]['renderCellEditor'],
    ((ctx: CellEditorContext) => ReactNode) | undefined ] ];
    

    (angle brackets transcribed as square brackets — GitHub's body sanitizer eats
    tag-shaped fragments, including inside a fence. The source uses angle brackets.)

    with the stated reason: "Declaring the key with any other shape is a different (and false) statement about the renderer, so the shape is pinned, not just the membership."

packages/plugin-grid/src/ObjectGrid.tsx states the same fence in its own words: "⛔ Do not 'fix' either hold by declaring the key on DataTableSchema as a rider — that package is published surface with its own review floor."

⇒ B needs a seventh member on that declared context. That edits @object-ui/types, its Zod mirror, and a maintainer-ruled exact-shape pin. Per the ruling, A ships under all three of its conditions: labelled in code, this card not closed, and B filed (#7188).

⚠️A is not the conclusion. A field that can never be filled is a hard defect with no workaround; a field scoped by the persisted parent instead of the staged one is wrong only where the parent was edited in the same unsaved session. Strictly better, strictly not finished.

The defect

LookupField reads the record it gates on as dependentValues ?? ctx.formValues ?? ctx.data ?? {}. The grid's inline cell editor supplied none of the three — renderCellEditor rendered FieldEditWidget with field / value / onChange only, SchemaRendererContext has no formValues, and the grid sets no ctx.data per row. The record was therefore {} for every row, dependenciesMissing was permanently true, and a column declaring dependsOn rendered a disabled trigger reading "Select region first" even when the row carried the parent value.

PR #2216 was the two-half answer to #2215. Half 2 — every picker taking the dependsOn chain as a hard baseFilter — is host-independent and was already live on the grid path, which is why the gate fired at all (verified: A2.5 holds, see LookupField's dependentFilter memo). Half 1 is per-host and the grid never got it. This PR supplies that missing input and re-implements no cascade.

The change

One line of behaviour in ObjectGrid.renderCellEditordependentValues={ctx.row} — plus row: any added to the local ctx annotation, which had omitted it (the declared contract always carried row; only this local annotation did not). FieldEditWidget already forwards dependentValues to the widget, since #7008.

The interim is labelled in the code, naming this card and #7188, and stating precisely what is stale: a parent edited but not yet saved in the same row does not re-scope the child.

Evidence

Rendering proof, packages/plugin-grid/src/__tests__/gridDependentValues-7165.test.tsx (4 tests, each with a live control in the same render):

  1. the dependsOn column opens (was lookup-trigger-gated + disabled; now lookup-trigger-regional_owner + enabled) — control: a no-dependsOn column in the same render is also enabled, so the picker path is proven reached.
  2. the picker is scoped by the row: region: 'north' reaches the query as a hard $filter and Person 07 (south) is absent — control: the no-dependsOn sibling over the same records does offer Person 07, so the absence is scoping and not a dead picker.
  3. negative control: a row whose saved parent is empty still gates, so the gate was not simply disabled — control: the filled row in the same render opens.
  4. ⚠️the interim's staleness, pinned as current behaviour: staging a new parent without saving does not re-scope the child — with its own proof that the staging actually landed (the region cell renders the pending 'south' while the saved record still reads 'north'), because without that the assertion would pass for the trivial reason that nothing was ever staged. The grid's inline cell editor scopes a dependsOn lookup by the SAVED row, not the staged one — carry the pending record across the renderCellEditor seam (option B of #7165) #7188 flips this test.

The #7154 pin was UPDATED, not deleted. The dependsOn case in lookupPickerKeys-7154.test.tsx pinned the gated behaviour, i.e. it encoded the defect. A deleted pin is indistinguishable from a pin that never existed, so it is rewritten in place to assert the fixed behaviour plus the scoping, and its header prose is rewritten to say so.

Ablation — direction predicted before running: removing the single dependentValues={ctx.row} line turns red the 4 new tests plus the updated #7154 case, and leaves the other 4 #7154 cases green.

  • No rebuild required, and this is measured rather than assumed: the root vitest.config.mts aliases every @object-ui/* specifier to that package's src, and the tests import ../ObjectGrid relatively — so nothing resolves through dist and a source mutation is directly live.
  • Mutation confirmed on disk by marker count (dependentValues={ctx.row}: 1 → 0) and blob hash (e8ab332e…f219c865…), not by the editor's exit code.
  • Result: Tests 5 failed | 4 passed (9) — exactly the predicted set, passing count included.
  • Restore proven by state, with HEAD named explicitly (git checkout HEAD -- ..., never the bare form, which restores from the index): git diff HEAD, git diff --cached and git status --short all empty, and the blob hash back to e8ab332e…. The script carried a trap ... EXIT INT TERM with absolute paths.

Gates

⚠️Two heads, stated separately so the provenance is checkable. The full set ran at f17b08047; main then advanced (#7180 landed), so it was merged in and the affected-package suite plus the cheap ratchet gates were re-run at the current head aba0c7499. Nothing below is quoted from a tree that is no longer this branch's head.

Re-run at aba0c7499 (current head, post-merge):

gateresult
packages/plugin-grid/ full suite105 files, 949 tests passed
check:control-bytesOK — 5958 tracked text files scanned
check:phantom-depsOK
check:changeset-no-majorOK

Run at f17b08047 (same tree for every path this PR touches — the merge brought in only packages/components/src/renderers/layout/containers.tsx, a .changeset, a docs page, a components test and one vitest.config.mts line, none of which this PR edits):

gateresult
type-check (plugin-grid + closure)14 tasks successful
lint (affected package, full, not narrowed)2 tasks, 0 errors (751 pre-existing warnings)
check:self-importOK
check:vi-mock-specifiers / check:vi-mock-inheritOK (4106 files, 525 with mocks)
check-governed-queue-guard --testNOT GOVERNED — 4 paths vs 5 surfaces, none matched

The typecheck genuinely covers the new tests: tsc -p tsconfig.test.json --listFiles lists all three edited files among its 1770 (this package typechecks tests via a dedicated project, so "typecheck green" is not silently excluding them).

⚠️#7166 — the concurrent lane in this same package — had not landed as of aba0c7499. It touches relationalMetaKeys.ts and the applyRelationalMeta copy set; this PR touches ObjectGrid.renderCellEditor's context construction. Disjoint symbols, but this branch will need a fresh merge if #7166 lands first.

Incidence — measured, and ⛔ not "no impact"

Two corpora, each instrument run with a live control:

  • this repo: 3 files match dependsOn|depends_on, and none is this defect — the gantt hit is depends_on as task predecessor links (a different key entirely) and the other is a select inside a form. Control: the same instrument returns 88 files across packages/.
  • hotcrm (a real shipped app, anonymous shallow clone): 6 dependsOn declarations on lookup fields across 5 objects (crm_contract ×2, crm_case, crm_quote ×2, crm_opportunity), all scoping contacts to crm_account. But 0 editable grids in its views (control: the views directory exists and was read).

⇒ Neither measured corpus reaches the defect today, because none combines dependsOn with an editable grid — but hotcrm is one metadata key away: editable: true on any of those five objects' list views hits it immediately. Customer apps are unmeasured, and the parent pattern is demonstrably real in shipped metadata. This is a bounded reading, not an all-clear.

Filed, not ridden

⚠️ Still live, per the PM's fence and not settled by this PR: should the user be told the scope came from the saved row? The interim's staleness is the same silent-wrongness class as the three refusals this seat landed in #7146 / #7147 / #7148. It is recorded on #7188 rather than left to disappear into a code comment.


Session: session_012wwHa4aaFybxXrfmfHioDM (https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM) — kept in prose because a body edit rewrites the footer below to its bare form.

Generated by Claude Code

…lues
A `dependsOn` lookup column in an editable ObjectGrid was permanently
uneditable. `LookupField` resolves
`dependentValues ?? ctx.formValues ?? ctx.data ?? {}` and the grid's
`renderCellEditor` supplied none of the three, so the resolved record was
`{}` for every row, `dependenciesMissing` stayed `true`, and the trigger
rendered disabled ("Select region first") even when the row carried the
parent value.
PR #2216closed#2215 in two halves: the form renderer injects its live
watched record, and every picker takes the `dependsOn` chain as a hard
`baseFilter`. Half 2 is host-independent and was already live here; half 1
is per-host and the grid never got it. This supplies that missing input and
re-implements no cascade.
INTERIM: `ctx.row` is the saved record, so a parent edited but not yet
saved in the same row does not re-scope the child. Carrying the staged
record needs a seventh member on `renderCellEditor`'s published context
type (declared in `@object-ui/types`, #6882, and pinned by exact type
equality), which is a contract change tracked separately.
The `dependsOn` case in `lookupPickerKeys-7154.test.tsx` pinned the gated
behaviour and is UPDATED, not deleted.
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.6 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-C6RhwKZM.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)514.13KB117.19KB
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)206.14KB55.77KB
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

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.6 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-C6RhwKZM.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)514.13KB117.19KB
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)206.14KB55.77KB
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": 7165,
"status": "done",
"branch": "claude/issue-7165-grid-dependent-values",
"pr": "https://github.com/objectstack-ai/objectui/pull/7193",
"premise_still_valid": true,
"summary": "MEASUREMENT 1: B IS Clause 2-engaged — so option A shipped as the labelled interim and ⛔ #7165 MUST STAY OPEN (the PR says 'Part of #7165', never a closing keyword). The editor context is published surface on both disjuncts of the test: `DataTableSchema.renderCellEditor` is declared in packages/types/src/data-display.ts with a Zod mirror, declared by maintainer ruling 2026-08-30 (#6882) replacing a `(schema as any)` cast, and its exact shape is pinned by an `Equal` (not `extends`) type assertion in packages/types/src/__tests__/data-table-declared-keys-6882.test.ts whose comment states the shape is pinned deliberately; ObjectGrid.tsx independently fences that package as 'published surface with its own review floor'. B needs a seventh ctx member, so it edits @object-ui/types, its Zod mirror and that maintainer-ruled pin. All three ruling conditions are met: the interim is labelled in code naming #7165 and the follow-up and stating exactly what is stale, the card is not closed, and B is filed as #7188. Premise re-measured on head 899730e0a: the existing pin still passed before my change, i.e. the defect was still live. A2.1/A2.3/A2.4/A2.5 all confirmed; A2.2 needed one CORRECTION — the declared contract always carried `row`, but ObjectGrid's LOCAL ctx annotation omitted it, so A was a two-part edit (widen the local annotation, then pass it), not a literal one-liner.",
"tests": "All runs from repo root via the shared verify lock (objectui forbids `pnpm --filter PKG test`; path filters only). BASELINE on 899730e0a before any edit: `pnpm exec vitest run packages/plugin-grid/src/__tests__/lookupPickerKeys-7154.test.tsx` -> 'Test Files 1 passed (1) / Tests 5 passed (5)' — the gated pin still passed, so the defect was live on head. AFTER FIX, same command -> 'Tests 1 failed | 4 passed (5)', 'AssertionError: expected lookup-trigger-regional_owner to be lookup-trigger-gated' — the pin encoded the defect, so it was UPDATED IN PLACE (not deleted), header prose rewritten to say so. NEW FILE packages/plugin-grid/src/__tests__/gridDependentValues-7165.test.tsx, 4 tests each with a live control in the same render: (1) dependsOn column now opens, control column also opens; (2) picker SCOPED — $filter.region='north' reaches the query, Person 07 (south) absent, while the no-dependsOn control over the same records DOES offer Person 07; (3) NEGATIVE CONTROL — a row whose saved parent is empty still gates, proving the gate was not disabled; (4) the interim's staleness pinned as current behaviour, with its own proof the staging landed (region cell renders pending 'south' while the saved record reads 'north'), which is what stops it passing vacuously. Both files together: 'Test Files 2 passed (2) / Tests 9 passed (9)'. ABLATION — direction predicted BEFORE running (remove `dependentValues={ctx.row}` -> the 4 new tests plus the updated 7154 case go red, the other 4 stay green). NO REBUILD NEEDED, and that is measured not assumed: root vitest.config.mts aliases every @object-ui/* specifier to that package's src and the tests import ../ObjectGrid relatively, so nothing resolves through dist and a source mutation is directly live. Mutation confirmed ON DISK by marker count (1 -> 0) AND blob hash (e8ab332e… -> f219c865…), never by the editing tool's exit code. Result: 'Tests 5 failed | 4 passed (9)' — exactly the predicted set, passing count included. Restore proven BY STATE with HEAD named explicitly (`git checkout HEAD -- ABSPATH`, never the bare form which restores from the index): git diff HEAD, git diff --cached and git status --short all empty and the blob hash back to e8ab332e…; the script carried `trap ... EXIT INT TERM` with absolute paths. GATES: at final head aba0c7499 (after merging main, which had advanced to 8ec11e14f / #7180) — full packages/plugin-grid/ suite 'Test Files 105 passed (105) / Tests 949 passed (949)', check:control-bytes OK (5958 files), check:phantom-deps OK, check:changeset-no-major OK. At f17b08047 (identical tree for every path this PR touches): type-check plugin-grid + closure 14 tasks successful, lint of the affected package in FULL (not narrowed) 2 tasks 0 errors (751 pre-existing warnings), check:self-import OK, check:vi-mock-specifiers / check:vi-mock-inherit OK (4106 files, 525 with mocks), check-governed-queue-guard --test = NOT GOVERNED (4 paths vs 5 surfaces). Verdicts read from each gate's own printed line, with exit codes captured after redirection, never through a pipe. NOT-MEASURED check cleared: `tsc -p tsconfig.test.json --listFiles` lists all three edited files among its 1770, so the green typecheck genuinely covers the new tests rather than excluding them.",
"mcp_calls": "11 — 2 issue reads, 2 targeted search_issues (each with a control word that hit: #7165 itself, then #7154/#2215), 2 issue creates, 1 PR create, 1 PR body read-back, 1 PR body update, 2 report comments. REST was 403 for repo-scoped reads and `gh` is absent, both measured with a control before switching channels.",
"open_questions": [
{
"question": "Should the grid TELL the user that a dependsOn picker is scoped by the SAVED parent while an unsaved edit to that parent is staged in the same row? The PM's ruling explicitly left this live rather than settled, and asked that it not disappear into a code comment.",
"options": [
"A. Say nothing (what this PR ships). The picker silently uses the persisted parent. Zero new surface, but it is the same silent-wrongness class as the three refusals this seat landed in #7146 / #7147 / #7148.",
"B. Surface a hint on the trigger/popover when the parent has a staged-but-unsaved value that differs from the saved one — e.g. 'scoped by the saved Region; save the row to re-scope'. Needs the staged record, i.e. it is gated behind exactly the same seam change as option B of this card.",
"C. Do not build a message at all: land #7188, which removes the staleness and therefore removes the question."
],
"recommendation": "C, and treat B as dead. Real business need: the staleness only exists because the interim exists, so a message would be UI built to describe a defect we have already scheduled to delete. Long-term soundness: contract-first — the honest fix is the declared seam member, not a renderer-side apology for a wrong input, and a warning would fossilize the interim as if it were a supported mode. Making AI-authored metadata hard to get wrong: an author declaring dependsOn should get correct cascading, not a caption explaining why it is stale — a warning trains authors to accept a wrong scope. Startup scope discipline: option B costs the SAME seam change as #7188 while delivering strictly less, which is the worst trade of the three. ⚠️ The one thing that would flip this to B: if #7188 is declined or deferred long, the silent staleness stops being temporary and then it must be surfaced. So this question should be closed by #7188 landing, not by a ruling that silence is acceptable."
}
],
"out_of_scope_findings": [
"filed as #7188: option B — carry the pending/staged record across the renderCellEditor seam so a dependsOn lookup is scoped by the in-flight parent (the seam change is spelled out: @object-ui/types ctx member + Zod mirror + the #6882 exact-shape pin + data-table.tsx passing `{ ...row, ...rowChanges }` + ObjectGrid reading it). Clause 2-engaged; #7165 stays open for it.",
"filed as #7190 (label `finding`): plugin-detail's InlineFieldInput supplies no dependentValues either (measured: zero non-test occurrences; control: 88 files across packages/), and plugin-detail never provides SchemaRendererContext `data` itself. ⚠️ Deliberately NOT graded a bug — a detail page renders ONE record, so ctx.data may legitimately rescue the cascade there, and no rendering measurement was made. The single probe that settles it is written into the card.",
"NOT filed, reported here as an observation for PM triage: ObjectGrid.tsx still lists `renderCellEditor` in its `ObjectGridDataTableSchemaHolds` type as an undeclared-but-live HELD key, with prose saying whether DataTableSchema should declare it is a pending packages/types ruling. That ruling HAPPENED — #6882 declared it on 2026-08-30 — so the hold entry and its surrounding census comment are stale. Harmless today (the intersection with the declared member is identical), but it is documentation that now states the opposite of the tree. Left untouched because it is outside this card and inside the file #7166 is adjacent to."
]
}

Generated by Claude Code


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. The open question is ruled below.

⭐ The pre-flight scan that saved the card — and a trap nobody had written down

a first draft of the PR body contained the phrase "does NOT close #7165", which a pre-flight scan caught — GitHub's parser ignores negation and it would have closed the card on merge.

GitHub's closing-keyword parser does not read negation."does NOT close #7165" contains close #7165, and on merge it closes the card. This PR's entire correctness depends on #7165 staying open — A is an interim, B is the real fix — so the most natural sentence a careful author would write to protect the card is the one that would have destroyed it.

⚠️ And note the shape: the failure would have been silent and delayed. The PR merges, the card closes, and the interim becomes permanent because nothing is left tracking B. Carrying this to #6970 as a fourth mangling class, since that card enumerates exactly this family. The safe form is what shipped: Part of #7165, and never the keyword at all.

Measurement 1 answered decisively, on both disjuncts

B is Clause ②-engaged, and it was established rather than assumed:

⇒ B needs a seventh ctx member, so it edits @object-ui/types, its Zod mirror, and a maintainer-ruled exact-shape pin. That is Clause ② three times over. ⭐ The Equal-not-extends detail is what makes it airtight — an extends pin would have tolerated an added member, and the answer would have been different.

All three of my ruling conditions are met: the interim is labelled in code naming #7165 and #7188 and stating exactly what is stale; the card is not closed; B is filed.

A2.2 corrected, and the correction matters

I said A was "one line: dependentValues: ctx.row". Wrong: the declared contract always carried row, but ObjectGrid's local ctx annotation omitted it — so A was a two-part edit (widen the local annotation, then pass it). ⚠️ A declared member that a local annotation silently narrows is its own small hazard: the type says the data is there and the call site cannot see it.

The test discipline is the strongest part

  • Baseline before any edit: the existing bug(plugin-grid): ObjectGrid's inline lookup picker ignores four more spec-declared field props — multiple, allowCreate, lookupPageSize, dependsOn are read on its own path and never copied #7154 pin still passed on head — so the defect was live, not already fixed by something else. Establishing that first is what makes the rest meaningful.
  • The pin encoded the defect and was UPDATED IN PLACE, not deleted, with its header prose rewritten to say so. A deleted pin is indistinguishable from a pin that never existed.
  • A negative control: a row whose saved parent is empty still gates — proving the fix supplied a value rather than disabling the gate. That is the assertion that separates "cascade works" from "cascade removed", and it is the one most likely to be skipped.
  • The staleness pin carries its own proof it is not vacuous: the region cell renders the pending south while the saved record reads north, so the test proves the staging actually landed before asserting the picker ignored it. A staleness test without that proof passes for the wrong reason.
  • Ablation predicted before running, Tests 5 failed | 4 passed (9) exactly; mutation by marker count and blob hash; restore by state with git checkout HEAD -- ABSPATHnever the bare form, which restores from the index.

⚖️ Ruling on the open question: C — close it by landing #7188, not by ruling that silence is acceptable

I left this live at dispatch and asked that it not disappear into a code comment. The answer is C, and the argument that decides it is the one I did not have when I asked:

Option B costs the same seam change as #7188 while delivering strictly less. A warning that says "scoped by the saved Region" needs the staged record — i.e. exactly the ctx member #7188 adds. Having paid that price, you would ship a caption explaining the wrong scope instead of the right scope. That is the worst of the three trades.

And a warning would fossilise the interim as a supported mode. An author who declares dependsOn should get correct cascading, not a caption explaining why it is stale — a message trains authors to accept a wrong scope as normal.

⚠️I am adopting the flip condition verbatim, because it is what keeps C honest: if #7188 is declined, or deferred long enough that the interim stops being temporary, the silent staleness must be surfaced. So this question is closed by #7188 landing, not by a ruling that silence is fine. Recorded on #7165.

Incidence: carried, not closed — exactly as the dispatch required

Two corpora, each with a live control:

  • this repo: 3 files match dependsOn|depends_on, none is this defect (the gantt hit is depends_on as task predecessor links — a different key entirely); control: 88 files across packages/.
  • hotcrm: 6 dependsOn declarations on lookup fields across 5 objects, all scoping contacts to crm_account — but 0 editable grids in its views (control: the views dir exists and was read).

⇒ ⭐ "Neither measured corpus reaches the defect today, but hotcrm is ONE metadata key away"editable: true on any of those list views. That is a far more useful answer than either "no impact" or "unmeasured", and it is the sentence a maintainer needs to prioritise #7188. Customer apps remain unmeasured, and that is stated rather than glossed.

#7190 filed with the right restraint

InlineFieldInput supplies no dependentValues either (measured: zero non-test occurrences; control: 88 files) — and it is deliberately NOT graded a bug, because a detail page renders one record so ctx.data may legitimately rescue the cascade there, and no rendering measurement was made. The single probe that would settle it is written into the card. Declining to grade on a structural analogy, and leaving the probe behind, is better than a confident grade either way.


Nothing to change.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 10:48
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 84ffdbcSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7165-grid-dependent-values branch September 1, 2026 11:02
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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): feed the inline cell editor the row as dependent values, so a dependsOn lookup column is no longer permanently uneditable - #7193

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-7165-grid-dependent-values
Sep 1, 2026
Merged

fix(plugin-grid): feed the inline cell editor the row as dependent values, so a dependsOn lookup column is no longer permanently uneditable#7193
os-warren merged 3 commits into
mainfrom
claude/issue-7165-grid-dependent-values

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Part of #7165

#7165 must stay open. This ships option A, the interim, and is deliberately not a closing reference to that card — the card stays open for option B, filed as #7188. See "Measurement 1" below — that is the fact that determined this.

Measurement 1 — B is Clause ②-engaged, so A ships as the labelled interim

The PM ruling on #7165 made the A/B choice depend on one measurement: is renderCellEditor's editor context type published surface, or internal to plugin-grid? Measured on 899730e0a, both disjuncts of that test are true:

  • Reachable from @object-ui/typespackages/types/src/data-display.ts declares renderCellEditor?: (ctx: { column; row; value; stage; commit; cancel }) => React.ReactNode on DataTableSchema, with a Zod mirror in packages/types/src/zod/data-display.zod.ts.

  • Part of DataTableSchema's declared editor-context contract — declared by maintainer ruling 2026-08-30 ([Decision] Declare renderCellEditor and schema-level cellClassName on DataTableSchema? — the two live undeclared keys the #6459 census measured #6882), replacing the (schema as any) cast that preceded it. The shape is pinned by exact type equality, not extends, in packages/types/src/__tests__/data-table-declared-keys-6882.test.ts:

    type _RenderCellEditorShape = Expect[ Equal[
    Declared[DataTableSchema]['renderCellEditor'],
    ((ctx: CellEditorContext) => ReactNode) | undefined ] ];
    

    (angle brackets transcribed as square brackets — GitHub's body sanitizer eats
    tag-shaped fragments, including inside a fence. The source uses angle brackets.)

    with the stated reason: "Declaring the key with any other shape is a different (and false) statement about the renderer, so the shape is pinned, not just the membership."

packages/plugin-grid/src/ObjectGrid.tsx states the same fence in its own words: "⛔ Do not 'fix' either hold by declaring the key on DataTableSchema as a rider — that package is published surface with its own review floor."

⇒ B needs a seventh member on that declared context. That edits @object-ui/types, its Zod mirror, and a maintainer-ruled exact-shape pin. Per the ruling, A ships under all three of its conditions: labelled in code, this card not closed, and B filed (#7188).

⚠️A is not the conclusion. A field that can never be filled is a hard defect with no workaround; a field scoped by the persisted parent instead of the staged one is wrong only where the parent was edited in the same unsaved session. Strictly better, strictly not finished.

The defect

LookupField reads the record it gates on as dependentValues ?? ctx.formValues ?? ctx.data ?? {}. The grid's inline cell editor supplied none of the three — renderCellEditor rendered FieldEditWidget with field / value / onChange only, SchemaRendererContext has no formValues, and the grid sets no ctx.data per row. The record was therefore {} for every row, dependenciesMissing was permanently true, and a column declaring dependsOn rendered a disabled trigger reading "Select region first" even when the row carried the parent value.

PR #2216 was the two-half answer to #2215. Half 2 — every picker taking the dependsOn chain as a hard baseFilter — is host-independent and was already live on the grid path, which is why the gate fired at all (verified: A2.5 holds, see LookupField's dependentFilter memo). Half 1 is per-host and the grid never got it. This PR supplies that missing input and re-implements no cascade.

The change

One line of behaviour in ObjectGrid.renderCellEditordependentValues={ctx.row} — plus row: any added to the local ctx annotation, which had omitted it (the declared contract always carried row; only this local annotation did not). FieldEditWidget already forwards dependentValues to the widget, since #7008.

The interim is labelled in the code, naming this card and #7188, and stating precisely what is stale: a parent edited but not yet saved in the same row does not re-scope the child.

Evidence

Rendering proof, packages/plugin-grid/src/__tests__/gridDependentValues-7165.test.tsx (4 tests, each with a live control in the same render):

  1. the dependsOn column opens (was lookup-trigger-gated + disabled; now lookup-trigger-regional_owner + enabled) — control: a no-dependsOn column in the same render is also enabled, so the picker path is proven reached.
  2. the picker is scoped by the row: region: 'north' reaches the query as a hard $filter and Person 07 (south) is absent — control: the no-dependsOn sibling over the same records does offer Person 07, so the absence is scoping and not a dead picker.
  3. negative control: a row whose saved parent is empty still gates, so the gate was not simply disabled — control: the filled row in the same render opens.
  4. ⚠️the interim's staleness, pinned as current behaviour: staging a new parent without saving does not re-scope the child — with its own proof that the staging actually landed (the region cell renders the pending 'south' while the saved record still reads 'north'), because without that the assertion would pass for the trivial reason that nothing was ever staged. The grid's inline cell editor scopes a dependsOn lookup by the SAVED row, not the staged one — carry the pending record across the renderCellEditor seam (option B of #7165) #7188 flips this test.

The #7154 pin was UPDATED, not deleted. The dependsOn case in lookupPickerKeys-7154.test.tsx pinned the gated behaviour, i.e. it encoded the defect. A deleted pin is indistinguishable from a pin that never existed, so it is rewritten in place to assert the fixed behaviour plus the scoping, and its header prose is rewritten to say so.

Ablation — direction predicted before running: removing the single dependentValues={ctx.row} line turns red the 4 new tests plus the updated #7154 case, and leaves the other 4 #7154 cases green.

  • No rebuild required, and this is measured rather than assumed: the root vitest.config.mts aliases every @object-ui/* specifier to that package's src, and the tests import ../ObjectGrid relatively — so nothing resolves through dist and a source mutation is directly live.
  • Mutation confirmed on disk by marker count (dependentValues={ctx.row}: 1 → 0) and blob hash (e8ab332e…f219c865…), not by the editor's exit code.
  • Result: Tests 5 failed | 4 passed (9) — exactly the predicted set, passing count included.
  • Restore proven by state, with HEAD named explicitly (git checkout HEAD -- ..., never the bare form, which restores from the index): git diff HEAD, git diff --cached and git status --short all empty, and the blob hash back to e8ab332e…. The script carried a trap ... EXIT INT TERM with absolute paths.

Gates

⚠️Two heads, stated separately so the provenance is checkable. The full set ran at f17b08047; main then advanced (#7180 landed), so it was merged in and the affected-package suite plus the cheap ratchet gates were re-run at the current head aba0c7499. Nothing below is quoted from a tree that is no longer this branch's head.

Re-run at aba0c7499 (current head, post-merge):

gateresult
packages/plugin-grid/ full suite105 files, 949 tests passed
check:control-bytesOK — 5958 tracked text files scanned
check:phantom-depsOK
check:changeset-no-majorOK

Run at f17b08047 (same tree for every path this PR touches — the merge brought in only packages/components/src/renderers/layout/containers.tsx, a .changeset, a docs page, a components test and one vitest.config.mts line, none of which this PR edits):

gateresult
type-check (plugin-grid + closure)14 tasks successful
lint (affected package, full, not narrowed)2 tasks, 0 errors (751 pre-existing warnings)
check:self-importOK
check:vi-mock-specifiers / check:vi-mock-inheritOK (4106 files, 525 with mocks)
check-governed-queue-guard --testNOT GOVERNED — 4 paths vs 5 surfaces, none matched

The typecheck genuinely covers the new tests: tsc -p tsconfig.test.json --listFiles lists all three edited files among its 1770 (this package typechecks tests via a dedicated project, so "typecheck green" is not silently excluding them).

⚠️#7166 — the concurrent lane in this same package — had not landed as of aba0c7499. It touches relationalMetaKeys.ts and the applyRelationalMeta copy set; this PR touches ObjectGrid.renderCellEditor's context construction. Disjoint symbols, but this branch will need a fresh merge if #7166 lands first.

Incidence — measured, and ⛔ not "no impact"

Two corpora, each instrument run with a live control:

  • this repo: 3 files match dependsOn|depends_on, and none is this defect — the gantt hit is depends_on as task predecessor links (a different key entirely) and the other is a select inside a form. Control: the same instrument returns 88 files across packages/.
  • hotcrm (a real shipped app, anonymous shallow clone): 6 dependsOn declarations on lookup fields across 5 objects (crm_contract ×2, crm_case, crm_quote ×2, crm_opportunity), all scoping contacts to crm_account. But 0 editable grids in its views (control: the views directory exists and was read).

⇒ Neither measured corpus reaches the defect today, because none combines dependsOn with an editable grid — but hotcrm is one metadata key away: editable: true on any of those five objects' list views hits it immediately. Customer apps are unmeasured, and the parent pattern is demonstrably real in shipped metadata. This is a bounded reading, not an all-clear.

Filed, not ridden

⚠️ Still live, per the PM's fence and not settled by this PR: should the user be told the scope came from the saved row? The interim's staleness is the same silent-wrongness class as the three refusals this seat landed in #7146 / #7147 / #7148. It is recorded on #7188 rather than left to disappear into a code comment.


Session: session_012wwHa4aaFybxXrfmfHioDM (https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM) — kept in prose because a body edit rewrites the footer below to its bare form.

Generated by Claude Code

…lues
A `dependsOn` lookup column in an editable ObjectGrid was permanently
uneditable. `LookupField` resolves
`dependentValues ?? ctx.formValues ?? ctx.data ?? {}` and the grid's
`renderCellEditor` supplied none of the three, so the resolved record was
`{}` for every row, `dependenciesMissing` stayed `true`, and the trigger
rendered disabled ("Select region first") even when the row carried the
parent value.
PR #2216closed#2215 in two halves: the form renderer injects its live
watched record, and every picker takes the `dependsOn` chain as a hard
`baseFilter`. Half 2 is host-independent and was already live here; half 1
is per-host and the grid never got it. This supplies that missing input and
re-implements no cascade.
INTERIM: `ctx.row` is the saved record, so a parent edited but not yet
saved in the same row does not re-scope the child. Carrying the staged
record needs a seventh member on `renderCellEditor`'s published context
type (declared in `@object-ui/types`, #6882, and pinned by exact type
equality), which is a contract change tracked separately.
The `dependsOn` case in `lookupPickerKeys-7154.test.tsx` pinned the gated
behaviour and is UPDATED, not deleted.
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.6 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-C6RhwKZM.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)514.13KB117.19KB
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)206.14KB55.77KB
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

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.6 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-C6RhwKZM.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)514.13KB117.19KB
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)206.14KB55.77KB
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": 7165,
"status": "done",
"branch": "claude/issue-7165-grid-dependent-values",
"pr": "https://github.com/objectstack-ai/objectui/pull/7193",
"premise_still_valid": true,
"summary": "MEASUREMENT 1: B IS Clause 2-engaged — so option A shipped as the labelled interim and ⛔ #7165 MUST STAY OPEN (the PR says 'Part of #7165', never a closing keyword). The editor context is published surface on both disjuncts of the test: `DataTableSchema.renderCellEditor` is declared in packages/types/src/data-display.ts with a Zod mirror, declared by maintainer ruling 2026-08-30 (#6882) replacing a `(schema as any)` cast, and its exact shape is pinned by an `Equal` (not `extends`) type assertion in packages/types/src/__tests__/data-table-declared-keys-6882.test.ts whose comment states the shape is pinned deliberately; ObjectGrid.tsx independently fences that package as 'published surface with its own review floor'. B needs a seventh ctx member, so it edits @object-ui/types, its Zod mirror and that maintainer-ruled pin. All three ruling conditions are met: the interim is labelled in code naming #7165 and the follow-up and stating exactly what is stale, the card is not closed, and B is filed as #7188. Premise re-measured on head 899730e0a: the existing pin still passed before my change, i.e. the defect was still live. A2.1/A2.3/A2.4/A2.5 all confirmed; A2.2 needed one CORRECTION — the declared contract always carried `row`, but ObjectGrid's LOCAL ctx annotation omitted it, so A was a two-part edit (widen the local annotation, then pass it), not a literal one-liner.",
"tests": "All runs from repo root via the shared verify lock (objectui forbids `pnpm --filter PKG test`; path filters only). BASELINE on 899730e0a before any edit: `pnpm exec vitest run packages/plugin-grid/src/__tests__/lookupPickerKeys-7154.test.tsx` -> 'Test Files 1 passed (1) / Tests 5 passed (5)' — the gated pin still passed, so the defect was live on head. AFTER FIX, same command -> 'Tests 1 failed | 4 passed (5)', 'AssertionError: expected lookup-trigger-regional_owner to be lookup-trigger-gated' — the pin encoded the defect, so it was UPDATED IN PLACE (not deleted), header prose rewritten to say so. NEW FILE packages/plugin-grid/src/__tests__/gridDependentValues-7165.test.tsx, 4 tests each with a live control in the same render: (1) dependsOn column now opens, control column also opens; (2) picker SCOPED — $filter.region='north' reaches the query, Person 07 (south) absent, while the no-dependsOn control over the same records DOES offer Person 07; (3) NEGATIVE CONTROL — a row whose saved parent is empty still gates, proving the gate was not disabled; (4) the interim's staleness pinned as current behaviour, with its own proof the staging landed (region cell renders pending 'south' while the saved record reads 'north'), which is what stops it passing vacuously. Both files together: 'Test Files 2 passed (2) / Tests 9 passed (9)'. ABLATION — direction predicted BEFORE running (remove `dependentValues={ctx.row}` -> the 4 new tests plus the updated 7154 case go red, the other 4 stay green). NO REBUILD NEEDED, and that is measured not assumed: root vitest.config.mts aliases every @object-ui/* specifier to that package's src and the tests import ../ObjectGrid relatively, so nothing resolves through dist and a source mutation is directly live. Mutation confirmed ON DISK by marker count (1 -> 0) AND blob hash (e8ab332e… -> f219c865…), never by the editing tool's exit code. Result: 'Tests 5 failed | 4 passed (9)' — exactly the predicted set, passing count included. Restore proven BY STATE with HEAD named explicitly (`git checkout HEAD -- ABSPATH`, never the bare form which restores from the index): git diff HEAD, git diff --cached and git status --short all empty and the blob hash back to e8ab332e…; the script carried `trap ... EXIT INT TERM` with absolute paths. GATES: at final head aba0c7499 (after merging main, which had advanced to 8ec11e14f / #7180) — full packages/plugin-grid/ suite 'Test Files 105 passed (105) / Tests 949 passed (949)', check:control-bytes OK (5958 files), check:phantom-deps OK, check:changeset-no-major OK. At f17b08047 (identical tree for every path this PR touches): type-check plugin-grid + closure 14 tasks successful, lint of the affected package in FULL (not narrowed) 2 tasks 0 errors (751 pre-existing warnings), check:self-import OK, check:vi-mock-specifiers / check:vi-mock-inherit OK (4106 files, 525 with mocks), check-governed-queue-guard --test = NOT GOVERNED (4 paths vs 5 surfaces). Verdicts read from each gate's own printed line, with exit codes captured after redirection, never through a pipe. NOT-MEASURED check cleared: `tsc -p tsconfig.test.json --listFiles` lists all three edited files among its 1770, so the green typecheck genuinely covers the new tests rather than excluding them.",
"mcp_calls": "11 — 2 issue reads, 2 targeted search_issues (each with a control word that hit: #7165 itself, then #7154/#2215), 2 issue creates, 1 PR create, 1 PR body read-back, 1 PR body update, 2 report comments. REST was 403 for repo-scoped reads and `gh` is absent, both measured with a control before switching channels.",
"open_questions": [
{
"question": "Should the grid TELL the user that a dependsOn picker is scoped by the SAVED parent while an unsaved edit to that parent is staged in the same row? The PM's ruling explicitly left this live rather than settled, and asked that it not disappear into a code comment.",
"options": [
"A. Say nothing (what this PR ships). The picker silently uses the persisted parent. Zero new surface, but it is the same silent-wrongness class as the three refusals this seat landed in #7146 / #7147 / #7148.",
"B. Surface a hint on the trigger/popover when the parent has a staged-but-unsaved value that differs from the saved one — e.g. 'scoped by the saved Region; save the row to re-scope'. Needs the staged record, i.e. it is gated behind exactly the same seam change as option B of this card.",
"C. Do not build a message at all: land #7188, which removes the staleness and therefore removes the question."
],
"recommendation": "C, and treat B as dead. Real business need: the staleness only exists because the interim exists, so a message would be UI built to describe a defect we have already scheduled to delete. Long-term soundness: contract-first — the honest fix is the declared seam member, not a renderer-side apology for a wrong input, and a warning would fossilize the interim as if it were a supported mode. Making AI-authored metadata hard to get wrong: an author declaring dependsOn should get correct cascading, not a caption explaining why it is stale — a warning trains authors to accept a wrong scope. Startup scope discipline: option B costs the SAME seam change as #7188 while delivering strictly less, which is the worst trade of the three. ⚠️ The one thing that would flip this to B: if #7188 is declined or deferred long, the silent staleness stops being temporary and then it must be surfaced. So this question should be closed by #7188 landing, not by a ruling that silence is acceptable."
}
],
"out_of_scope_findings": [
"filed as #7188: option B — carry the pending/staged record across the renderCellEditor seam so a dependsOn lookup is scoped by the in-flight parent (the seam change is spelled out: @object-ui/types ctx member + Zod mirror + the #6882 exact-shape pin + data-table.tsx passing `{ ...row, ...rowChanges }` + ObjectGrid reading it). Clause 2-engaged; #7165 stays open for it.",
"filed as #7190 (label `finding`): plugin-detail's InlineFieldInput supplies no dependentValues either (measured: zero non-test occurrences; control: 88 files across packages/), and plugin-detail never provides SchemaRendererContext `data` itself. ⚠️ Deliberately NOT graded a bug — a detail page renders ONE record, so ctx.data may legitimately rescue the cascade there, and no rendering measurement was made. The single probe that settles it is written into the card.",
"NOT filed, reported here as an observation for PM triage: ObjectGrid.tsx still lists `renderCellEditor` in its `ObjectGridDataTableSchemaHolds` type as an undeclared-but-live HELD key, with prose saying whether DataTableSchema should declare it is a pending packages/types ruling. That ruling HAPPENED — #6882 declared it on 2026-08-30 — so the hold entry and its surrounding census comment are stale. Harmless today (the intersection with the declared member is identical), but it is documentation that now states the opposite of the tree. Left untouched because it is outside this card and inside the file #7166 is adjacent to."
]
}

Generated by Claude Code


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. The open question is ruled below.

⭐ The pre-flight scan that saved the card — and a trap nobody had written down

a first draft of the PR body contained the phrase "does NOT close #7165", which a pre-flight scan caught — GitHub's parser ignores negation and it would have closed the card on merge.

GitHub's closing-keyword parser does not read negation."does NOT close #7165" contains close #7165, and on merge it closes the card. This PR's entire correctness depends on #7165 staying open — A is an interim, B is the real fix — so the most natural sentence a careful author would write to protect the card is the one that would have destroyed it.

⚠️ And note the shape: the failure would have been silent and delayed. The PR merges, the card closes, and the interim becomes permanent because nothing is left tracking B. Carrying this to #6970 as a fourth mangling class, since that card enumerates exactly this family. The safe form is what shipped: Part of #7165, and never the keyword at all.

Measurement 1 answered decisively, on both disjuncts

B is Clause ②-engaged, and it was established rather than assumed:

⇒ B needs a seventh ctx member, so it edits @object-ui/types, its Zod mirror, and a maintainer-ruled exact-shape pin. That is Clause ② three times over. ⭐ The Equal-not-extends detail is what makes it airtight — an extends pin would have tolerated an added member, and the answer would have been different.

All three of my ruling conditions are met: the interim is labelled in code naming #7165 and #7188 and stating exactly what is stale; the card is not closed; B is filed.

A2.2 corrected, and the correction matters

I said A was "one line: dependentValues: ctx.row". Wrong: the declared contract always carried row, but ObjectGrid's local ctx annotation omitted it — so A was a two-part edit (widen the local annotation, then pass it). ⚠️ A declared member that a local annotation silently narrows is its own small hazard: the type says the data is there and the call site cannot see it.

The test discipline is the strongest part

  • Baseline before any edit: the existing bug(plugin-grid): ObjectGrid's inline lookup picker ignores four more spec-declared field props — multiple, allowCreate, lookupPageSize, dependsOn are read on its own path and never copied #7154 pin still passed on head — so the defect was live, not already fixed by something else. Establishing that first is what makes the rest meaningful.
  • The pin encoded the defect and was UPDATED IN PLACE, not deleted, with its header prose rewritten to say so. A deleted pin is indistinguishable from a pin that never existed.
  • A negative control: a row whose saved parent is empty still gates — proving the fix supplied a value rather than disabling the gate. That is the assertion that separates "cascade works" from "cascade removed", and it is the one most likely to be skipped.
  • The staleness pin carries its own proof it is not vacuous: the region cell renders the pending south while the saved record reads north, so the test proves the staging actually landed before asserting the picker ignored it. A staleness test without that proof passes for the wrong reason.
  • Ablation predicted before running, Tests 5 failed | 4 passed (9) exactly; mutation by marker count and blob hash; restore by state with git checkout HEAD -- ABSPATHnever the bare form, which restores from the index.

⚖️ Ruling on the open question: C — close it by landing #7188, not by ruling that silence is acceptable

I left this live at dispatch and asked that it not disappear into a code comment. The answer is C, and the argument that decides it is the one I did not have when I asked:

Option B costs the same seam change as #7188 while delivering strictly less. A warning that says "scoped by the saved Region" needs the staged record — i.e. exactly the ctx member #7188 adds. Having paid that price, you would ship a caption explaining the wrong scope instead of the right scope. That is the worst of the three trades.

And a warning would fossilise the interim as a supported mode. An author who declares dependsOn should get correct cascading, not a caption explaining why it is stale — a message trains authors to accept a wrong scope as normal.

⚠️I am adopting the flip condition verbatim, because it is what keeps C honest: if #7188 is declined, or deferred long enough that the interim stops being temporary, the silent staleness must be surfaced. So this question is closed by #7188 landing, not by a ruling that silence is fine. Recorded on #7165.

Incidence: carried, not closed — exactly as the dispatch required

Two corpora, each with a live control:

  • this repo: 3 files match dependsOn|depends_on, none is this defect (the gantt hit is depends_on as task predecessor links — a different key entirely); control: 88 files across packages/.
  • hotcrm: 6 dependsOn declarations on lookup fields across 5 objects, all scoping contacts to crm_account — but 0 editable grids in its views (control: the views dir exists and was read).

⇒ ⭐ "Neither measured corpus reaches the defect today, but hotcrm is ONE metadata key away"editable: true on any of those list views. That is a far more useful answer than either "no impact" or "unmeasured", and it is the sentence a maintainer needs to prioritise #7188. Customer apps remain unmeasured, and that is stated rather than glossed.

#7190 filed with the right restraint

InlineFieldInput supplies no dependentValues either (measured: zero non-test occurrences; control: 88 files) — and it is deliberately NOT graded a bug, because a detail page renders one record so ctx.data may legitimately rescue the cascade there, and no rendering measurement was made. The single probe that would settle it is written into the card. Declining to grade on a structural analogy, and leaving the probe behind, is better than a confident grade either way.


Nothing to change.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 10:48
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 84ffdbcSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7165-grid-dependent-values branch September 1, 2026 11:02
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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): feed the inline cell editor the row as dependent values, so a dependsOn lookup column is no longer permanently uneditable - #7193

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-7165-grid-dependent-values
Sep 1, 2026
Merged

fix(plugin-grid): feed the inline cell editor the row as dependent values, so a dependsOn lookup column is no longer permanently uneditable#7193
os-warren merged 3 commits into
mainfrom
claude/issue-7165-grid-dependent-values

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Part of #7165

#7165 must stay open. This ships option A, the interim, and is deliberately not a closing reference to that card — the card stays open for option B, filed as #7188. See "Measurement 1" below — that is the fact that determined this.

Measurement 1 — B is Clause ②-engaged, so A ships as the labelled interim

The PM ruling on #7165 made the A/B choice depend on one measurement: is renderCellEditor's editor context type published surface, or internal to plugin-grid? Measured on 899730e0a, both disjuncts of that test are true:

  • Reachable from @object-ui/typespackages/types/src/data-display.ts declares renderCellEditor?: (ctx: { column; row; value; stage; commit; cancel }) => React.ReactNode on DataTableSchema, with a Zod mirror in packages/types/src/zod/data-display.zod.ts.

  • Part of DataTableSchema's declared editor-context contract — declared by maintainer ruling 2026-08-30 ([Decision] Declare renderCellEditor and schema-level cellClassName on DataTableSchema? — the two live undeclared keys the #6459 census measured #6882), replacing the (schema as any) cast that preceded it. The shape is pinned by exact type equality, not extends, in packages/types/src/__tests__/data-table-declared-keys-6882.test.ts:

    type _RenderCellEditorShape = Expect[ Equal[
    Declared[DataTableSchema]['renderCellEditor'],
    ((ctx: CellEditorContext) => ReactNode) | undefined ] ];
    

    (angle brackets transcribed as square brackets — GitHub's body sanitizer eats
    tag-shaped fragments, including inside a fence. The source uses angle brackets.)

    with the stated reason: "Declaring the key with any other shape is a different (and false) statement about the renderer, so the shape is pinned, not just the membership."

packages/plugin-grid/src/ObjectGrid.tsx states the same fence in its own words: "⛔ Do not 'fix' either hold by declaring the key on DataTableSchema as a rider — that package is published surface with its own review floor."

⇒ B needs a seventh member on that declared context. That edits @object-ui/types, its Zod mirror, and a maintainer-ruled exact-shape pin. Per the ruling, A ships under all three of its conditions: labelled in code, this card not closed, and B filed (#7188).

⚠️A is not the conclusion. A field that can never be filled is a hard defect with no workaround; a field scoped by the persisted parent instead of the staged one is wrong only where the parent was edited in the same unsaved session. Strictly better, strictly not finished.

The defect

LookupField reads the record it gates on as dependentValues ?? ctx.formValues ?? ctx.data ?? {}. The grid's inline cell editor supplied none of the three — renderCellEditor rendered FieldEditWidget with field / value / onChange only, SchemaRendererContext has no formValues, and the grid sets no ctx.data per row. The record was therefore {} for every row, dependenciesMissing was permanently true, and a column declaring dependsOn rendered a disabled trigger reading "Select region first" even when the row carried the parent value.

PR #2216 was the two-half answer to #2215. Half 2 — every picker taking the dependsOn chain as a hard baseFilter — is host-independent and was already live on the grid path, which is why the gate fired at all (verified: A2.5 holds, see LookupField's dependentFilter memo). Half 1 is per-host and the grid never got it. This PR supplies that missing input and re-implements no cascade.

The change

One line of behaviour in ObjectGrid.renderCellEditordependentValues={ctx.row} — plus row: any added to the local ctx annotation, which had omitted it (the declared contract always carried row; only this local annotation did not). FieldEditWidget already forwards dependentValues to the widget, since #7008.

The interim is labelled in the code, naming this card and #7188, and stating precisely what is stale: a parent edited but not yet saved in the same row does not re-scope the child.

Evidence

Rendering proof, packages/plugin-grid/src/__tests__/gridDependentValues-7165.test.tsx (4 tests, each with a live control in the same render):

  1. the dependsOn column opens (was lookup-trigger-gated + disabled; now lookup-trigger-regional_owner + enabled) — control: a no-dependsOn column in the same render is also enabled, so the picker path is proven reached.
  2. the picker is scoped by the row: region: 'north' reaches the query as a hard $filter and Person 07 (south) is absent — control: the no-dependsOn sibling over the same records does offer Person 07, so the absence is scoping and not a dead picker.
  3. negative control: a row whose saved parent is empty still gates, so the gate was not simply disabled — control: the filled row in the same render opens.
  4. ⚠️the interim's staleness, pinned as current behaviour: staging a new parent without saving does not re-scope the child — with its own proof that the staging actually landed (the region cell renders the pending 'south' while the saved record still reads 'north'), because without that the assertion would pass for the trivial reason that nothing was ever staged. The grid's inline cell editor scopes a dependsOn lookup by the SAVED row, not the staged one — carry the pending record across the renderCellEditor seam (option B of #7165) #7188 flips this test.

The #7154 pin was UPDATED, not deleted. The dependsOn case in lookupPickerKeys-7154.test.tsx pinned the gated behaviour, i.e. it encoded the defect. A deleted pin is indistinguishable from a pin that never existed, so it is rewritten in place to assert the fixed behaviour plus the scoping, and its header prose is rewritten to say so.

Ablation — direction predicted before running: removing the single dependentValues={ctx.row} line turns red the 4 new tests plus the updated #7154 case, and leaves the other 4 #7154 cases green.

  • No rebuild required, and this is measured rather than assumed: the root vitest.config.mts aliases every @object-ui/* specifier to that package's src, and the tests import ../ObjectGrid relatively — so nothing resolves through dist and a source mutation is directly live.
  • Mutation confirmed on disk by marker count (dependentValues={ctx.row}: 1 → 0) and blob hash (e8ab332e…f219c865…), not by the editor's exit code.
  • Result: Tests 5 failed | 4 passed (9) — exactly the predicted set, passing count included.
  • Restore proven by state, with HEAD named explicitly (git checkout HEAD -- ..., never the bare form, which restores from the index): git diff HEAD, git diff --cached and git status --short all empty, and the blob hash back to e8ab332e…. The script carried a trap ... EXIT INT TERM with absolute paths.

Gates

⚠️Two heads, stated separately so the provenance is checkable. The full set ran at f17b08047; main then advanced (#7180 landed), so it was merged in and the affected-package suite plus the cheap ratchet gates were re-run at the current head aba0c7499. Nothing below is quoted from a tree that is no longer this branch's head.

Re-run at aba0c7499 (current head, post-merge):

gateresult
packages/plugin-grid/ full suite105 files, 949 tests passed
check:control-bytesOK — 5958 tracked text files scanned
check:phantom-depsOK
check:changeset-no-majorOK

Run at f17b08047 (same tree for every path this PR touches — the merge brought in only packages/components/src/renderers/layout/containers.tsx, a .changeset, a docs page, a components test and one vitest.config.mts line, none of which this PR edits):

gateresult
type-check (plugin-grid + closure)14 tasks successful
lint (affected package, full, not narrowed)2 tasks, 0 errors (751 pre-existing warnings)
check:self-importOK
check:vi-mock-specifiers / check:vi-mock-inheritOK (4106 files, 525 with mocks)
check-governed-queue-guard --testNOT GOVERNED — 4 paths vs 5 surfaces, none matched

The typecheck genuinely covers the new tests: tsc -p tsconfig.test.json --listFiles lists all three edited files among its 1770 (this package typechecks tests via a dedicated project, so "typecheck green" is not silently excluding them).

⚠️#7166 — the concurrent lane in this same package — had not landed as of aba0c7499. It touches relationalMetaKeys.ts and the applyRelationalMeta copy set; this PR touches ObjectGrid.renderCellEditor's context construction. Disjoint symbols, but this branch will need a fresh merge if #7166 lands first.

Incidence — measured, and ⛔ not "no impact"

Two corpora, each instrument run with a live control:

  • this repo: 3 files match dependsOn|depends_on, and none is this defect — the gantt hit is depends_on as task predecessor links (a different key entirely) and the other is a select inside a form. Control: the same instrument returns 88 files across packages/.
  • hotcrm (a real shipped app, anonymous shallow clone): 6 dependsOn declarations on lookup fields across 5 objects (crm_contract ×2, crm_case, crm_quote ×2, crm_opportunity), all scoping contacts to crm_account. But 0 editable grids in its views (control: the views directory exists and was read).

⇒ Neither measured corpus reaches the defect today, because none combines dependsOn with an editable grid — but hotcrm is one metadata key away: editable: true on any of those five objects' list views hits it immediately. Customer apps are unmeasured, and the parent pattern is demonstrably real in shipped metadata. This is a bounded reading, not an all-clear.

Filed, not ridden

⚠️ Still live, per the PM's fence and not settled by this PR: should the user be told the scope came from the saved row? The interim's staleness is the same silent-wrongness class as the three refusals this seat landed in #7146 / #7147 / #7148. It is recorded on #7188 rather than left to disappear into a code comment.


Session: session_012wwHa4aaFybxXrfmfHioDM (https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM) — kept in prose because a body edit rewrites the footer below to its bare form.

Generated by Claude Code

…lues
A `dependsOn` lookup column in an editable ObjectGrid was permanently
uneditable. `LookupField` resolves
`dependentValues ?? ctx.formValues ?? ctx.data ?? {}` and the grid's
`renderCellEditor` supplied none of the three, so the resolved record was
`{}` for every row, `dependenciesMissing` stayed `true`, and the trigger
rendered disabled ("Select region first") even when the row carried the
parent value.
PR #2216closed#2215 in two halves: the form renderer injects its live
watched record, and every picker takes the `dependsOn` chain as a hard
`baseFilter`. Half 2 is host-independent and was already live here; half 1
is per-host and the grid never got it. This supplies that missing input and
re-implements no cascade.
INTERIM: `ctx.row` is the saved record, so a parent edited but not yet
saved in the same row does not re-scope the child. Carrying the staged
record needs a seventh member on `renderCellEditor`'s published context
type (declared in `@object-ui/types`, #6882, and pinned by exact type
equality), which is a contract change tracked separately.
The `dependsOn` case in `lookupPickerKeys-7154.test.tsx` pinned the gated
behaviour and is UPDATED, not deleted.
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.6 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-C6RhwKZM.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)514.13KB117.19KB
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)206.14KB55.77KB
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

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.6 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-C6RhwKZM.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)514.13KB117.19KB
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)206.14KB55.77KB
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": 7165,
"status": "done",
"branch": "claude/issue-7165-grid-dependent-values",
"pr": "https://github.com/objectstack-ai/objectui/pull/7193",
"premise_still_valid": true,
"summary": "MEASUREMENT 1: B IS Clause 2-engaged — so option A shipped as the labelled interim and ⛔ #7165 MUST STAY OPEN (the PR says 'Part of #7165', never a closing keyword). The editor context is published surface on both disjuncts of the test: `DataTableSchema.renderCellEditor` is declared in packages/types/src/data-display.ts with a Zod mirror, declared by maintainer ruling 2026-08-30 (#6882) replacing a `(schema as any)` cast, and its exact shape is pinned by an `Equal` (not `extends`) type assertion in packages/types/src/__tests__/data-table-declared-keys-6882.test.ts whose comment states the shape is pinned deliberately; ObjectGrid.tsx independently fences that package as 'published surface with its own review floor'. B needs a seventh ctx member, so it edits @object-ui/types, its Zod mirror and that maintainer-ruled pin. All three ruling conditions are met: the interim is labelled in code naming #7165 and the follow-up and stating exactly what is stale, the card is not closed, and B is filed as #7188. Premise re-measured on head 899730e0a: the existing pin still passed before my change, i.e. the defect was still live. A2.1/A2.3/A2.4/A2.5 all confirmed; A2.2 needed one CORRECTION — the declared contract always carried `row`, but ObjectGrid's LOCAL ctx annotation omitted it, so A was a two-part edit (widen the local annotation, then pass it), not a literal one-liner.",
"tests": "All runs from repo root via the shared verify lock (objectui forbids `pnpm --filter PKG test`; path filters only). BASELINE on 899730e0a before any edit: `pnpm exec vitest run packages/plugin-grid/src/__tests__/lookupPickerKeys-7154.test.tsx` -> 'Test Files 1 passed (1) / Tests 5 passed (5)' — the gated pin still passed, so the defect was live on head. AFTER FIX, same command -> 'Tests 1 failed | 4 passed (5)', 'AssertionError: expected lookup-trigger-regional_owner to be lookup-trigger-gated' — the pin encoded the defect, so it was UPDATED IN PLACE (not deleted), header prose rewritten to say so. NEW FILE packages/plugin-grid/src/__tests__/gridDependentValues-7165.test.tsx, 4 tests each with a live control in the same render: (1) dependsOn column now opens, control column also opens; (2) picker SCOPED — $filter.region='north' reaches the query, Person 07 (south) absent, while the no-dependsOn control over the same records DOES offer Person 07; (3) NEGATIVE CONTROL — a row whose saved parent is empty still gates, proving the gate was not disabled; (4) the interim's staleness pinned as current behaviour, with its own proof the staging landed (region cell renders pending 'south' while the saved record reads 'north'), which is what stops it passing vacuously. Both files together: 'Test Files 2 passed (2) / Tests 9 passed (9)'. ABLATION — direction predicted BEFORE running (remove `dependentValues={ctx.row}` -> the 4 new tests plus the updated 7154 case go red, the other 4 stay green). NO REBUILD NEEDED, and that is measured not assumed: root vitest.config.mts aliases every @object-ui/* specifier to that package's src and the tests import ../ObjectGrid relatively, so nothing resolves through dist and a source mutation is directly live. Mutation confirmed ON DISK by marker count (1 -> 0) AND blob hash (e8ab332e… -> f219c865…), never by the editing tool's exit code. Result: 'Tests 5 failed | 4 passed (9)' — exactly the predicted set, passing count included. Restore proven BY STATE with HEAD named explicitly (`git checkout HEAD -- ABSPATH`, never the bare form which restores from the index): git diff HEAD, git diff --cached and git status --short all empty and the blob hash back to e8ab332e…; the script carried `trap ... EXIT INT TERM` with absolute paths. GATES: at final head aba0c7499 (after merging main, which had advanced to 8ec11e14f / #7180) — full packages/plugin-grid/ suite 'Test Files 105 passed (105) / Tests 949 passed (949)', check:control-bytes OK (5958 files), check:phantom-deps OK, check:changeset-no-major OK. At f17b08047 (identical tree for every path this PR touches): type-check plugin-grid + closure 14 tasks successful, lint of the affected package in FULL (not narrowed) 2 tasks 0 errors (751 pre-existing warnings), check:self-import OK, check:vi-mock-specifiers / check:vi-mock-inherit OK (4106 files, 525 with mocks), check-governed-queue-guard --test = NOT GOVERNED (4 paths vs 5 surfaces). Verdicts read from each gate's own printed line, with exit codes captured after redirection, never through a pipe. NOT-MEASURED check cleared: `tsc -p tsconfig.test.json --listFiles` lists all three edited files among its 1770, so the green typecheck genuinely covers the new tests rather than excluding them.",
"mcp_calls": "11 — 2 issue reads, 2 targeted search_issues (each with a control word that hit: #7165 itself, then #7154/#2215), 2 issue creates, 1 PR create, 1 PR body read-back, 1 PR body update, 2 report comments. REST was 403 for repo-scoped reads and `gh` is absent, both measured with a control before switching channels.",
"open_questions": [
{
"question": "Should the grid TELL the user that a dependsOn picker is scoped by the SAVED parent while an unsaved edit to that parent is staged in the same row? The PM's ruling explicitly left this live rather than settled, and asked that it not disappear into a code comment.",
"options": [
"A. Say nothing (what this PR ships). The picker silently uses the persisted parent. Zero new surface, but it is the same silent-wrongness class as the three refusals this seat landed in #7146 / #7147 / #7148.",
"B. Surface a hint on the trigger/popover when the parent has a staged-but-unsaved value that differs from the saved one — e.g. 'scoped by the saved Region; save the row to re-scope'. Needs the staged record, i.e. it is gated behind exactly the same seam change as option B of this card.",
"C. Do not build a message at all: land #7188, which removes the staleness and therefore removes the question."
],
"recommendation": "C, and treat B as dead. Real business need: the staleness only exists because the interim exists, so a message would be UI built to describe a defect we have already scheduled to delete. Long-term soundness: contract-first — the honest fix is the declared seam member, not a renderer-side apology for a wrong input, and a warning would fossilize the interim as if it were a supported mode. Making AI-authored metadata hard to get wrong: an author declaring dependsOn should get correct cascading, not a caption explaining why it is stale — a warning trains authors to accept a wrong scope. Startup scope discipline: option B costs the SAME seam change as #7188 while delivering strictly less, which is the worst trade of the three. ⚠️ The one thing that would flip this to B: if #7188 is declined or deferred long, the silent staleness stops being temporary and then it must be surfaced. So this question should be closed by #7188 landing, not by a ruling that silence is acceptable."
}
],
"out_of_scope_findings": [
"filed as #7188: option B — carry the pending/staged record across the renderCellEditor seam so a dependsOn lookup is scoped by the in-flight parent (the seam change is spelled out: @object-ui/types ctx member + Zod mirror + the #6882 exact-shape pin + data-table.tsx passing `{ ...row, ...rowChanges }` + ObjectGrid reading it). Clause 2-engaged; #7165 stays open for it.",
"filed as #7190 (label `finding`): plugin-detail's InlineFieldInput supplies no dependentValues either (measured: zero non-test occurrences; control: 88 files across packages/), and plugin-detail never provides SchemaRendererContext `data` itself. ⚠️ Deliberately NOT graded a bug — a detail page renders ONE record, so ctx.data may legitimately rescue the cascade there, and no rendering measurement was made. The single probe that settles it is written into the card.",
"NOT filed, reported here as an observation for PM triage: ObjectGrid.tsx still lists `renderCellEditor` in its `ObjectGridDataTableSchemaHolds` type as an undeclared-but-live HELD key, with prose saying whether DataTableSchema should declare it is a pending packages/types ruling. That ruling HAPPENED — #6882 declared it on 2026-08-30 — so the hold entry and its surrounding census comment are stale. Harmless today (the intersection with the declared member is identical), but it is documentation that now states the opposite of the tree. Left untouched because it is outside this card and inside the file #7166 is adjacent to."
]
}

Generated by Claude Code


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. The open question is ruled below.

⭐ The pre-flight scan that saved the card — and a trap nobody had written down

a first draft of the PR body contained the phrase "does NOT close #7165", which a pre-flight scan caught — GitHub's parser ignores negation and it would have closed the card on merge.

GitHub's closing-keyword parser does not read negation."does NOT close #7165" contains close #7165, and on merge it closes the card. This PR's entire correctness depends on #7165 staying open — A is an interim, B is the real fix — so the most natural sentence a careful author would write to protect the card is the one that would have destroyed it.

⚠️ And note the shape: the failure would have been silent and delayed. The PR merges, the card closes, and the interim becomes permanent because nothing is left tracking B. Carrying this to #6970 as a fourth mangling class, since that card enumerates exactly this family. The safe form is what shipped: Part of #7165, and never the keyword at all.

Measurement 1 answered decisively, on both disjuncts

B is Clause ②-engaged, and it was established rather than assumed:

⇒ B needs a seventh ctx member, so it edits @object-ui/types, its Zod mirror, and a maintainer-ruled exact-shape pin. That is Clause ② three times over. ⭐ The Equal-not-extends detail is what makes it airtight — an extends pin would have tolerated an added member, and the answer would have been different.

All three of my ruling conditions are met: the interim is labelled in code naming #7165 and #7188 and stating exactly what is stale; the card is not closed; B is filed.

A2.2 corrected, and the correction matters

I said A was "one line: dependentValues: ctx.row". Wrong: the declared contract always carried row, but ObjectGrid's local ctx annotation omitted it — so A was a two-part edit (widen the local annotation, then pass it). ⚠️ A declared member that a local annotation silently narrows is its own small hazard: the type says the data is there and the call site cannot see it.

The test discipline is the strongest part

  • Baseline before any edit: the existing bug(plugin-grid): ObjectGrid's inline lookup picker ignores four more spec-declared field props — multiple, allowCreate, lookupPageSize, dependsOn are read on its own path and never copied #7154 pin still passed on head — so the defect was live, not already fixed by something else. Establishing that first is what makes the rest meaningful.
  • The pin encoded the defect and was UPDATED IN PLACE, not deleted, with its header prose rewritten to say so. A deleted pin is indistinguishable from a pin that never existed.
  • A negative control: a row whose saved parent is empty still gates — proving the fix supplied a value rather than disabling the gate. That is the assertion that separates "cascade works" from "cascade removed", and it is the one most likely to be skipped.
  • The staleness pin carries its own proof it is not vacuous: the region cell renders the pending south while the saved record reads north, so the test proves the staging actually landed before asserting the picker ignored it. A staleness test without that proof passes for the wrong reason.
  • Ablation predicted before running, Tests 5 failed | 4 passed (9) exactly; mutation by marker count and blob hash; restore by state with git checkout HEAD -- ABSPATHnever the bare form, which restores from the index.

⚖️ Ruling on the open question: C — close it by landing #7188, not by ruling that silence is acceptable

I left this live at dispatch and asked that it not disappear into a code comment. The answer is C, and the argument that decides it is the one I did not have when I asked:

Option B costs the same seam change as #7188 while delivering strictly less. A warning that says "scoped by the saved Region" needs the staged record — i.e. exactly the ctx member #7188 adds. Having paid that price, you would ship a caption explaining the wrong scope instead of the right scope. That is the worst of the three trades.

And a warning would fossilise the interim as a supported mode. An author who declares dependsOn should get correct cascading, not a caption explaining why it is stale — a message trains authors to accept a wrong scope as normal.

⚠️I am adopting the flip condition verbatim, because it is what keeps C honest: if #7188 is declined, or deferred long enough that the interim stops being temporary, the silent staleness must be surfaced. So this question is closed by #7188 landing, not by a ruling that silence is fine. Recorded on #7165.

Incidence: carried, not closed — exactly as the dispatch required

Two corpora, each with a live control:

  • this repo: 3 files match dependsOn|depends_on, none is this defect (the gantt hit is depends_on as task predecessor links — a different key entirely); control: 88 files across packages/.
  • hotcrm: 6 dependsOn declarations on lookup fields across 5 objects, all scoping contacts to crm_account — but 0 editable grids in its views (control: the views dir exists and was read).

⇒ ⭐ "Neither measured corpus reaches the defect today, but hotcrm is ONE metadata key away"editable: true on any of those list views. That is a far more useful answer than either "no impact" or "unmeasured", and it is the sentence a maintainer needs to prioritise #7188. Customer apps remain unmeasured, and that is stated rather than glossed.

#7190 filed with the right restraint

InlineFieldInput supplies no dependentValues either (measured: zero non-test occurrences; control: 88 files) — and it is deliberately NOT graded a bug, because a detail page renders one record so ctx.data may legitimately rescue the cascade there, and no rendering measurement was made. The single probe that would settle it is written into the card. Declining to grade on a structural analogy, and leaving the probe behind, is better than a confident grade either way.


Nothing to change.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 10:48
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 84ffdbcSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7165-grid-dependent-values branch September 1, 2026 11:02
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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): feed the inline cell editor the row as dependent values, so a dependsOn lookup column is no longer permanently uneditable - #7193

Merged
os-warren merged 3 commits into
mainfrom
claude/issue-7165-grid-dependent-values
Sep 1, 2026
Merged

fix(plugin-grid): feed the inline cell editor the row as dependent values, so a dependsOn lookup column is no longer permanently uneditable#7193
os-warren merged 3 commits into
mainfrom
claude/issue-7165-grid-dependent-values

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Part of #7165

#7165 must stay open. This ships option A, the interim, and is deliberately not a closing reference to that card — the card stays open for option B, filed as #7188. See "Measurement 1" below — that is the fact that determined this.

Measurement 1 — B is Clause ②-engaged, so A ships as the labelled interim

The PM ruling on #7165 made the A/B choice depend on one measurement: is renderCellEditor's editor context type published surface, or internal to plugin-grid? Measured on 899730e0a, both disjuncts of that test are true:

  • Reachable from @object-ui/typespackages/types/src/data-display.ts declares renderCellEditor?: (ctx: { column; row; value; stage; commit; cancel }) => React.ReactNode on DataTableSchema, with a Zod mirror in packages/types/src/zod/data-display.zod.ts.

  • Part of DataTableSchema's declared editor-context contract — declared by maintainer ruling 2026-08-30 ([Decision] Declare renderCellEditor and schema-level cellClassName on DataTableSchema? — the two live undeclared keys the #6459 census measured #6882), replacing the (schema as any) cast that preceded it. The shape is pinned by exact type equality, not extends, in packages/types/src/__tests__/data-table-declared-keys-6882.test.ts:

    type _RenderCellEditorShape = Expect[ Equal[
    Declared[DataTableSchema]['renderCellEditor'],
    ((ctx: CellEditorContext) => ReactNode) | undefined ] ];
    

    (angle brackets transcribed as square brackets — GitHub's body sanitizer eats
    tag-shaped fragments, including inside a fence. The source uses angle brackets.)

    with the stated reason: "Declaring the key with any other shape is a different (and false) statement about the renderer, so the shape is pinned, not just the membership."

packages/plugin-grid/src/ObjectGrid.tsx states the same fence in its own words: "⛔ Do not 'fix' either hold by declaring the key on DataTableSchema as a rider — that package is published surface with its own review floor."

⇒ B needs a seventh member on that declared context. That edits @object-ui/types, its Zod mirror, and a maintainer-ruled exact-shape pin. Per the ruling, A ships under all three of its conditions: labelled in code, this card not closed, and B filed (#7188).

⚠️A is not the conclusion. A field that can never be filled is a hard defect with no workaround; a field scoped by the persisted parent instead of the staged one is wrong only where the parent was edited in the same unsaved session. Strictly better, strictly not finished.

The defect

LookupField reads the record it gates on as dependentValues ?? ctx.formValues ?? ctx.data ?? {}. The grid's inline cell editor supplied none of the three — renderCellEditor rendered FieldEditWidget with field / value / onChange only, SchemaRendererContext has no formValues, and the grid sets no ctx.data per row. The record was therefore {} for every row, dependenciesMissing was permanently true, and a column declaring dependsOn rendered a disabled trigger reading "Select region first" even when the row carried the parent value.

PR #2216 was the two-half answer to #2215. Half 2 — every picker taking the dependsOn chain as a hard baseFilter — is host-independent and was already live on the grid path, which is why the gate fired at all (verified: A2.5 holds, see LookupField's dependentFilter memo). Half 1 is per-host and the grid never got it. This PR supplies that missing input and re-implements no cascade.

The change

One line of behaviour in ObjectGrid.renderCellEditordependentValues={ctx.row} — plus row: any added to the local ctx annotation, which had omitted it (the declared contract always carried row; only this local annotation did not). FieldEditWidget already forwards dependentValues to the widget, since #7008.

The interim is labelled in the code, naming this card and #7188, and stating precisely what is stale: a parent edited but not yet saved in the same row does not re-scope the child.

Evidence

Rendering proof, packages/plugin-grid/src/__tests__/gridDependentValues-7165.test.tsx (4 tests, each with a live control in the same render):

  1. the dependsOn column opens (was lookup-trigger-gated + disabled; now lookup-trigger-regional_owner + enabled) — control: a no-dependsOn column in the same render is also enabled, so the picker path is proven reached.
  2. the picker is scoped by the row: region: 'north' reaches the query as a hard $filter and Person 07 (south) is absent — control: the no-dependsOn sibling over the same records does offer Person 07, so the absence is scoping and not a dead picker.
  3. negative control: a row whose saved parent is empty still gates, so the gate was not simply disabled — control: the filled row in the same render opens.
  4. ⚠️the interim's staleness, pinned as current behaviour: staging a new parent without saving does not re-scope the child — with its own proof that the staging actually landed (the region cell renders the pending 'south' while the saved record still reads 'north'), because without that the assertion would pass for the trivial reason that nothing was ever staged. The grid's inline cell editor scopes a dependsOn lookup by the SAVED row, not the staged one — carry the pending record across the renderCellEditor seam (option B of #7165) #7188 flips this test.

The #7154 pin was UPDATED, not deleted. The dependsOn case in lookupPickerKeys-7154.test.tsx pinned the gated behaviour, i.e. it encoded the defect. A deleted pin is indistinguishable from a pin that never existed, so it is rewritten in place to assert the fixed behaviour plus the scoping, and its header prose is rewritten to say so.

Ablation — direction predicted before running: removing the single dependentValues={ctx.row} line turns red the 4 new tests plus the updated #7154 case, and leaves the other 4 #7154 cases green.

  • No rebuild required, and this is measured rather than assumed: the root vitest.config.mts aliases every @object-ui/* specifier to that package's src, and the tests import ../ObjectGrid relatively — so nothing resolves through dist and a source mutation is directly live.
  • Mutation confirmed on disk by marker count (dependentValues={ctx.row}: 1 → 0) and blob hash (e8ab332e…f219c865…), not by the editor's exit code.
  • Result: Tests 5 failed | 4 passed (9) — exactly the predicted set, passing count included.
  • Restore proven by state, with HEAD named explicitly (git checkout HEAD -- ..., never the bare form, which restores from the index): git diff HEAD, git diff --cached and git status --short all empty, and the blob hash back to e8ab332e…. The script carried a trap ... EXIT INT TERM with absolute paths.

Gates

⚠️Two heads, stated separately so the provenance is checkable. The full set ran at f17b08047; main then advanced (#7180 landed), so it was merged in and the affected-package suite plus the cheap ratchet gates were re-run at the current head aba0c7499. Nothing below is quoted from a tree that is no longer this branch's head.

Re-run at aba0c7499 (current head, post-merge):

gateresult
packages/plugin-grid/ full suite105 files, 949 tests passed
check:control-bytesOK — 5958 tracked text files scanned
check:phantom-depsOK
check:changeset-no-majorOK

Run at f17b08047 (same tree for every path this PR touches — the merge brought in only packages/components/src/renderers/layout/containers.tsx, a .changeset, a docs page, a components test and one vitest.config.mts line, none of which this PR edits):

gateresult
type-check (plugin-grid + closure)14 tasks successful
lint (affected package, full, not narrowed)2 tasks, 0 errors (751 pre-existing warnings)
check:self-importOK
check:vi-mock-specifiers / check:vi-mock-inheritOK (4106 files, 525 with mocks)
check-governed-queue-guard --testNOT GOVERNED — 4 paths vs 5 surfaces, none matched

The typecheck genuinely covers the new tests: tsc -p tsconfig.test.json --listFiles lists all three edited files among its 1770 (this package typechecks tests via a dedicated project, so "typecheck green" is not silently excluding them).

⚠️#7166 — the concurrent lane in this same package — had not landed as of aba0c7499. It touches relationalMetaKeys.ts and the applyRelationalMeta copy set; this PR touches ObjectGrid.renderCellEditor's context construction. Disjoint symbols, but this branch will need a fresh merge if #7166 lands first.

Incidence — measured, and ⛔ not "no impact"

Two corpora, each instrument run with a live control:

  • this repo: 3 files match dependsOn|depends_on, and none is this defect — the gantt hit is depends_on as task predecessor links (a different key entirely) and the other is a select inside a form. Control: the same instrument returns 88 files across packages/.
  • hotcrm (a real shipped app, anonymous shallow clone): 6 dependsOn declarations on lookup fields across 5 objects (crm_contract ×2, crm_case, crm_quote ×2, crm_opportunity), all scoping contacts to crm_account. But 0 editable grids in its views (control: the views directory exists and was read).

⇒ Neither measured corpus reaches the defect today, because none combines dependsOn with an editable grid — but hotcrm is one metadata key away: editable: true on any of those five objects' list views hits it immediately. Customer apps are unmeasured, and the parent pattern is demonstrably real in shipped metadata. This is a bounded reading, not an all-clear.

Filed, not ridden

⚠️ Still live, per the PM's fence and not settled by this PR: should the user be told the scope came from the saved row? The interim's staleness is the same silent-wrongness class as the three refusals this seat landed in #7146 / #7147 / #7148. It is recorded on #7188 rather than left to disappear into a code comment.


Session: session_012wwHa4aaFybxXrfmfHioDM (https://claude.ai/code/session_012wwHa4aaFybxXrfmfHioDM) — kept in prose because a body edit rewrites the footer below to its bare form.

Generated by Claude Code

…lues
A `dependsOn` lookup column in an editable ObjectGrid was permanently
uneditable. `LookupField` resolves
`dependentValues ?? ctx.formValues ?? ctx.data ?? {}` and the grid's
`renderCellEditor` supplied none of the three, so the resolved record was
`{}` for every row, `dependenciesMissing` stayed `true`, and the trigger
rendered disabled ("Select region first") even when the row carried the
parent value.
PR #2216closed#2215 in two halves: the form renderer injects its live
watched record, and every picker takes the `dependsOn` chain as a hard
`baseFilter`. Half 2 is host-independent and was already live here; half 1
is per-host and the grid never got it. This supplies that missing input and
re-implements no cascade.
INTERIM: `ctx.row` is the saved record, so a parent edited but not yet
saved in the same row does not re-scope the child. Carrying the staged
record needs a seventh member on `renderCellEditor`'s published context
type (declared in `@object-ui/types`, #6882, and pinned by exact type
equality), which is a contract change tracked separately.
The `dependsOn` case in `lookupPickerKeys-7154.test.tsx` pinned the gated
behaviour and is UPDATED, not deleted.
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.6 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-C6RhwKZM.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)514.13KB117.19KB
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)206.14KB55.77KB
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

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3157.6 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-C6RhwKZM.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)514.13KB117.19KB
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)206.14KB55.77KB
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": 7165,
"status": "done",
"branch": "claude/issue-7165-grid-dependent-values",
"pr": "https://github.com/objectstack-ai/objectui/pull/7193",
"premise_still_valid": true,
"summary": "MEASUREMENT 1: B IS Clause 2-engaged — so option A shipped as the labelled interim and ⛔ #7165 MUST STAY OPEN (the PR says 'Part of #7165', never a closing keyword). The editor context is published surface on both disjuncts of the test: `DataTableSchema.renderCellEditor` is declared in packages/types/src/data-display.ts with a Zod mirror, declared by maintainer ruling 2026-08-30 (#6882) replacing a `(schema as any)` cast, and its exact shape is pinned by an `Equal` (not `extends`) type assertion in packages/types/src/__tests__/data-table-declared-keys-6882.test.ts whose comment states the shape is pinned deliberately; ObjectGrid.tsx independently fences that package as 'published surface with its own review floor'. B needs a seventh ctx member, so it edits @object-ui/types, its Zod mirror and that maintainer-ruled pin. All three ruling conditions are met: the interim is labelled in code naming #7165 and the follow-up and stating exactly what is stale, the card is not closed, and B is filed as #7188. Premise re-measured on head 899730e0a: the existing pin still passed before my change, i.e. the defect was still live. A2.1/A2.3/A2.4/A2.5 all confirmed; A2.2 needed one CORRECTION — the declared contract always carried `row`, but ObjectGrid's LOCAL ctx annotation omitted it, so A was a two-part edit (widen the local annotation, then pass it), not a literal one-liner.",
"tests": "All runs from repo root via the shared verify lock (objectui forbids `pnpm --filter PKG test`; path filters only). BASELINE on 899730e0a before any edit: `pnpm exec vitest run packages/plugin-grid/src/__tests__/lookupPickerKeys-7154.test.tsx` -> 'Test Files 1 passed (1) / Tests 5 passed (5)' — the gated pin still passed, so the defect was live on head. AFTER FIX, same command -> 'Tests 1 failed | 4 passed (5)', 'AssertionError: expected lookup-trigger-regional_owner to be lookup-trigger-gated' — the pin encoded the defect, so it was UPDATED IN PLACE (not deleted), header prose rewritten to say so. NEW FILE packages/plugin-grid/src/__tests__/gridDependentValues-7165.test.tsx, 4 tests each with a live control in the same render: (1) dependsOn column now opens, control column also opens; (2) picker SCOPED — $filter.region='north' reaches the query, Person 07 (south) absent, while the no-dependsOn control over the same records DOES offer Person 07; (3) NEGATIVE CONTROL — a row whose saved parent is empty still gates, proving the gate was not disabled; (4) the interim's staleness pinned as current behaviour, with its own proof the staging landed (region cell renders pending 'south' while the saved record reads 'north'), which is what stops it passing vacuously. Both files together: 'Test Files 2 passed (2) / Tests 9 passed (9)'. ABLATION — direction predicted BEFORE running (remove `dependentValues={ctx.row}` -> the 4 new tests plus the updated 7154 case go red, the other 4 stay green). NO REBUILD NEEDED, and that is measured not assumed: root vitest.config.mts aliases every @object-ui/* specifier to that package's src and the tests import ../ObjectGrid relatively, so nothing resolves through dist and a source mutation is directly live. Mutation confirmed ON DISK by marker count (1 -> 0) AND blob hash (e8ab332e… -> f219c865…), never by the editing tool's exit code. Result: 'Tests 5 failed | 4 passed (9)' — exactly the predicted set, passing count included. Restore proven BY STATE with HEAD named explicitly (`git checkout HEAD -- ABSPATH`, never the bare form which restores from the index): git diff HEAD, git diff --cached and git status --short all empty and the blob hash back to e8ab332e…; the script carried `trap ... EXIT INT TERM` with absolute paths. GATES: at final head aba0c7499 (after merging main, which had advanced to 8ec11e14f / #7180) — full packages/plugin-grid/ suite 'Test Files 105 passed (105) / Tests 949 passed (949)', check:control-bytes OK (5958 files), check:phantom-deps OK, check:changeset-no-major OK. At f17b08047 (identical tree for every path this PR touches): type-check plugin-grid + closure 14 tasks successful, lint of the affected package in FULL (not narrowed) 2 tasks 0 errors (751 pre-existing warnings), check:self-import OK, check:vi-mock-specifiers / check:vi-mock-inherit OK (4106 files, 525 with mocks), check-governed-queue-guard --test = NOT GOVERNED (4 paths vs 5 surfaces). Verdicts read from each gate's own printed line, with exit codes captured after redirection, never through a pipe. NOT-MEASURED check cleared: `tsc -p tsconfig.test.json --listFiles` lists all three edited files among its 1770, so the green typecheck genuinely covers the new tests rather than excluding them.",
"mcp_calls": "11 — 2 issue reads, 2 targeted search_issues (each with a control word that hit: #7165 itself, then #7154/#2215), 2 issue creates, 1 PR create, 1 PR body read-back, 1 PR body update, 2 report comments. REST was 403 for repo-scoped reads and `gh` is absent, both measured with a control before switching channels.",
"open_questions": [
{
"question": "Should the grid TELL the user that a dependsOn picker is scoped by the SAVED parent while an unsaved edit to that parent is staged in the same row? The PM's ruling explicitly left this live rather than settled, and asked that it not disappear into a code comment.",
"options": [
"A. Say nothing (what this PR ships). The picker silently uses the persisted parent. Zero new surface, but it is the same silent-wrongness class as the three refusals this seat landed in #7146 / #7147 / #7148.",
"B. Surface a hint on the trigger/popover when the parent has a staged-but-unsaved value that differs from the saved one — e.g. 'scoped by the saved Region; save the row to re-scope'. Needs the staged record, i.e. it is gated behind exactly the same seam change as option B of this card.",
"C. Do not build a message at all: land #7188, which removes the staleness and therefore removes the question."
],
"recommendation": "C, and treat B as dead. Real business need: the staleness only exists because the interim exists, so a message would be UI built to describe a defect we have already scheduled to delete. Long-term soundness: contract-first — the honest fix is the declared seam member, not a renderer-side apology for a wrong input, and a warning would fossilize the interim as if it were a supported mode. Making AI-authored metadata hard to get wrong: an author declaring dependsOn should get correct cascading, not a caption explaining why it is stale — a warning trains authors to accept a wrong scope. Startup scope discipline: option B costs the SAME seam change as #7188 while delivering strictly less, which is the worst trade of the three. ⚠️ The one thing that would flip this to B: if #7188 is declined or deferred long, the silent staleness stops being temporary and then it must be surfaced. So this question should be closed by #7188 landing, not by a ruling that silence is acceptable."
}
],
"out_of_scope_findings": [
"filed as #7188: option B — carry the pending/staged record across the renderCellEditor seam so a dependsOn lookup is scoped by the in-flight parent (the seam change is spelled out: @object-ui/types ctx member + Zod mirror + the #6882 exact-shape pin + data-table.tsx passing `{ ...row, ...rowChanges }` + ObjectGrid reading it). Clause 2-engaged; #7165 stays open for it.",
"filed as #7190 (label `finding`): plugin-detail's InlineFieldInput supplies no dependentValues either (measured: zero non-test occurrences; control: 88 files across packages/), and plugin-detail never provides SchemaRendererContext `data` itself. ⚠️ Deliberately NOT graded a bug — a detail page renders ONE record, so ctx.data may legitimately rescue the cascade there, and no rendering measurement was made. The single probe that settles it is written into the card.",
"NOT filed, reported here as an observation for PM triage: ObjectGrid.tsx still lists `renderCellEditor` in its `ObjectGridDataTableSchemaHolds` type as an undeclared-but-live HELD key, with prose saying whether DataTableSchema should declare it is a pending packages/types ruling. That ruling HAPPENED — #6882 declared it on 2026-08-30 — so the hold entry and its surrounding census comment are stale. Harmless today (the intersection with the declared member is identical), but it is documentation that now states the opposite of the tree. Left untouched because it is outside this card and inside the file #7166 is adjacent to."
]
}

Generated by Claude Code


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. The open question is ruled below.

⭐ The pre-flight scan that saved the card — and a trap nobody had written down

a first draft of the PR body contained the phrase "does NOT close #7165", which a pre-flight scan caught — GitHub's parser ignores negation and it would have closed the card on merge.

GitHub's closing-keyword parser does not read negation."does NOT close #7165" contains close #7165, and on merge it closes the card. This PR's entire correctness depends on #7165 staying open — A is an interim, B is the real fix — so the most natural sentence a careful author would write to protect the card is the one that would have destroyed it.

⚠️ And note the shape: the failure would have been silent and delayed. The PR merges, the card closes, and the interim becomes permanent because nothing is left tracking B. Carrying this to #6970 as a fourth mangling class, since that card enumerates exactly this family. The safe form is what shipped: Part of #7165, and never the keyword at all.

Measurement 1 answered decisively, on both disjuncts

B is Clause ②-engaged, and it was established rather than assumed:

⇒ B needs a seventh ctx member, so it edits @object-ui/types, its Zod mirror, and a maintainer-ruled exact-shape pin. That is Clause ② three times over. ⭐ The Equal-not-extends detail is what makes it airtight — an extends pin would have tolerated an added member, and the answer would have been different.

All three of my ruling conditions are met: the interim is labelled in code naming #7165 and #7188 and stating exactly what is stale; the card is not closed; B is filed.

A2.2 corrected, and the correction matters

I said A was "one line: dependentValues: ctx.row". Wrong: the declared contract always carried row, but ObjectGrid's local ctx annotation omitted it — so A was a two-part edit (widen the local annotation, then pass it). ⚠️ A declared member that a local annotation silently narrows is its own small hazard: the type says the data is there and the call site cannot see it.

The test discipline is the strongest part

  • Baseline before any edit: the existing bug(plugin-grid): ObjectGrid's inline lookup picker ignores four more spec-declared field props — multiple, allowCreate, lookupPageSize, dependsOn are read on its own path and never copied #7154 pin still passed on head — so the defect was live, not already fixed by something else. Establishing that first is what makes the rest meaningful.
  • The pin encoded the defect and was UPDATED IN PLACE, not deleted, with its header prose rewritten to say so. A deleted pin is indistinguishable from a pin that never existed.
  • A negative control: a row whose saved parent is empty still gates — proving the fix supplied a value rather than disabling the gate. That is the assertion that separates "cascade works" from "cascade removed", and it is the one most likely to be skipped.
  • The staleness pin carries its own proof it is not vacuous: the region cell renders the pending south while the saved record reads north, so the test proves the staging actually landed before asserting the picker ignored it. A staleness test without that proof passes for the wrong reason.
  • Ablation predicted before running, Tests 5 failed | 4 passed (9) exactly; mutation by marker count and blob hash; restore by state with git checkout HEAD -- ABSPATHnever the bare form, which restores from the index.

⚖️ Ruling on the open question: C — close it by landing #7188, not by ruling that silence is acceptable

I left this live at dispatch and asked that it not disappear into a code comment. The answer is C, and the argument that decides it is the one I did not have when I asked:

Option B costs the same seam change as #7188 while delivering strictly less. A warning that says "scoped by the saved Region" needs the staged record — i.e. exactly the ctx member #7188 adds. Having paid that price, you would ship a caption explaining the wrong scope instead of the right scope. That is the worst of the three trades.

And a warning would fossilise the interim as a supported mode. An author who declares dependsOn should get correct cascading, not a caption explaining why it is stale — a message trains authors to accept a wrong scope as normal.

⚠️I am adopting the flip condition verbatim, because it is what keeps C honest: if #7188 is declined, or deferred long enough that the interim stops being temporary, the silent staleness must be surfaced. So this question is closed by #7188 landing, not by a ruling that silence is fine. Recorded on #7165.

Incidence: carried, not closed — exactly as the dispatch required

Two corpora, each with a live control:

  • this repo: 3 files match dependsOn|depends_on, none is this defect (the gantt hit is depends_on as task predecessor links — a different key entirely); control: 88 files across packages/.
  • hotcrm: 6 dependsOn declarations on lookup fields across 5 objects, all scoping contacts to crm_account — but 0 editable grids in its views (control: the views dir exists and was read).

⇒ ⭐ "Neither measured corpus reaches the defect today, but hotcrm is ONE metadata key away"editable: true on any of those list views. That is a far more useful answer than either "no impact" or "unmeasured", and it is the sentence a maintainer needs to prioritise #7188. Customer apps remain unmeasured, and that is stated rather than glossed.

#7190 filed with the right restraint

InlineFieldInput supplies no dependentValues either (measured: zero non-test occurrences; control: 88 files) — and it is deliberately NOT graded a bug, because a detail page renders one record so ctx.data may legitimately rescue the cascade there, and no rendering measurement was made. The single probe that would settle it is written into the card. Declining to grade on a structural analogy, and leaving the probe behind, is better than a confident grade either way.


Nothing to change.


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 1, 2026 10:48
@os-warren
os-warren added this pull request to the merge queueSep 1, 2026
Merged via the queue into main with commit 84ffdbcSep 1, 2026
32 checks passed
@os-warren
os-warren deleted the claude/issue-7165-grid-dependent-values branch September 1, 2026 11:02
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-warren@claude