Skip to content

feat(spec): userActions.create/import accept the edit/delete CEL predicate union - #7758

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-7692-useractions-create-import-predicate
Aug 12, 2026
Merged

feat(spec): userActions.create/import accept the edit/delete CEL predicate union#7758
os-zhuang merged 3 commits into
mainfrom
claude/issue-7692-useractions-create-import-predicate

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #7692

The gap

#3076 (objectui#2614) gave userActions.edit and userActions.delete a
boolean-or-predicates union so the built-in row affordances could be gated on
record state. create and import were left as bare booleans, and there is no
other lever for them — so a child object's related-list [+ New] button cannot
be gated on the parent record's state, while the row Edit / Delete beside it
can. On a frozen parent the row actions correctly grey out and [+ New] still
renders. The server-side guard rejects the insert (409), so this is an affordance
leak rather than a data-integrity hole — but it is one an app has no way to close.

The ruling

Maintainer ruling recorded on the issue, 2026-08-11, quoted verbatim and
untranslated:

Ruling: widen. userActions.create — and import, same shape, same
argument — widen to the union edit/delete already carry (boolean | { enabled?, visibleWhen: CEL }), evaluation context identical to
edit/delete's. No new dialect; pure symmetry completion of #3076. The
renderer counterpart (related-list toolbar honoring create.visibleWhen) is
objectui's downstream card, scoped after the spec half lands.

What changed

packages/spec/src/data/object.zod.tscreate and import now take the
same RowCrudActionOverrideSchema union edit/delete already use. Not a
copy, not a narrowed variant: the same schema piece, so there is exactly one
definition of the object form.

userActions: {
  create: { visibleWhen: 'record.version_status == "draft"' },
  import: { enabled: true, disabledWhen: 'record.frozen == true' },
}

enabled keeps the bare boolean's meaning (omitted ⇒ the managedBy bucket
default), visibleWhen is fail-closed, disabledWhen fail-soft — identical to
the row pair. resolveCrudAffordances carries the predicates through as
createPredicates / importPredicates alongside the existing
editPredicates / deletePredicates, via the same normalizeRowCrudOverride
collapse, so a declared predicate is reachable rather than declared-and-inert.

What record.* binds to is NOT the same in the two positions, and the schema says so

This is the one place the card could have shipped a false promise, so it is
stated in the schema rather than implied:

  • edit / delete evaluate per row, against that row's own record. That is
    the docs(design): record create/edit/subtable surface + return-flow model (#2604) #2614 case, and the reason for the RowCrud name — kept as-is for export
    compatibility.
  • create / import gate a record that does not exist yet, so they evaluate
    once per toolbar, against the record in scope where the toolbar renders:
    the host (parent) record on a record page's related list, and nothing at
    all on a standalone object list — where a predicate reading record.* has
    nothing to bind and therefore hides the button under the fail-closed rule.

The describes and the RowCrudActionOverrideSchema docblock spell that out,
including the authoring consequence: gate a toolbar action on parent state only
where a parent is actually in scope; anything else the child row must carry
itself, via the denormalised parent-status snapshot pattern edit/delete
already use. That is the same honesty the row describes carry, not a new promise.

One consumer had to track the widened producer

packages/plugins/plugin-hono-server/src/current-user-endpoints.ts — the
/me/permissions managed-write clamp tested create with a bare
ua.create !== true, while edit/delete beside it already read through
isWriteOptedIn. Left alone, widening create would have made
create: { enabled: true, visibleWhen: … } clamp the create permission hint
off — a silent tightening introduced by this PR, not a pre-existing bug.
create now reads through the same helper, with the same fail-closed rule when
enabled is omitted.

Verification

Reverse verification (via git checkout origin/main -- ... on the two source
files, tests kept; restored with git checkout from the branch — no git stash).
Direction predicted before running, and it held both ways:

  • spec: 6 of 8 new cases red — the object-form parses, the
    resolveCrudAffordances carry-through, and the unknown-key case. That last one
    matters: on the reverted source it fails with
    Invalid input: expected boolean, received object, not on hideWhen
    which is why the case asserts the issue mentions hideWhen rather than only
    asserting success === false. Without that assertion it would have been green
    for the wrong reason on the very code the card targets.
  • 2 green, deliberately: the boolean back-compat case (green by design — that
    is what it pins), and the "a bare string is rejected" case, which guards against
    over-widening and so cannot discriminate under-widening. Recorded rather than
    dressed up as evidence.
  • plugin-hono-server: the clamp case red (expected false to be true) on the
    reverted clamp.

Gates run locally, all green: full @objectstack/spec suite (377 files / 9891
tests), @objectstack/spec typecheck, plugin-hono-server test (18 files /
211 tests) + typecheck, check:generated (13 artifacts up to date),
check:authorable-surface, check:docs, check:api-surface,
check:export-origins, check:merge-driver, check:adr-anchors,
check:spec-parsed-alias, check:nul-bytes.

Generated closure regenerated, not hand-edited: gen:schema (no tracked delta —
RowCrudActionOverride's keys were already registered) and gen:docs
(content/docs/references/data/object.mdx picks up the widened userActions
row and the reworded predicate describes).

Scope

Back-compatible in the strict sense: every payload that validated before still
validates identically, and the boolean-only path still produces no predicate keys
at all. The only newly-accepted shapes are the object forms on create /
import. Minor on @objectstack/spec because the accepted-input surface grows;
patch on plugin-hono-server, which only tracks it.

The renderer half — the objectui related-list toolbar honouring
create.visibleWhen — is the downstream card and is not in this PR.


Generated by Claude Code

…icate union (#7692)

#3076 (objectui#2614) gave `userActions.edit`/`delete` a boolean-or-predicates
union so the built-in row affordances could be gated on record state.
`create`/`import` were left as bare booleans with no other lever, so a child
object's related-list [+ New] button could not be gated on the parent record's
state while the row Edit/Delete beside it could. On a frozen parent the row
actions grey out and [+ New] still renders; the server guard 409s the insert, so
it is an affordance leak an app has no way to close.

Both keys now take the SAME union — the same RowCrudActionOverrideSchema, not a
new dialect. resolveCrudAffordances carries the predicates through as
createPredicates/importPredicates alongside the existing edit/delete pair, via
the same normalizeRowCrudOverride collapse.

What `record.*` binds to differs between the two positions, and the schema says
so rather than implying a symmetry it does not have: edit/delete evaluate per
row against that row's own record; create/import gate a record that does not
exist yet, so they evaluate once per toolbar against the record in scope — the
host (parent) record on a related list, and nothing on a standalone object list,
where a `record.*` predicate therefore hides the button under the fail-closed
rule.

plugin-hono-server tracks the widened producer: the /me/permissions managed-write
clamp tested `create` with a bare `!== true`, which would have clamped away a
legitimate `create: { enabled: true, visibleWhen: … }` opt-in. It now reads
`create` through the same opt-in helper as edit/delete.

The renderer half (related-list toolbar honouring create.visibleWhen) is
objectui's downstream card and is not part of this change.

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

vercel Bot commented Aug 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 12, 2026 3:18am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-hono-server, @objectstack/spec.

107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/plugin-hono-server, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/plugin-hono-server)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/plugin-hono-server, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-hono-server, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/plugin-hono-server, @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-hono-server, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/plugin-hono-server, @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling labels Aug 11, 2026
…ractions-create-import-predicate

# Conflicts:
#	packages/spec/src/data/object.zod.ts
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 12, 2026
Merged via the queue into main with commit b9f930b Aug 12, 2026
27 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-7692-useractions-create-import-predicate branch August 12, 2026 03:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

userActions.create is boolean-only — related-list [+ New] cannot be gated on parent-record state, unlike edit/delete (#3076 left create behind)

2 participants