Repository navigation
objectui: no gate diffs the action renderers' forward whitelists against the keys the runtime actually reads — every new spec action key is silently dropped until five lists are edited #4050
Description
Migrated from objectstack-ai/objectstack#6975 under the file-at-destination ruling (objectstack-ai/objectstack#7167, maintainer 2026-08-10). Originally filed 2026-08-09T07:03:40Z. The full prior thread — including triage rulings and hold/restart conditions — remains on the source issue and MUST be read before acting on this card.
Carried out of objectstack#6938 (bodyShape), whose body proposed this and whose triage marked it a non-binding candidate for the implementing dev. Filed standalone so the suggestion survives objectstack-ai/objectstack#6938 closing, and deliberately not implemented in objectui PR #3932 — the reasons are the substance of this card.
The recurring shape
Every action renderer forwards an explicit whitelist of keys to the ActionRunner. That is a deliberate design, not an oversight: a key no renderer honours must not look wired. Its cost is that a NEW spec key stays invisible until five separate lists are edited, and nothing fails while they are not — the key parses, publishes, and reads as honoured.
Confirmed instances, same face each time:
bodyExtra— objectstack#6837, objectui PR fix(cli,driver-sql): defer migrate DDL until confirmed + detect SQLite occupancy (#3917) objectstack#3924. Payload dropped one hop before the runner.bodyShape— objectstack#6938, objectui PR seed-loader: a dropped reference FIELD is invisible in the summary —totalErroredcounts dropped records only, so the boot banner reads clean objectstack#3932. Declared{ wrap: 'data' }sent a flat body.openIn,resultDialog,undoable,locations— each carries an in-comment note at its forward site recording its own instance.
Four of the six were found by a human reading the lists side by side. There is no gate.
Why PR objectstack-ai/objectstack#3932 did not land the proposed pin
The proposal — diff the renderers' forwarded key sets against the spec keys the runtime actually reads — needs two inputs, and only one of them is mechanically derivable.
Derivable: the forwarded key sets. AST-read the object literal each renderer passes to execute(...). packages/core/src/actions/__tests__/actionKeys.pin.test.ts already does this class of walk (it reads ActionDef's members off the interface with the typescript compiler API), so the technique is established in-repo.
Not derivable: "the keys the runtime reads". The read sites span packages/core (ActionRunner) and packages/app-shell (useConsoleActionRuntime.apiHandler, RecordDetailView.apiHandler), and a git grep 'action\.' over them returns a large set of action.name / action.objectName / action.api / action.method hits with no way to separate "body-path key a renderer must forward" from "mechanic the runner resolves itself". Grepping produces a false-positive list, not a contract.
And the omissions are load-bearing, not noise. recordIdParam is forwarded by none of the five and that is correct — objectstack-ai/objectstack#6938 checked it: the apiHandler reads it only under if (rowRecord && action.recordIdParam), and rowRecord arrives only from the spread-based hosts (DeclaredActionsBar / RelatedRecordActionsBridge / ObjectGrid), which carry recordIdParam too. It is unreachable on the declared renderers, not broken. element:button's omission of bodyShape is likewise correct and deliberate: its list mirrors spec's InlineActionSchema pick list, which does not include the key.
So a mechanical diff needs a hand-maintained justified-omission registry beside it — which is itself the drift-prone list this whole thread is about, one level up. That is a design decision about where the contract for "which keys each renderer surface owes the runtime" should live, not a test to bolt on, and it wants a maintainer's call rather than a dev's guess mid-fix.
Directions worth weighing (not a recommendation to implement as-is)
- A — declare the surface, generate the lists. Put the per-surface key set in one declared table (
declared-actionsurfaces vsinlinesurfaces, mirroringActionSchema/InlineActionSchema), have the renderers forward off it, and let the pin re-derive both halves. Removes the five hand-maintained lists entirely rather than testing them. Highest cost, and the only option that makes the failure structurally impossible instead of merely loud. - B — pin cross-renderer agreement only. AST-read the four declared renderers' lists and assert they agree on their shared subset. No omission registry needed (the renderers are compared against each other), and it catches the real recurrence mode of "added to one list, forgotten in three". Does not catch a key missing from all four — which is exactly how
bodyExtraandbodyShapepresented. - C — spec-side. Have the spec's
ActionSchemamark which keys are renderer-forwardable and publish that as data the pin consumes. Puts the contract at the producer, where #0.1 says it belongs; needs a spec change and cross-repo sequencing.
B is cheap and partial; A and C address the actual failure but are contract-shaping. objectstack-ai/objectstack#6938's PR deliberately made no guess between them.
Grade
Observation-class: nothing a user hits today that is not already tracked by the per-key cards. This is preventive infrastructure for the next key — finding, no pm:queue, unassigned for triage.
Activity
os-zhuang commentedon Aug 10, 2026
Maintainer ruling (2026-08-10, directed in session
session_01BPWqbmEFU8gJepBJTHESXd): approved — build the gate.Diff each action renderer's forward whitelist against the keys the runtime actually reads, as a sibling of
check:react-declaration-parity. A new spec action key missing from a whitelist must be a red check, not a silent drop; extraction failure is red, never a silent pass (#4690 anti-pattern).needs-user-decision→pm:queue.Generated by Claude Code
yinlianghui commentedon Aug 10, 2026
CLAIM (objectui whole-repo seat PM, session
session_017Qqyix2QcnpUC9XeYVDzx3) — dispatching one dev agent for the pack #4050 + #4192 (per the packing note on #4192: the gate and the concrete gap it catches are one change).claude/issue-4050-action-whitelist-gate, worktreeobjectui-issue-4050(M container).check:react-declaration-parity, extraction failure red never silent). Red-first sequencing: the new gate must first go red naming bug(components): action:menu's execute payload is narrower than action:button's — an overflow action loses its dialog title, description, Undo and record-id field #4192's measured menu gap, then the menu whitelist fix turns it green — with the justified-omission registry carrying evidence-cited entries (recordIdParamunreachable-by-design,element:buttonmirrorsInlineActionSchema), per this card's own analysis.ObjectGrid.tsxand the spread-based hosts are AST-read subjects only — ObjectGrid bulk-bar Clear leaves row checkboxes ticked — onClearSelection omits the selectionResetKey bump that resetSelection() performs (#3056 drift) #4140's agent owns ObjectGrid edits in parallel.Generated by Claude Code
yinlianghui commentedon Aug 10, 2026
ACCEPT (objectui seat PM, session
session_017Qqyix2QcnpUC9XeYVDzx3, review of record) — PR #4207, closing #4050 + #4192 as the dispatched pack. Delivered across the 22:43Z host restart: the pre-restart agent built the 694-line gate and the renderer fixes; the continuation agent verified every registry citation itself (all 19 JUSTIFIED + 7 KNOWN_GAPS check out), refactored the gate into the repo's testable-gate shape (pure functions over an injectable root,ExtractionErrornotprocess.exit, 34 synthetic-repo tests so every red the gate can produce is exercised permanently), and corrected the dispatch's own wiring claim — objectui runs its family gates in the Type Check job, not Lint, and the gate is wired there plus the bidirectionally-enforced ci-cd-pipeline doc row.The maintainer's ruling is implemented as written: 5 surfaces diffed against 41 runtime-read keys from 3 consumers; a new authorable+read key missing from a whitelist is a red check (reverse case 2 plants
bodyCodec— the seventh drift, simulated — and every declared surface goes red); extraction failure is RED, never silent (reverse case 4 measured exit=1 on a renamedexecute()call). The gate's first run immediately out-measured the filing: action:icon and action:group droplabel/descriptionlike the menu AND both dropresultDialog— #3646's defect still live on two of four surfaces — all fixed in the red-first two-commit stack.On #4192's re-scope (recorded on that card too): the "loses Undo" claim is DISPROVEN with the deeper evidence —
undoable/recordIdFieldare read only under therowRecordguard, written by exactly five spread-based host sites, none dispatching through these renderers; both keys are held as JUSTIFIED entries the gate fails on if reachability ever changes, which is the honest encoding of "unreachable, not dropped".actionParamsis not owed (not authorable; the runner accepts arrayparams— why the defect was a wrong dialog TITLE, not a missing dialog), pinned latent. The real user-facing fix — dialog titles from the action's ownlabel— is behaviorally pinned and reverse-verified.#4202 (objectName/onClick pre-existing gaps, ratcheted KNOWN_GAPS) graded: stays a
finding— the ratchet means it cannot widen silently, so it queues on its merits, not urgency. CI converged 20/20 zero failures; real changeset. Flipping ready + auto-merge.Generated by Claude Code
ci(gate): diff each action renderer's forward whitelist against the k…