Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

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

Closed

Description

@os-zhuang

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:

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-action surfaces vs inline surfaces, mirroring ActionSchema / 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 bodyExtra and bodyShape presented.
  • C — spec-side. Have the spec's ActionSchema mark 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

  1. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    ContributorAuthor

    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

  2. self-assigned this
    on Aug 10, 2026
  3. yinlianghui commented on Aug 10, 2026

    @yinlianghui
    Contributor

    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).


    Generated by Claude Code

  4. yinlianghui commented on Aug 10, 2026

    @yinlianghui
    Contributor

    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, ExtractionError not process.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 renamed execute() call). The gate's first run immediately out-measured the filing: action:icon and action:group drop label/description like the menu AND both drop resultDialog — #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/recordIdField are read only under the rowRecord guard, 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". actionParams is not owed (not authorable; the runner accepts array params — 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 own label — 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

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions