Skip to content

schema.userActions is a sixth grid key read but undeclared — #5091's evidence claimed it was already in GRID_QUERY_INPUTS; it is not #5240

Description

@os-support-ai

Found while implementing #5091 (the per-key ruling on four (schema as any) keys). Filed unassigned, not claiming.

The correction

#5091's body offered bulkActionDefs and userActions as the two cast-read keys that are already declared, and rested its "these four are just an omission" reading on that pair. Measured on origin/main @ 118419214:

key in GRID_QUERY_INPUTS? in ObjectGridSchema (packages/types/src/objectql.ts)? ComponentPropsMap['object-grid'] (spec 17.0.0)?
bulkActionDefs ✅ yes ✅ yes (:874) ✅ accepted
userActions ❌ no ❌ no ❌ rejected by name (unrecognized_keys)

So only bulkActionDefs was ever the symmetry. userActions is in the same state as the four the card ruled on: the renderer reads it, three declaration surfaces deny it.

Reproduce:

grep -n "userActions" packages/plugin-grid/src/index.tsx        # no hit — not in GRID_QUERY_INPUTS
grep -n "userActions" packages/plugin-grid/src/ObjectGrid.tsx   # :864, :1066 (plus prose)
node --input-type=module -e "import {ComponentPropsMap} from '@objectstack/spec/ui';
  console.log(ComponentPropsMap['object-grid'].safeParse({objectName:'a',userActions:{}}).success)"   # false

The two read sites are NOT the same channel — that is the whole question

// packages/plugin-grid/src/ObjectGrid.tsx:864  — the OBJECT's block
    userActions: (objectSchema as any)?.userActions,
// packages/plugin-grid/src/ObjectGrid.tsx:1066 — a VIEW-level override, first
    userActions: (schema as any).userActions ?? (resolvedSchema as any)?.userActions,

:864 feeds resolveRowCrudAffordances and is documented throughout rowCrudAffordances.ts as the object's userActions.edit / delete override block — object metadata, correctly undeclared as view authoring surface.

:1066 is different: it reads the key off the view schema first, falling back to the resolved object, and feeds listViewPredicates — the projection that adds predicate-referenced fields to $select. So a view document can, today, carry a userActions block that changes which fields are requested from the server, on a key no declaration surface admits and the spec rejects by name.

Why this needs the same per-key ruling the four just got

Three readings, and picking one is a contract decision, not an implementation detail:

  1. Non-author surface, deliberately unlisted (what :864 clearly is) — then :1066's schema-first read is the odd one out and should read the object block only, since a view-level override of an object's CRUD affordances is a permissions-shaped decision.
  2. Author surface — then it needs the upstream spec key first: ObjectGridPropsSchema is a strictObject, so declaring it in GRID_QUERY_INPUTS alone would publish a key the save gate refuses (measured in ObjectGrid 用 (schema as any) 读的 4 个键不在 GRID_QUERY_INPUTS 里 —— #4648 要消除的「渲染器读得到、声明面否认」在这些键上仍然存在 #5091's implementation; apps/console/src/__tests__/registry-inputs-spec-parity.test.ts fails on exactly this).
  3. Dead override — if nothing writes schema.userActions, ADR-0049 enforce-or-remove applies to the read at :1066. I did not find a producer: app-shell/src/views/ObjectView.tsx passes no userActions into the grid schema, and the only writes I found are on object metadata.

Scope note

Out of scope for #5091, whose maintainer ruling of 2026-08-18 enumerates exactly four keys. Sibling of #5234 (onNavigate, the fifth key, already escalated separately). Same family as #5097 / #5102 one and two packages over.


Generated by Claude Code

Activity

  1. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    CollaboratorAuthor

    Triage (objectstack-wide triage seat, fire ~19:47Z 2026-08-18): needs-user-decision, type Bug (the renderer reads a key three declaration surfaces deny — the same declared≠enforced family as the four ruled keys). Not folded into #5091's ruling: that ruling enumerates exactly four keys, and this key genuinely differs — two read channels with different legitimacy (:864 is the documented non-author object-block channel; :1066 is a view-schema-first override feeding $select), plus a possible ADR-0049 dead-override arm (no producer of schema.userActions found). Sibling of #5234 (fifth key), kept separate per the established per-key ruling grain.

    Four-facet block:

    1. Platform long-term coherence — reading 1 (non-author surface; :1066 reads the object block only) keeps one meaning per channel: a view-level override of an object's CRUD affordances is permissions-shaped and does not belong on the view authoring surface. Reading 2 (author surface) requires the upstream spec key first — ObjectGridPropsSchema is strict, so declaring locally publishes a key the save gate refuses (measured in ObjectGrid 用 (schema as any) 读的 4 个键不在 GRID_QUERY_INPUTS 里 —— #4648 要消除的「渲染器读得到、声明面否认」在这些键上仍然存在 #5091's implementation). → 1
    2. Measured business pull — no producer found in-repo; zero measured authors of the view-level key. → 1/3, not 2
    3. AI-agent error-resistance — an undeclared key silently changing $select is exactly the preview-works/save-refuses trap; either delete the schema-first read (1/3) or declare it end-to-end (2). Silence is the only wrong option. → 1/3
    4. Startup scope discipline — 1/3 removes an unproduced surface; 2 declares-and-maintains for zero pull. → 1/3

    Recommendation: reading 1 with the ADR-0049 arm applied to :1066 — make :1066 read the resolved object block only, deleting the view-schema-first read (no producer). Escalated rather than adjudicated because the same-family keys received per-key maintainer rulings and this one shades into permissions semantics.


    Generated by Claude Code

  2. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    CollaboratorAuthor

    Claim: PM loop round 16

    Session: session_01RV6yuVCxymHYE16PL9vQkE
    Branch: claude/issue-5240-user-actions-non-author-key
    Worktree: objectui-issue-5240
    Domain: repo:objectui
    File surface: packages/plugin-grid/src/ObjectGrid.tsx + gridNonAuthorKeys.test.tsx (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus
    Clause-②: no — userActions is not declared on the view authoring surface today (the spec's strictObject rejects it by name), and the ruling found zero producers. Removing an undeclared read narrows nothing that was ever contracted. ⚠️ If you find a producer, that falsifies the ruling's premise — stop and report the fork, do not proceed.
    Serial constraints cleared: packages/plugin-grid is free — PR #5314 (#4815), which held bulkParamToField.ts, merged at 13:22. In-flight: #5254 (packages/components/src/renderers/form/** — surface confirmed by #5266's dev in its scope check), #5309 (packages/app-shell), plus PR #5323 (apps/console/vite.config.ts) awaiting CI.

    The ruling — maintainer, 2026-08-19, verbatim 「全部接受」. Reading 1 + the ADR-0049 arm.

    userActions is non-author surface, deliberately unlisted. The object's userActions block — the documented object-metadata channel feeding resolveRowCrudAffordances — is the one legitimate source.

    1. Make the ObjectGrid.tsx:1066 read consume the resolved object block only — delete the view-schema-first (schema as any).userActions ?? resolvedSchema read. An undeclared view-level key must not be able to change the server $select projection: that is the preview-works / save-refuses trap.
    2. Add an exemption comment at the surviving read site naming the channel, per the PR test(plugin-grid): record the three grid keys ruled non-author surface, and stop at the fourth #5241 pattern. Read that PR and match it — a differently-shaped exemption comment is a second convention.
    3. Extend gridNonAuthorKeys.test.tsx.

    ⛔ Reading 2 (declaring it on the author surface) is rejected. The spec's strictObject rejects the key by name, and zero producers author it — declaring here alone publishes a key the save gate refuses.

    ⚠️ On record: a prior card's evidence was false

    The ruling states it explicitly:

    #5091's evidence claim that this key was already declared was false; this ruling supersedes that claim.

    So ⛔ do not resolve any apparent contradiction by trusting #5091's description of GRID_QUERY_INPUTS. Re-derive the actual declaration surfaces yourself and report what you find. Two reads exist and they differ in legitimacy: :864 is the documented non-author object-block channel; :1066 is the view-schema-first override feeding $select. Only the second one goes.

    Verification

    • Re-derive on current origin/main first — line numbers are from 2026-08-18 and main has taken ~18 merges today. ⛔ Do not trust :864 / :1066 as line numbers; find the two reads by reading.
    • Confirm the zero-producer claim yourself, and counter-probe it with a known-present neighbouring key — an unprobed zero is not a reading.
    • Reverse-verify: revert with tests kept, predict exactly which assertions go red and name them BEFORE running, then report predicted-vs-observed. Any assertion green on both legs pins nothing — say so rather than counting it.
    • ⛔ Run vitest from the repository root; do not rely on assertCanonicalVitestInvocation to catch a package-cwd run (objectui#5313: it does not cover the 11 packages owning a standalone vitest.config.ts, and plugin-grid is measured to be one of them).
    • State whether a build artifact sits between your edit and the thing under test.

    Changeset: patch on @object-ui/plugin-grid. ⛔ NEVER declare major — objectui's major is pinned to @objectstack's major.
    ⛔ Do not edit content/docs/releases/, docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md.


    Generated by Claude Code

  3. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    CollaboratorAuthor

    Dev report — PM round 16, session session_01RV6yuVCxymHYE16PL9vQkE. STOPPED at the declared fork condition. No PR.

    The claim comment's ⚠️ fired: "If you find a producer, that falsifies the ruling's premise — stop and report the fork, do not proceed." I found two live producers, and the key turns out to be declared on the spec's view-document surface. Reporting rather than implementing.

    Re-derived declaration surfaces (measured on origin/main @ 8477be5d2)

    I did not trust #5091's account, and I also did not trust this card's own body. Both are partly wrong.

    surface verdict evidence
    GRID_QUERY_INPUTS (plugin-grid/src/index.tsx) ❌ absent grep -n userActions packages/plugin-grid/src/index.tsx — no hit
    ComponentPropsMap['object-grid'] (@objectstack/spec@17) ❌ rejects by name safeParse returns unrecognized_keys: ["userActions"]
    ListViewSchema (@objectstack/spec/ui) ✅ DECLARED ListViewSchema.safeParse({name:'my_view',label:'My View',columns:[{field:'name'}],userActions:{search:true,sort:true}}) → success: true
    packages/types/src/zod/objectql.zod.ts:472 ✅ declared userActions: UserActionsSchema.optional()

    So the card's framing — "three declaration surfaces deny it" — holds only for the component-props surfaces. On the view-document surface the key is spec-legal and saveable today.

    The two producers (the ruling says there are zero)

    1. packages/react/src/spec-bridge/bridges/list-view.ts:206 — if (spec.userActions) node.userActions = spec.userActions; inside bridgeListView, which builds a node of type: 'object-grid' (line 173). Live, not dead code: registered at packages/react/src/spec-bridge/SpecBridge.ts:20 (this.register('list', bridgeListView)) and pinned by a test — packages/react/src/spec-bridge/__tests__/P1SpecBridge.test.ts:94 "should pass through userActions". This node is handed straight to ObjectGrid as schema, i.e. it feeds the exact read this card asks me to delete.
    2. packages/app-shell/src/views/ObjectView.tsx:1902-1904 — builds userActions unconditionally as an object literal on the view schema:
      userActions: {
          ...(normalizeListViewSchema(listSchema ?? {}) as { userActions?: object }).userActions,
          ...(normalizeListViewSchema(viewDef ?? {}) as { userActions?: object }).userActions,
      },
      
      Comment above it: "Toolbar policy — one vocabulary (Migrate the remaining ListView legacy vocabulary to spec-canonical keys, and audit ObjectView/DetailView (#2231 phases 4–5) #2890)." This is the very file the card's body cites as evidence that no producer exists ("app-shell/src/views/ObjectView.tsx passes no userActions into the grid schema"). It does, on every render.

    Counter-probe (per the order — an unprobed zero is not a reading): the same repo-wide grep surfaces the neighbouring producers in the same file — node.bulkActionDefs at list-view.ts:201 and node.conditionalFormatting at :202. The method is live and symmetric; the zero is refuted, not unproven.

    What is actually wrong here — a key-name COLLISION, not an unlisted key

    The two userActions are different shapes sharing one name:

    • View-level = UserActionsConfigSchema — toolbar toggles. Keys: sort, search, filter, refresh, rowHeight, addRecordForm, editInline, buttons. It rejects edit by name: UserActionsConfigSchema.safeParse({edit:{visibleWhen:'x'}}) → unrecognized_keys: ["edit"].
    • Object-level = CRUD affordance blocks {edit, delete, create} carrying visibleWhen / disabledWhen predicates (objectui#2614), which is what resolveRowCrudAffordances consumes at the :864 read.

    And listViewPredicates (packages/core/src/utils/predicate-fields.ts) expects the object shape:

    for (const override of Object.values(view.userActions ?? {})) {
      if (!override || typeof override !== 'object') continue;
      const o = override as Record<string, unknown>;
      preds.push(o.visibleWhen, o.disabledWhen);
    }
    

    Feed it the toolbar block {search:true, sort:true} and every value is a boolean, so every iteration hits continue — zero predicates harvested. The ?? at ObjectGrid.tsx:1066 therefore lets a legitimately-authored, spec-valid, bridge-produced toolbar-policy block shadow the object's CRUD predicates, silently dropping their fields from $select. That is precisely objectui#3501's failure: CEL faults No such key on the absent field, fail-closed, and the row Edit/Delete button disappears for everyone with nothing pointing at the projection.

    So the ruling's remedy is right, and more urgently right than the card knew — but its three stated premises are all falsified: not "zero producers", not "an undeclared view-level key", and not an ADR-0049 dead override. The override is live and actively harmful.

    Why I could not just land the ruled patch anyway

    The ruling's execution list is three items, and two of them would pin a false statement:

    • Item 2 — the exemption comment "naming the channel", per the PR test(plugin-grid): record the three grid keys ruled non-author surface, and stop at the fourth #5241 pattern. That pattern is explicitly evidence-shaped: "The producer is the evidence: 'non-author' is a claim about who WRITES the key, so a reader can check it rather than trust it." Every entry in test(plugin-grid): record the three grid keys ruled non-author surface, and stop at the fourth #5241's NON_AUTHOR_KEYS carries a real producer. Here the truthful comment is not "non-author surface, deliberately unlisted" — it is "a different key of the same name, declared on ListViewSchema, that must not be read as the object's CRUD block". Writing the ruled wording would be a checked-in falsehood.
    • Item 3 — extend gridNonAuthorKeys.test.tsx. (The file exists on main; it has no userActions coverage today.) Its assertion shape is "not published / rejected by name / unknown-prop / still read". Asserting "no producer authors this" for userActions would pin something two live producers contradict.
    • Scope. The worse instance is outside this card's file surface: packages/plugin-list/src/ListView.tsx:1591 carries the identical expression (schema as any).userActions ?? (objectDef as any)?.userActions, and on the app-shell path its left operand is always a truthy object literal, so the object's CRUD block is never reached there. Fixing only plugin-grid leaves the unconditional instance live. (I did not forward-file this as a separate card: it is the same defect the maintainer is being asked to rule on, and filing would pre-commit a scope decision that is theirs. Flagging it here so it is not lost.)

    Note on the two paths, since they differ: ListView does not forward userActions to its child object-grid node (checked baseProps and the grid branch, ListView.tsx:1860-1950), so on the app-shell path ObjectGrid:1066 currently falls back correctly and it is ListView:1591 that breaks. On the SpecBridge path the bridge emits object-grid directly with userActions attached, so ObjectGrid:1066 — this card's read — is the one that breaks.

    The decision I need

    Q1 — the exemption comment's content. The channel is not "non-author surface"; it is a name collision between two declared keys.

    • A. Delete the view-schema-first read as ruled, but write the comment/test around the measured reason (view userActions = spec UserActionsConfigSchema toolbar policy; object userActions = CRUD blocks; the object block is the only source listViewPredicates can interpret). Same diff shape as ruled, truthful evidence.
    • B. Keep the ruled wording. Cheapest, but checks in a claim the repo contradicts, on the one surface test(plugin-grid): record the three grid keys ruled non-author surface, and stop at the fourth #5241 built specifically to be checkable.
    • Recommendation: A. It preserves the ruling's decision and its diff while making the artifact survive the next census. On the three axes: real business need — there are real authored documents carrying view userActions (spec-legal, two producers), so this is a live projection bug, not a speculative surface; long-term soundness — one meaning per channel is exactly contract-first, and a comment that names the collision is what stops the ?? being re-added; AI-error-resistance — an AI author writing spec-legal userActions:{search:true} today silently breaks row-action gating with a success receipt, the worst available failure shape.

    Q2 — scope. plugin-list/src/ListView.tsx:1591 has the identical collision, unconditionally triggered.

    • A. Widen this card's file surface to both read sites (one defect class, one mechanical fix, same gate family) — but that touches packages/plugin-list, outside my dispatched surface.
    • B. Fix plugin-grid here; file plugin-list as a sibling card.
    • C. Fix at the source instead: stop bridgeListView writing a key ComponentPropsMap['object-grid'] rejects by name, and/or rename one of the two colliding keys.
    • Recommendation: B for landing speed, but C is the contract-first answer and should be filed regardless: a producer emitting a key the consumer's own strict props schema refuses by name is the generator-side defect, and per basic rule 5 the fix belongs at the producer, not in consumer tolerance. C is a spec-coordination change and needs its own ruling.

    Q3 — does the :864 object-block read need anything at all? It consumes objectSchema.userActions (the object channel) and is uncontested. If the answer to Q1 is A, the exemption comment arguably belongs at :864 (the surviving legitimate read) and at the collision point, which is what the ruling intended. No further decision needed if A is chosen.

    Verification notes

    • Reverse-verification: not performed, and not applicable — I made no edit, so there is nothing to revert and no predicted-vs-observed to report. Saying so rather than filling the template.
    • Build artifact between edit and test: none in what I measured. All evidence is source reads plus safeParse probes against the published @objectstack/spec@17 in node_modules — no local package build sits in that path, so no stale-dist risk applies. (Had I proceeded, one would sit between an edit and the test: listViewPredicates lives in @object-ui/core, which plugin-grid consumes through dist.)
    • No vitest run: with no edit there is nothing to verify, and a green pre-existing suite would prove nothing about the fork.
    • Worktree objectui-issue-5240 removed; branch claude/issue-5240-user-actions-non-author-key pushed empty (write-route probe, HTTP 200) and left in place as the claim's landing marker.

    Generated by Claude Code


    Generated by Claude Code

  4. removed their assignment
    on Aug 19, 2026
  5. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    CollaboratorAuthor

    PM: roll-back clause fired — ruling premise falsified, card returned to needs-user-decision

    Dispatch outcome: needs_decision, premise_still_valid: false, zero edits, no PR. The dev stopped exactly at the fork condition my dispatch order declared verbatim — "⚠️ If you find a producer, that falsifies the ruling's premise — stop and report the fork, do not proceed." It did, and it declined to fabricate a reverse-verification it had not run ("stating that rather than filling the template"). That is the correct outcome, not a failed run.

    Full evidence: the dev's report above (comment 5343911290). I am not re-adjudicating this — the ruling being overturned is the maintainer's, and the PM loop's roll-back clause is explicit: dev stops, card returns to needs-user-decision, PM reports the fork, ⛔ never silently re-rules.

    Label state moved in one write: pm:dispatched → needs-user-decision; assignee cleared. Branch claude/issue-5240-user-actions-non-author-key is pushed empty as the claim's landing marker; worktree removed.

    What was measured (origin/main @ 8477be5d2, verbatim from the dev)

    ListViewSchema.safeParse({name:'my_view',label:'My View',columns:[{field:'name'}],
                              userActions:{search:true,sort:true}})   → success: true
    ComponentPropsMap['object-grid'].safeParse({objectName:'a',
                              userActions:{search:true}})             → success: false, unrecognized_keys:['userActions']
    UserActionsConfigSchema.safeParse({edit:{visibleWhen:'x'}})       → success: false, unrecognized_keys:['edit']
    UserActionsConfigSchema.shape keys = sort, search, filter, refresh, rowHeight,
                                         addRecordForm, editInline, buttons
    

    Two live producers write userActions onto the view schema:

    • packages/react/src/spec-bridge/bridges/list-view.ts:206 — inside bridgeListView, which emits a type:'object-grid' node, is registered at SpecBridge.ts:20, and is pinned by P1SpecBridge.test.ts:94.
    • packages/app-shell/src/views/ObjectView.tsx:1902-1904 — builds userActions unconditionally as an object literal. This is the very file this card cited as proof that no producer exists.

    Zero-producer counter-probe (a zero is not a reading until a known-present neighbour is probed through the same method): the same grep surfaces node.bulkActionDefs (list-view.ts:201) and node.conditionalFormatting (:202) in the same file — the method is live and symmetric, so the original zero is refuted, not merely unproven.

    Which premises died, and which part of the ruling survives

    Ruling premise Status
    "zero producers author it" False — two live producers, one of them in the file cited as the proof
    "an undeclared view-level key" False — ListViewSchema accepts it on the view-document surface
    Reading 3, ADR-0049 dead override False — the override is not dead
    #5091's claim about GRID_QUERY_INPUTS / ComponentPropsMap['object-grid'] Confirmed false independently — the key is absent there and rejected by name. The card is right about the component-props surfaces and wrong about the view-document surface.
    Item 1 of the ruling — delete the view-schema-first read at :1066 Survives, and is more right than the card knew (see below)

    The real defect is a key-name collision, not an unlisted key. View-level userActions is UserActionsConfigSchema — toolbar policy (sort/search/filter/refresh/rowHeight/addRecordForm/editInline/buttons), which rejects edit by name. Object-level userActions is the CRUD blocks {edit,delete,create} carrying visibleWhen/disabledWhen, and that is what listViewPredicates actually reads. Its loop skips every non-object value, so a spec-valid toolbar block shadows the object's CRUD predicates through the ?? and silently zeroes their fields out of $select — objectui#3501's fail-closed CEL fault, reached with a success receipt.

    So the ruled remedy is right; items 2 and 3 of the ruling would check in and pin a statement the repo contradicts — on the one artifact (#5241's exemption-comment pattern, whose own header says the producer IS the evidence) built specifically to make such claims checkable.

    The fork — three questions for the maintainer

    Q1 — what should the exemption comment (and the test pinning it) actually say?

    • A (dev recommends). Delete the view-schema-first read exactly as ruled, but write the comment and test around the measured reason: view userActions = toolbar policy, object userActions = CRUD blocks, only the object block is interpretable by listViewPredicates. Same diff the ruling ordered, truthful evidence.
    • B. Keep the ruled wording ("non-author surface, deliberately unlisted"). Cheapest, but checks a falsifiable claim into the census artifact.

    Q2 — scope. packages/plugin-list/src/ListView.tsx:1591 carries the identical (schema as any).userActions ?? (objectDef as any)?.userActions, and on the app-shell path its left operand is always a truthy object literal (ObjectView builds it unconditionally), so the object's CRUD block is never reached there. That instance is worse than the one this card targets.

    • A. Widen this card to both read sites (one defect class, one mechanical fix), accepting the touch on packages/plugin-list.
    • B (dev recommends for landing speed). Fix plugin-grid here; file plugin-list as a sibling card.
    • C (dev recommends filing regardless). Fix at the producer: stop bridgeListView writing onto an object-grid node a key that ComponentPropsMap['object-grid'] rejects by name, and/or rename one of the two colliding keys. Contract-first answer — a producer emitting a key the consumer's own strictObject refuses is a generator-side defect — but it is a spec-coordination change needing its own ruling, so it should not block B.

    Q3 — does the surviving object-block read (ObjectGrid.tsx:864) need anything of its own?

    • A. Nothing; it is the uncontested legitimate channel.
    • B (dev recommends, if Q1=A). Carry the collision comment there too — that is what "exemption comment at the surviving read site" most plausibly meant, and naming the collision at both reads costs nothing.

    Deliberately NOT done

    The plugin-list instance was not filed as a card — by the dev, and I am holding the same line: filing it pre-commits Q2's scope decision, which is the maintainer's. It is recorded here so it cannot be lost. If Q2 = B, I file it; if Q2 = A, it folds into this card. The bridgeListView producer break (Q2/C) is likewise unfiled pending the ruling.


    Generated by Claude Code

  6. os-zhuang commented on Aug 20, 2026

    @os-zhuang
    Contributor

    Maintainer ruling recorded (2026-08-20, live decision-inbox session with the triage seat, session session_01PjAP6vbcsg2yMtvySPv1Qo)

    Ruled: Q1=A · Q2=B · Q3=B. Provenance: maintainer confirmed the four presented recommendations, verbatim: 「4 张 同意」.

    • Q1=A: delete the view-schema-first read at ObjectGrid.tsx:1066 exactly as originally ruled, but the exemption comment and its pin state the measured reason — view-level userActions is toolbar policy (UserActionsConfigSchema), object-level userActions is the CRUD-predicate block, and only the object block is interpretable by listViewPredicates; the collision, not "deliberately unlisted", is the truth the census artifact checks in.
    • Q2=B: this card fixes plugin-grid only; the identical (worse) plugin-list/src/ListView.tsx:1591 instance is filed as a sibling card by the repo:objectui seat per its holding note, and the producer-side question (bridgeListView emitting a key ComponentPropsMap['object-grid'] rejects by name — fix at producer or rename a colliding key) is filed as its own spec-coordination card routed through triage. Neither blocks this landing.
    • Q3=B: the surviving object-block read (:864) carries the same collision comment — naming it at both reads costs nothing.

    State transition in the same stroke: needs-user-decision → pm:queue (re-dispatchable; the prior dev's evidence and diff shape are on this thread).


    Generated by Claude Code

  7. os-support-ai commented on Aug 20, 2026

    @os-support-ai
    CollaboratorAuthor

    Q2=B discharged: the sibling card is #5398.

    repo:objectui execution seat, session session_01RV6yuVCxymHYE16PL9vQkE. The ruling assigned this filing to this seat — "filed as a sibling card by the repo:objectui seat per its holding note" — and it is now done. Filed unassigned, bug + pm:queue, carrying the ListView.tsx:1591 collision and the ObjectView.tsx:1902-1904 unconditional-construction measurement that makes that instance worse than this card's.

    ⚠️ It notes a serial dependency on this card: land #5240 first (or read its final diff), so both read sites get the same shape and the same collision comment rather than two spellings of one fix.

    The producer half (bridgeListView emitting a key ComponentPropsMap['object-grid'] rejects by name) is not in #5398 — the ruling routes that through triage as its own spec-coordination card, and this seat has not filed it.

    For the record on why it is only being filed now: it was held while Q2 was open, deliberately, since filing pre-commits the scope decision. After the ruling it was delayed a few hours more by a shared-identity API rate limit that made the duplicate search impossible — and filing without that search is how #5387 became a duplicate earlier in this session, so it waited. The search has now run with a counter-probe (the same method returns 6 cards in the userActions family — #4421, #4646, #5153, #4419, #4213 — and none covers this collision), so the zero is a reading rather than an outage.


    Generated by Claude Code

  8. os-support-ai commented on Aug 20, 2026

    @os-support-ai
    CollaboratorAuthor

    Claim: PM loop round 21
    Session: session_01RV6yuVCxymHYE16PL9vQkE
    Branch: claude/issue-5240-grid-userActions-collision
    Worktree: objectui-issue-5240
    Domain: repo:objectui
    File surface: packages/plugin-grid/src/ObjectGrid.tsx (the :1066 view-schema-first read and the surviving :864 object-block read) plus the #5241-pattern exemption comment and its pin (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus
    Clause-②: no — removes a read of an undeclared surface and records the measured reason; does not widen any public surface
    Serial constraints cleared: ⛔ #5398 is the sibling on plugin-list/src/ListView.tsx:1591 and is deliberately NOT dispatched with this card — it is serial behind it so both read sites end up with one shape, not two spellings. ⛔ #5378 also touches plugin-grid and is not in this batch for the same reason. The other three cards this round (#3318 packages/fields, #5301 packages/plugin-dashboard, #5389 packages/auth) are disjoint packages. No open PR touches plugin-grid.

    Dispatched on the maintainer's 2026-08-20 ruling recorded above (Q1=A · Q2=B · Q3=B, 「4 张 同意」).


    Generated by Claude Code

  9. os-support-ai commented on Aug 20, 2026

    @os-support-ai
    CollaboratorAuthor

    Dev report — PM round 21, session session_01RV6yuVCxymHYE16PL9vQkE. Draft PR #5426 open at efc95b643.

    {
      "issue": 5240,
      "status": "done",
      "branch": "claude/issue-5240-grid-userActions-collision",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5426",
      "premise_still_valid": true,
      "summary": "Implemented the 2026-08-20 ruling (Q1=A, Q2=B, Q3=B). ObjectGrid's $select predicate harvest now reads the resolved OBJECT block only — the view-schema-first `(schema as any).userActions ??` operand is deleted — and both userActions read sites (the harvest, and the resolveRowCrudAffordances call) carry a comment naming the measured reason: view-level userActions is toolbar policy (UserActionsConfigSchema: sort/search/filter/refresh/rowHeight/addRecordForm/editInline/buttons, which rejects `edit` by name), object-level userActions is the CRUD-predicate block carrying visibleWhen/disabledWhen, and only the object block is interpretable by listViewPredicates. The phrase 'non-author surface, deliberately unlisted' appears nowhere. I re-derived every premise of the ruling on origin/main @ bd977f86f before editing and all of them held: UserActionsConfigSchema shape keys are exactly the eight toolbar names and it answers unrecognized_keys for edit/delete/create; ListViewSchema accepts the toolbar block and refuses the CRUD block at path userActions; ComponentPropsMap['object-grid'] refuses userActions by name; both producers are still live (bridgeListView copies it onto a type:'object-grid' node, ObjectView builds one unconditionally). gridNonAuthorKeys.test.tsx gains 11 assertions pinning each clause of the comment, in its own section that explicitly does NOT claim the key is unwritten (two producers write it) — including the producer-as-evidence assertion through SpecBridge.transformListView. patch changeset on @object-ui/plugin-grid. plugin-list and app-shell untouched (Q2=B).",
      "tests": "All at efc95b643 (final commit; `git rev-parse --short HEAD` on the tree every command below ran against). Repo-root vitest per AGENTS.md. `pnpm exec vitest run packages/plugin-grid/` -> 'Test Files 81 passed (81) / Tests 737 passed (737)'. `pnpm exec vitest run packages/plugin-list/` (downstream consumer of ObjectGrid) -> '42 passed (42) / 632 passed (632)'. `pnpm exec vitest run apps/console/src/__tests__/registry-inputs-spec-parity.test.ts` (the census parity test the card names) -> '65 passed'. `pnpm --filter @object-ui/plugin-grid type-check` -> exit 0, output echoed the script name and both tsc invocations, so it was not a zero-match silent pass; it needed `pnpm --filter '@object-ui/plugin-grid^...' build` first because a fresh worktree has no dependency .d.ts. `pnpm --filter @object-ui/plugin-grid lint` -> 0 errors, 641 warnings (pre-existing baseline). check:control-bytes, check:spec-symbols, check:phantom-deps, check:self-import, check-changeset-presence.mjs, check-changeset-no-major.mjs -> all OK. Control-byte self-scan over the three changed files: no match. REVERSE-VERIFICATION, fix committed first, predicted before running: LEG 1 (restore the view-first `??`) predicted exactly one red, the assertion '...and a spec-legal toolbar block on the view does not knock it out'; observed '1 failed | 30 passed', that assertion, `expected [ 'id', 'name' ] to include 'status'` — matches. LEG 2 (delete the userActions line from the harvest entirely) predicted two red, both projection cases; observed '2 failed | 29 passed' — matches. Leg 2 is what makes the first projection case non-vacuous: it is green on both legs of leg 1, so alone it pins the read's existence rather than this fix, and I counted it on that basis only. BUILD ARTIFACT BETWEEN EDIT AND TEST: none, proven not assumed — vitest.config.mts resolve.alias maps every @object-ui/* specifier to that package's src/, and this worktree had no dist/ anywhere while every suite ran (packages/core/dist, packages/react/dist, packages/plugin-grid/dist all absent), so source is provably what executed; @objectstack/spec is the published 17.0.0 from node_modules and nothing local builds it. Both ablation legs therefore needed no rebuild, and the restore leg was verified byte-identical to the commit (`git status --porcelain` empty, `git diff HEAD` empty) before the final green runs. CI not awaited, per the standing dispatch contract.",
      "open_questions": [],
      "out_of_scope_findings": [
        "NOT filed by me, and still unfiled as of this report: the producer-side half — bridgeListView writing `userActions` onto an `object-grid` node whose own ComponentPropsMap entry refuses the key by name. The 2026-08-20 ruling routes it through triage as its own spec-coordination card, so filing it here would pre-empt that seat. Duplicate search run with a counter-probe: the same query returns #5398, #5340 (bridgeListView accessorKey/header) and #4585 (SpecBridge exportOptions), so the zero is a reading, not an outage.",
        "No new out-of-scope defect found. #5398 (the plugin-list instance) was already filed by this seat before dispatch and is deliberately serial behind this PR; its fix should copy this PR's shape and comment wording rather than invent a second spelling."
      ]
    }

    Notes for the PM, outside the machine-read block:

    • The PR body carries the full measurement table, the two-leg reverse-verification and the no-build-artifact proof.
    • #5398 is referenced in the PR body without any closing keyword; it remains open.
    • Worktree objectui-issue-5240 removed cleanly without --force, which is itself the evidence that nothing uncommitted was left behind.

    Generated by Claude Code


    Generated by Claude Code

  10. os-support-ai commented on Aug 20, 2026

    @os-support-ai
    CollaboratorAuthor

    ACCEPT — PR #5426 enqueued

    repo:objectui execution seat, session session_01RV6yuVCxymHYE16PL9vQkE. All 21 check runs completed: 19 success, 2 skipped. Shards 6m28s — 9m22s, Type Check 6m13s, Lint 5m00s. Marked ready, then auto-merge (SQUASH).

    Implements the maintainer's 2026-08-20 ruling (「4 张 同意」) — Q1=A · Q2=B · Q3=B.

    The part that mattered most: the pin does not check in the falsified claim

    This card fired the roll-back clause once already. The original ruling's premises — "zero producers", "an undeclared view-level key", "an ADR-0049 dead override" — were falsified by measurement, one of them in the very file the card cited as proof. The action survived; the stated reasons did not. Q1=A exists so the census artifact does not inherit a claim the repository contradicts.

    Verified by the PM against the diff: the phrase "non-author surface, deliberately unlisted" appears nowhere as an assertion. Every match for that wording is the docblock explicitly rejecting it — "why it is NOT a fifth non-author key", "does NOT assert 'no producer writes this' (two do)", "the measurement that killed the original 'deliberately unlisted'". The 11 new assertions pin what was measured instead: the view schema refuses edit/delete/create by name while accepting the toolbar vocabulary; a view document carrying the toolbar block is spec-legal while the CRUD block is refused; SpecBridge.transformListView really copies it onto the object-grid node — the producer as evidence, proving the opposite of the NON_AUTHOR_KEYS table it sits beside; listViewPredicates harvests zero fields from the toolbar block; and the object's operand reaches $select with a toolbar block present.

    One piece of test reasoning worth recording

    Two ablation legs, both predicted before running and both matched (1 red, then 2 red). The second leg — deleting the userActions line from the harvest entirely, the "tidy finish" mutation — exists for a specific reason the dev spelled out: the first projection assertion is green on both sides of leg 1, so on its own it pins the read's existence, not this fix. Leg 2 is what gives it meaning.

    Going looking for the assertion in your own test that would otherwise be decorative, and adding the leg that makes it bite, is the habit this lane wants.

    Also verified

    • Surface is 3 files, plugin-grid only. ⛔ plugin-list, app-shell and .github/workflows/ all untouched — Q2=B held.
    • No build artifact between edit and test, proven rather than asserted: the worktree had no dist/ anywhere while the suites ran (packages/core/dist, packages/react/dist, packages/plugin-grid/dist all absent) and vitest.config.mts aliases every @object-ui/* to that package's src/, so source is provably what executed. @objectstack/spec is the published 17.0.0 from node_modules.
    • Downstream consumer checked: plugin-list 632 tests green, and ListView reads schema.userActions as toolbar policy without forwarding it into its child grid node — so the app-shell path is unaffected.
    • The producer-side half (bridgeListView writing a key ComponentPropsMap['object-grid'] refuses by name) was not filed by the dev, correctly: the ruling routes it through triage, and filing it would pre-empt that seat. A duplicate search with counter-probe was run and reported.

    Unblocks

    #5398 — the plugin-list ListView.tsx:1591 sibling, deliberately serial behind this. Its fix should copy this PR's shape and comment wording rather than invent a second spelling; that was the whole point of sequencing them.

    Card closes on merge via Fixes #5240.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions