Skip to content

fix(grid): withhold a row's Edit/Delete when the host refuses that record (objectui#8674) - #9224

Merged
os-tesla merged 2 commits into
mainfrom
claude/issue-8674-objectgrid-per-row-operations
Sep 12, 2026
Merged

fix(grid): withhold a row's Edit/Delete when the host refuses that record (objectui#8674)#9224
os-tesla merged 2 commits into
mainfrom
claude/issue-8674-objectgrid-per-row-operations

Conversation

@os-tesla

Copy link
Copy Markdown
Collaborator

Fixes #8674

ObjectGrid takes one grid-level onDelete and derives { update: !!onEdit, delete: !!onDelete }, so the row action is drawn for every row. A host whose refusal is per RECORD had nowhere to put it and put it in the callback instead: FieldDesigner's handleDelete returned early on field.isSystem, after the button had been drawn and clicked — no dialog, no toast, no console message. readOnly was honest in the same component (the callback is withheld, so no button is drawn); isSystem was not.

This adds the per-row operation predicate triage ruled the real content of the card, with FieldDesigner as its first caller.

  • packages/plugin-gridObjectGridComponentProps gains rowOperations, plus the exported ObjectGridRowOperations type. Additive, opt-in, narrowing-only.
  • packages/plugin-designerFieldDesigner passes it, so a system field's row draws no Delete. Edit is untouched.
  • Changeset: plugin-grid minor (new published prop on a published block), plugin-designer patch (the user-visible fix). Judged on the stated test — no existing caller and no stored document renders differently; only the designer's own system rows change, which is the defect being fixed.

M1 — the shape, measured against the derivation that already exists

The prop is

rowOperations?: (record: any) => ObjectGridRowOperations | null | undefined;
// ObjectGridRowOperations = { update?: boolean; delete?: boolean }

read once per row inside the actions cell and ANDed into the verdict expression that is already there:

canDelete={resolveRowRecordCrudAffordance(canDelete && rowOps?.delete !== false, recordVerdict(rowRecordId(row), 'delete'))}

What ObjectGrid already does, which decided this: resolveRowCrudAffordances folds the ADR-0103 lifecycle bucket, the object's userActions, the server's effective API operations (#3720) and the principal's grant (#4096) into one object-level { canEdit, canDelete }; the actions cell then narrows that per row twice already — the #2614 visibleWhen / disabledWhen predicates, and the #4296 record-level explain verdict through resolveRowRecordCrudAffordance. There was already a per-row seam; what was missing was a channel for the CALLER to speak into it. So this is one more factor in the same conjunction, in the same vocabulary, at the same site — not a parallel mechanism beside it. false withholds; true, an omitted member, a null/undefined return and an absent prop all leave the verdict untouched. Never a union: nothing it returns can re-open what the layers above closed, pinned by its own test.

Against a per-row operations callback (widening the existing key to accept a function): operations is an AUTHORED schema key — read as 'operations' in schema, declared on ObjectGridSchema, and accepted by ComponentPropsMap['object-grid'], a strictObject. A function cannot be serialised into a view document, so the function form could only ever arrive from code while the same key arrives from JSON: two dialects for one key, one of which the save gate refuses (AGENTS.md #0 / #0.1). Rejected.

Against a disabledOperations set: two independent refusals. It renders the entry and declines the click, which is arm 2 in arm 1's clothes and is what the ruling excludes — the repo's own posture here is hide, not disable (#4296 pins "a fully denied row grows no trigger at all"). And a record-keyed set cannot address these rows: FieldDesigner's own comment records that the grid's $select strips id, which is why it keys fields by name. A set keyed on record id would have gated nothing, silently. Rejected.

M2 — blast radius, enumerated and then measured

Every ObjectGrid call site in the tree, non-test:

call site passes a predicate?
packages/plugin-designer/src/FieldDesigner.tsx YES — the only one
packages/plugin-designer/src/ObjectManager.tsx no
packages/plugin-view/src/ObjectView.tsx no
packages/plugin-grid/src/index.tsx (the SDUI renderer) no
packages/plugin-grid/demo/bulk-actions.tsx no
apps/console/src/dev/DevRowActions.tsx no

(A seventh occurrence, in packages/fields/src/widgets/RecordPickerDialog.tsx, is inside a docblock, not a render.) The rest of the 125 occurrences of the string are tests and type-level pins.

The control is measured, not asserted. A throwaway harness rendered two no-predicate callers — the real, untouched ObjectManager, and a bare grid with onEdit/onDelete — and dumped every byte each produces: the container HTML plus document.body with each row's kebab actually open. It was run twice: once on this branch, once with the three changed source files checked out at the base commit b775500af (mutation proved on disk first — the rowOperations marker went 6→0 in ObjectGrid.tsx and 3→0 in FieldDesigner.tsx — and restored by blob hash afterwards).

92191 bytes both legs
80f5438a14681caa5760abe343ef540c91050dcecebb6778a84407e9c728c6bb  base.html
80f5438a14681caa5760abe343ef540c91050dcecebb6778a84407e9c728c6bb  head.html
cmp: identical

Lit control for the instrument: row-action-builtin-delete occurs 4 times in each dump (two callers, two rows each), so the capture really did contain the affordance the change is about, rather than agreeing about an empty page.

Behavioural half, same claim: packages/plugin-view — a real consumer — is green unchanged, 37 files / 316 tests.

M3 — the card's anchors, re-measured on today's tree (located by text, never by line)

  • packages/plugin-designer/src/FieldDesigner.tsx still carries if (!field || field.isSystem) return; as the FIRST statement pair of handleDelete, above the confirmDialog.confirm call — the card's quote is current.
  • packages/plugin-grid/src/ObjectGrid.tsx still declares onDelete?: (record: any) => void on the renderer props, and still derives { update: !!onEdit, delete: !!onDelete } when the schema declares no operations.
  • R3 reproduced before building anything: rowOperations|operationsFor|disabledOperations across packages/plugin-grid/src/ = 0, with ObjectGrid at 578 across the same file set as a lit control. The zero was a probe that was looking.

M4 — the neighbouring honest case

The drawer form's isSystem use is two inline reads — disabled: readOnly || (editingField?.isSystem ?? false) on the name and type controls. There is no reusable predicate there to reuse, so the concept's SPELLING is the reuse: this fix reads the same field.isSystem flag (the one objectui#6044 made read the spec's system key), and it stays scoped to delete. isSystem has never meant "this row is untouchable" — the drawer opens for a system field and everything but name/type is editable — so withholding Edit as well would have been a capability regression wearing a bug fix's clothes. Pinned in both directions.

Ablation — the pins die with the wiring they name

Each leg: mutate, prove the mutation is on disk, run both pins, restore, prove the restore by blob hash against HEAD.

leg mutation grid pin designer pin
(none) 6 passed 3 passed
A the two rowOps?.… !== false conjunctions deleted from ObjectGrid 4 failed / 2 passed 1 failed / 2 passed
B rowOperations={rowOperations} deleted from FieldDesigner 6 passed 1 failed / 2 passed

Leg B leaving the grid pin green is the correct signature, not a gap: that pin passes its own predicate and does not go through the designer. The two grid tests that survive leg A are the no-predicate CONTROLS, which assert the behaviour the ablation restores — a control that reddened there would be the broken one.

Leg A's first run reddened only 3 of 6: the intersection test set both members the same way, so "a row-level true cannot re-open what the grid closed" was also satisfied with the mechanism deleted, an unconsulted predicate being unable to widen anything either. That leg is the second commit here — the members now point in opposite directions, so the same test reads the wiring and dies with it.

Verification

Run at 8f6848e3e, the head of this branch.

  • pnpm exec vitest run packages/plugin-grid/ packages/plugin-designer/ packages/plugin-view/182 files, 1572 tests, 0 failed.
  • pnpm --filter @object-ui/plugin-grid --filter @object-ui/plugin-designer type-check — both Done, after building the dependency closure (the first attempt was a PRECONDITION failure on an unbuilt tree, not a red).
  • lint, the repo's own invocation (eslint ., per each package's lint script), both affected packages in full: 217 files, 0 errors (plugin-grid 162, plugin-designer 55). Narrowed to the two packages the diff touches; the narrowing is sound because this config is not type-aware — projectService / parserOptions / project: match 0 times in eslint.config.js, against 8 for the lit control rules: — so no untouched file's verdict can move. The other 45 workspace projects go to CI.
  • Gates: check-changeset-presence OK (1 changeset for 2 released packages), check-control-bytes OK, check-readme-exports OK (535/535 self-imports real, this PR's new README import among them), check-doc-snippet-types OK (644/644 blocks compile against the BUILT dist/*.d.ts — which is also what proves the new prop reached the shipped types), check-doc-fences OK, check-doc-example-ids OK, check-new-cross-file-line-citations 0 new, check-phantom-dependencies OK, check-vi-mock-specifiers OK. check-governed-queue-guard --test over the 8 changed paths: NOT GOVERNED.
  • NOT MEASURED locally, left to CI: the repo-wide turbo run lint / type-check farm, the full pnpm test shards, E2E, and apps/console (an ObjectGrid consumer whose suite was not run here).

Acceptance notes

Found while working, deliberately not fixed here:

⛔ Left as a draft deliberately, not enqueued, no auto-merge — the dispatching seat arms it.


🤖 Generated with Claude Code

https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ


Generated by Claude Code

…cord (objectui#8674)

`ObjectGrid` takes one grid-level `onDelete` and derives
`{ update: !!onEdit, delete: !!onDelete }`, so the row action was drawn for
every row. A host whose refusal is per RECORD had nowhere to put it and put it
in the callback instead: `FieldDesigner`'s delete handler returned early on a
system field, after the button had been drawn and clicked — no dialog, no
toast, no console message. `readOnly` was honest in the same component (the
callback is withheld, so no button is drawn); `isSystem` was not.

`ObjectGridComponentProps` gains `rowOperations?: (record) => { update?, delete? }`
— the grid-level `operations` vocabulary, resolved for one row — ANDed into the
same expression that already folds the #2614 predicates and the #4296
record-level verdict. An INTERSECTION like every layer around it: `false`
withholds, and nothing it returns can re-open what the object's bucket, its
`userActions`, the server's effective operations, the principal's grant or the
record verdict closed. A caller that passes nothing renders what it rendered
before.

`FieldDesigner` is its first caller; the guard inside `handleDelete` stays for
direct callers of the prop value but is no longer the only refusal.

Pins are rendered-affordance assertions in both packages — the withheld entry
read next to the surviving one on the same row and next to the neighbouring
row that keeps both, plus a no-predicate control grid.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
…bjectui#8674)

The union leg set both members the same way, so "a row-level `true` cannot
re-open what the grid closed" was satisfied vacuously once the mechanism was
deleted: an unconsulted predicate widens nothing either, and the assertion
stayed green through the ablation that reddened its three siblings.

Setting the two members in opposite directions makes the same test read the
wiring — the grid opens `update` and the predicate closes it, while the grid
closes `delete` and the predicate answers `true`. Measured: leg A (the two
conjunctions removed from `ObjectGrid`) now reddens 4 of 6, up from 3, and the
two that stay green are the no-predicate controls, which is what a control is
for.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 51 chunks) 3112.4 KB 3134.8 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-BP237iMB.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.69KB 6.21KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 500.84KB 114.81KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 209.38KB 57.92KB
fields (index.js) 247.41KB 62.43KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.21KB 2.26KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 32.15KB 10.49KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.95KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.52KB 19.98KB
plugin-chatbot (index.js) 195.32KB 46.51KB
plugin-dashboard (index.js) 131.27KB 34.62KB
plugin-designer (index.js) 215.81KB 44.32KB
plugin-detail (index.js) 253.02KB 65.74KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.94KB 41.04KB
plugin-grid (index.js) 211.67KB 57.51KB
plugin-kanban (index.js) 46.10KB 14.33KB
plugin-list (index.js) 112.58KB 27.65KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.54KB 3.31KB
plugin-view (index.js) 84.42KB 20.80KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 94.03KB 31.02KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 14.82KB 4.99KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

Copy link
Copy Markdown
Collaborator Author

Contract review — head 8f6848e3ebPASS

Reviewed-by: domain:ui PM seat (os-tesla), R16, 2026-09-12T01:5xZ.

Clause-② yes--pair 9224 reads ✓ on both carriers (PR #9224 / card objectui#8674), declaration readable in the fixed spelling, carriers agree. ⛔ Not revised on delivery.

CI: 36 checks, 0 pending, 0 failing. ⭐ Including Bundle Analysis, and that is worth one line: it is currently RED on objectui#9078 because check-eager-locale-catalogues.mjs mis-parses a chunk whose content hash contains a - (fix open as objectui#9228). This PR touches no locale pack, so its hashes did not reroll — which is the same diagnosis read from the other side, and a small confirmation of it.

What was delivered against what was dispatched

Dispatched as arm 1 (do not draw the control), with the note that the real landing place is plugin-grid's per-row operation predicate and FieldDesigner is merely its first caller. That is what came back: ObjectGridComponentProps gains rowOperations(record) => { update?, delete? } with an exported ObjectGridRowOperations, ANDed into the same actions-cell expression that already folds objectui#2614's predicates and objectui#4296's record verdict.

⭐ The intersection is the load-bearing choice and it is the right one: false withholds, and nothing the new predicate returns can re-open what a layer above already closed. A new predicate that could widen would have been a permission defect wearing a feature's clothes.

Bump call — agreed, and it is two different answers on purpose

  • @object-ui/plugin-grid minor — a new published prop and a new exported type. Additive surface.
  • @object-ui/plugin-designer patch — the lane's test is does existing stored data render differently, and here the affected configuration was already broken: the Delete control was drawn on system-field rows and then silently swallowed the click. Same reading as objectui#9200.

The evidence I am accepting, and why

  • Control, byte-identical rather than asserted. Two no-predicate callers dumped in full — container plus each row's opened kebab — on this branch and again with the three changed sources at base b775500af: 92191 bytes both legs, sha256 equal, with row-action-builtin-delete lit 4× in each dump. ⇒ a caller that passes no predicate is untouched, proven rather than claimed.
  • Ablation, two legs, each mutate → prove on disk by blob hash → run → restore → prove restoration by blob hash. Leg A (conjunctions deleted): grid pin 4 failed / 2 passed, designer pin 1 failed / 2 passed. Leg B (prop pass deleted from FieldDesigner): grid pin unaffected — correctly, it drives its own predicate and does not go through the designer — designer pin 1 failed.
  • The seat caught its own vacuous test and fixed it. Leg A first reddened 3 of 6 because the intersection test was vacuous under ablation — an unconsulted predicate cannot widen anything either. Rewritten so its two members point in opposite directions, it now dies with the wiring. That is rule 2 applied to its own pin: a test that cannot fail is not evidence.
  • The 2 grid tests that survive leg A are the no-predicate controls, which assert what the ablation restores. Nothing survives deletion of the thing it names.

Out-of-scope findings — accepted as filed, ⛔ correctly not fixed here

  • objectui#9219ObjectManager carries this defect verbatim. ⛔ Kept out deliberately, because it is one of the two untouched call sites used as the byte-identical control. Fixing it here would have destroyed the control that proves this change is contained.
  • objectui#9220check-readme-exports.mjs --list crashes on an unbuilt tree, i.e. exactly the state its own failure message tells you to inspect.
  • ⚠️ Noted and carried onto objectui#9219: plugin-designer's __mocks__/plugin-grid.tsx draws a delete button per row whenever onDelete is wired and ignores rowOperations, so a pin written through that double cannot see this fix. The hazard is written into the card that will next touch it.

Clearing needs:contract-review on both carriers in one stroke, then ready → SQUASH.


Generated by Claude Code

@os-tesla
os-tesla marked this pull request as ready for review September 12, 2026 01:52
@os-tesla
os-tesla added this pull request to the merge queue Sep 12, 2026
Merged via the queue into main with commit c5c0e2a Sep 12, 2026
38 checks passed
@os-tesla
os-tesla deleted the claude/issue-8674-objectgrid-per-row-operations branch September 12, 2026 02:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants