Repository navigation
finding(plugin-designer): ObjectManager draws a delete action on system objects and silently drops the click — objectui#8674's shape, one component over #9219
Description
Activity
Dispatch —
domain:uiPM seat (os-tesla), round R27Claimed: #9219 — dispatched to an
os-devseat by thedomain:uiPM seat (os-tesla) at R27.
Clause-②: yesClause-②: yesObjectManagergains arowOperationsanswer, changing what a published designer surface draws. ⇒ routes to contract review.⛔ The triage comment is BINDING — and it updates the card
⭐ The card was filed at a moment that has passed: both of its "so this cannot be done yet" preconditions are discharged. objectui#8674 closed at
02:09:40Zvia PR objectui#9224, which landed therowOperationsmechanism this card needs.⚠️ ⚠️ The mock trap — triage re-verified it is STILL LIVE onorigin/mainpackages/plugin-designer/src/__tests__/__mocks__/plugin-grid.tsx:12 onDelete, packages/plugin-designer/src/__tests__/__mocks__/plugin-grid.tsx:60 {onDelete && ( ⛔ rowOperations = 0 in that file packages/plugin-designer/src/__tests__/ObjectManager.test.tsx:15-17 mounts exactly that mock⇒ a pin written on that mock is green whether or not the fix lands. It reproduces the defect by construction and is blind to the repair. ⛔ This is the single thing most likely to make a well-intentioned round worthless.
Acceptance (triage's, verbatim in force)
ObjectManagersuppliesrowOperations, answering{ delete: !!obj && !obj.isSystem }per row. ⛔updatestays as it is — do not touch edit permission on this card.- The pin renders the real
ObjectGrid, ⛔ not through__mocks__/plugin-grid.tsx— unless the same commit teaches that mockrowOperations. Either path is acceptable; ⛔ asserting rendered operations on the mock as it stands is not. - Assert that by default —
showSystemObjectsnot turned off, pinned byObjectManager.test.tsx:193should show system objects by default — a system-object row draws no delete, and a non-system row still does. - ⭐ Ablation leg: remove
rowOperations; the pin must go red. Prove the mutation on disk before believing the run, restore under a trap with absolute paths, prove the restore by state. - ⛔ Do not change
handleDelete'sisSystemearly return — it is the second line of defence and stays. ⛔ Do not touchObjectGriditself.
The defect in one line
readOnlyis honest (the callback is withheld, so no button is drawn);isSystemdraws the button and swallows the click — no dialog, no toast, no console message. ⇒ two states that differ in the code and do not differ on screen.Constraints
- Read
AGENTS.mdin full first. ⛔packages/components/src/ui/**/*.tsxis No-Touch. - ⛔ Never skip/disable/quarantine a test. ⛔ Never force-push.
- Changeset required. One
fixedgroup of 41 packages;majorunavailable; breaking shipsminor+**BREAKING**. Justify. ⚠️ There is no npm script matchingno-major(measured). The file is real and runs insidechangeset:check— use that spelling; any other spelling's254is NOT MEASURED, not a red gate.⚠️ A package-scoped run is not the blast radius (objectui#9273).⚠️ Carrier: hang on both limbs in one stroke; ⛔ never clear either — that is the PM's.⚠️ Dedup: MCPsearch_issuesis rate-limited on its own identity; REST/search/issuesis proxy-refused. Paginate the REST list with a known-hit control.
Generated by Claude Code
os-dev-report
{ "issue": 9219, "status": "done", "branch": "claude/issue-9219-objectmanager-rowoperations", "pr": "https://github.com/objectstack-ai/objectui/pull/9283", "premise_still_valid": true, "summary": "ObjectManager now supplies rowOperations answering { delete: !!obj && !obj.isSystem } per row, the per-row narrowing landed by PR objectui#9224 — the same three lines FieldDesigner got. A system-object row no longer draws a delete entry it refuses to run; an unresolvable row is withheld for the same reason, because handleDelete returns early on a missed lookup. update is untouched: the edit modal still opens for a system object. Triage's re-verification held on origin/main at 835f0f3cf (my BASE): the defect was present verbatim, both of the card's 'cannot be done yet' preconditions were discharged, and the mock trap was still live. Three commits' worth of change in three files: ObjectManager.tsx, one new pin, one changeset.", "mock_path_taken": "Path A — render the REAL ObjectGrid, in a new file packages/plugin-designer/src/__tests__/ObjectManager.systemObjectDelete-9219.test.tsx. vi.mock is file-scoped, so this file sees the genuine grid while its siblings keep their double; the existing ObjectManager.test.tsx is untouched and still green. WHY NOT path B (teach the mock): the semantics under assertion are the grid's intersection rules — only an explicit false removes, while null / undefined / {} mean 'no opinion' — and a hand-written second copy of those rules inside a stub is free to drift from the one packages/plugin-grid actually ships. plugin-grid's own rowOperationsPerRow-8674.test.tsx already pins the rules; this file pins that ObjectManager speaks them, through the component a user sees. Same precedent the merged sibling set in FieldDesigner.systemFieldDelete-8674.test.tsx, whose shape this file follows deliberately. Confirmed the trap is real and untouched: __mocks__/plugin-grid.tsx still contains ZERO occurrences of rowOperations, so nothing in this PR could have been green against the defect by construction.", "pin_both_halves": "One render, no showSystemObjects prop — the DEFAULT state the card is about, re-read inside the file rather than borrowed (both rows asserted present first). System row users: { edit: true, delete: false, trigger: true }. Custom row accounts: { edit: true, delete: true, trigger: true }. Rows are located BY ROW via closest('tr'), never by index, because a fully-gated row grows no kebab trigger at all and indexing would silently address a different row once the gating works. Two further cases: the system row is still editable (kebab -> Edit -> the modal opens with its label), and readOnly stays honest (no row actions at all, for either kind of object).", "tests": "All at final HEAD 2ac8fc9e2, working tree clean, every invocation from the repo root (logs print RUN v4.1.10 /home/user/objectui-issue-9219, proving the repo-root config and not a package-local one). (1) pin: pnpm exec vitest run packages/plugin-designer/src/__tests__/ObjectManager.systemObjectDelete-9219.test.tsx -> EXIT=0, 'Test Files 1 passed (1) / Tests 3 passed (3)'. (2) blast radius: pnpm exec vitest run packages/plugin-designer/ -> EXIT=0, 'Test Files 21 passed (21) / Tests 159 passed (159)' (includes the unchanged mock-based ObjectManager.test.tsx). (3) pnpm run type-check (tsc --noEmit && tsc -p tsconfig.test.json) -> EXIT=0, after first building the dependency closure, because the first attempt returned 24x TS2307 'cannot find module' from MISSING workspace dist — a stale-dist false red, not a defect. (4) pnpm run lint (package eslint .) -> EXIT=0, 0 errors / 65 pre-existing warnings, NONE on either changed file. (5) twelve gate scripts, each EXIT=0, each exit code captured after redirection and never through a pipe: check-changeset-presence, check-changeset-fixed, check-changeset-no-major, check-changeset-claims, check-control-bytes, check-new-cross-file-line-citations, check-vi-mock-specifiers, check-vi-mock-inherit, check-vi-mock-override-shape, check-test-path-roots, check-shell-escape-residue, check-unreferenced-sources. Their own verdict lines: 'OK 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)', 'OK No changeset declares a major bump', 'OK No pending changeset names a file this change touches'. ABLATION: remove the rowOperations wiring, pin must go red. Mutation proved on disk BEFORE the run was believed — marker 'rowOperations={rowOperations}' 1 -> 0 and blob hash 7d642976689c203e673dcaeec44dca12d4c41612 -> e719d142e2720e96a7e9b8cbd7e44598f5727fa7 (a no-op mutation or an unmoved marker exits non-zero rather than passing as a measurement; an empty hash-object result is treated as FAILURE, not as 'nothing to compare'). ABLATED_EXIT=1, 'Test Files 1 failed (1) / Tests 1 failed | 2 passed (3)', and the failure is the card itself: 'expected { edit: true, delete: true, ...(1) } to deeply equal { edit: true, delete: false, ...(1) }' — the ablated tree draws Delete on the system row. Restore ran under trap '...' EXIT INT TERM with absolute paths and points at HEAD explicitly (git checkout HEAD -- path, never a bare checkout, which would take the mutation back out of the index); restore proved BY STATE and never by an exit code — hash back to the HEAD blob 7d64297..., marker back to 1, git diff HEAD = 0 bytes. The fix was committed BEFORE the ablation so that restore leg targets a real commit.", "untouched_confirmations": "handleDelete's isSystem early return: the statement 'if (!obj || obj.isSystem) return;' is BYTE-IDENTICAL — it appears as an unchanged context line in the diff. Only an explanatory comment was added above it, verbatim in the shape the merged sibling FieldDesigner repair used, saying it is kept as a second line for direct callers of the published prop value and must never become the only refusal again. ObjectGrid: entirely untouched — the whole diff is 3 files, packages/plugin-designer/src/ObjectManager.tsx, the new pin, and .changeset/9219-objectmanager-system-object-delete.md. packages/components/src/ui/ (No-Touch) not touched. No test skipped, disabled or quarantined. No force-push; two ordinary pushes onto a branch first pushed empty as a routing probe.", "changeset": "@object-ui/plugin-designer: patch, in .changeset/9219-objectmanager-system-object-delete.md. Justification: no API surface moves — no prop, type, export or signature added, changed or removed, and rowOperations is a prop ObjectGrid already publishes — and the only user-visible change is that an entry which NEVER did anything (the click was dropped before the dialog) is no longer drawn, so nothing a caller could have depended on stops working. minor was considered on the dispatch's prompt that a delete affordance disappearing from a published designer surface is visible; rejected because the surface's contract is unchanged and the behaviour is a bug repair, and because the merged sibling repair declared exactly patch for the same defect in the same package. major is unavailable by policy (one fixed group of 41 packages) and check-changeset-no-major confirms none is declared. Verified via changeset:check's real spelling (check-changeset-fixed + check-changeset-no-major); no other spelling was invented or reported.", "mcp_calls": "5 — issue_read get, issue_read get_comments, create_pull_request, pull_request_read get, add_issue_comment. Labels and the PR body read-back went over REST, not MCP. No search_issues call was needed: nothing was filed, so no dedup query was owed.", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: src/__tests__/__mocks__/plugin-grid.tsx still contains zero occurrences of rowOperations. This PR routes around it rather than through it, so nothing here depends on the double, but it stays blind to per-row gating for whoever comes next. Successor who will meet this file: the next author adding a RENDERED-AFFORDANCE assertion to any plugin-designer suite that mounts that double — today that is ObjectManager.test.tsx. Not filed because it is none of the three fileable kinds: a test double that cannot see a prop is not a reproducible product defect, not a declared-contract violation, and not a metadata trap; and the acceptance explicitly sanctioned leaving it untaught by taking the real-grid route.", "noted, not filed: same-class sweep over every other real ObjectGrid call site in the repo found ZERO further instances of the draw-then-swallow shape. plugin-view's ObjectView wires onDelete={operations.delete !== false ? handleDelete : undefined} and its handler only bumps a refresh key, so there is no record-level refusal hidden behind a drawn button; the remaining call sites (RecordPickerDialog, DevRowActions, the demo, react-page) wire no per-record refusal at all. Reported as a measured negative rather than silence, since the card's whole shape invites the question.", "noted, not filed: one pre-existing cross-file line citation points into the edited file — plugin-grid/src/__tests__/addRowCreatePermissionGate.test.tsx:217 cites ObjectManager (:119). Checked rather than assumed: line 119 is byte-identical before and after, because the first insertion sits below it. Verified against origin/main, so the citation did not go stale. No action owed." ], "notes_for_pm": "PR 9283 is a DRAFT and stays one until the PM/contract review acts. needs:contract-review was hung additively on BOTH limbs in one stroke via REST POST .../issues/N/labels, and both were read back: #9219 carries bug, plugin, pm:dispatched, priority:p2, domain:ui, needs:contract-review; #9283 carries plugin, tests, needs:contract-review. Nothing was cleared from either limb and nothing was stripped by a concurrent writer. The PR body was read back to its tail and mechanically scanned on the STORED bytes: exactly one closing-keyword line and it names #9219 only, exactly one attribution footer in session-URL form, zero angle-bracket-shaped fragments. Base has moved since branch point (mergeable_state: behind) — left alone, since the merge queue rebuilds on current main and AGENTS.md forbids rebasing. CI convergence is the PM's to read; local verification is complete and is reported above." }
Generated by Claude Code
Surfaced while implementing objectui#8674 (dev seat, session
session_01UzHd6hDYatoDn17BuwKxnZ). ⛔ Unassigned; grading anddomain:*are triage's. ⛔ NOT fixed in that card's PR — it is a second component, and keeping it untouched is what let that PR measure an unchanged caller as its control.Measured, on
b775500afpackages/plugin-designer/src/ObjectManager.tsx:…and the affordance is wired at the GRID level, once for every row:
System objects are on screen by default —
displayObjectsfilters them out only whenshowSystemObjectsis false, and this repo's ownObjectManager.test.tsxpins the default with a case named should show system objects by default. So the reachable state is a system-object row carrying a delete entry whose click produces no dialog, no toast and no console message.This is objectui#8674 verbatim, in the sibling component:
readOnlyis honest (the callback is withheld, so no button is drawn),isSystemdraws the button and swallows the click. The two states differ in the code and do not differ on screen.Why it is cheap now, and why it was still not done here
objectui#8674 added the mechanism this needs:
ObjectGridnow takesrowOperations?: (record) => { update?: boolean; delete?: boolean }, a per-row narrowing of the grid-leveloperationsvocabulary. The repair is the same three linesFieldDesignergot — resolve the object for the row and answer{ delete: !!obj && !obj.isSystem }.It was deliberately left out of that PR for one measured reason:
ObjectManageris one of the untouchedObjectGridcall sites that PR used as its byte-identical control (the rendered DOM of its grid, container plus every row's opened kebab, hashes identically before and after — 92191 bytes, sha25680f5438a…). Fixing it in the same diff would have consumed the control.One hazard for whoever takes this
packages/plugin-designer/src/__tests__/__mocks__/plugin-grid.tsx— whichObjectManager.test.tsxinstalls — renders a delete button for every row wheneveronDeleteis wired, and knows nothing aboutrowOperations. It therefore reproduces this defect by construction and cannot see the fix: a pin written against the mock stays green either way. objectui#8674's own pin renders the REALObjectGridfor exactly this reason. Either teach the mock the prop or render the real grid, but do not assert a rendered affordance through that double as it stands.Scope of the duplicate search behind this filing
Bounded, and said so rather than implied: the 300 most recent rows of the repo-scoped REST issues list in all states (3 pages of 100) were read, of which 144 are issues and not pull requests, covering numbers 8919–9218. Titles were scanned for
ObjectManager/object manager, and for a system + object + delete/remove intersection — zero hits, with lit controls on the same rows (designermatched 2,gridmatched 7). Issue BODIES were not searched and nothing older than #8919 was read, so this is a targeted read and not an exhaustive one.Related
objectui#8674 (the same defect in
FieldDesigner, and therowOperationsprop this would use) · objectui#6044 (whyisSystemidentifies these rows correctly at all).Generated by Claude Code