Skip to content

feat(types)!: retire the ActionCondition branch shape from ActionSchema.condition - #5981

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3917-retire-action-condition-branch
Aug 24, 2026
Merged

feat(types)!: retire the ActionCondition branch shape from ActionSchema.condition#5981
yinlianghui merged 1 commit into
mainfrom
claude/issue-3917-retire-action-condition-branch

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#3917

Maintainer ruling 2026-08-09, route B — retire the { expression, then, else } shape per enforce-or-remove (directive 「全部接受」 covering that round's whole decision inbox). Option A (honouring the branch DSL in ActionRunner.execute) is explicitly not taken.

What was wrong

@object-ui/types declared ActionSchema.condition as an { expression, then, else } branch DSL and shipped a zod mirror (ActionConditionSchema) that accepted it. Nothing ever read expression / then / else. The only consumer of the key is ActionRunner.execute, which reads it as a predicate gate; a branch object carries no source, so the runner's normalizer read it as "no gate declared" and executed the action unconditionally — predicate never evaluated, then / else never dispatched, os validate / os build green, zero diagnostics. Two docs pages taught the shape with worked examples, so an author following the documentation ("amounts over 1000 go to manager approval") got unconditional execution.

What this PR does

  • Removes ActionCondition (crud.ts) and ActionConditionSchema + ActionConditionSchemaType (zod/crud.zod.ts), and both barrel exports (index.ts, zod/index.zod.ts).
  • Retypes ActionSchema.condition to the predicate the runtime actually honours — boolean | string | { dialect?: string; source: string } — the same three arms ActionRunner's own ActionDef.condition carries and the same vocabulary visible / disabled use. The key survives as a gate; only the branch shape is retired, which is what makes the rewritten docs coherent with the type.
  • Rewrites both teaching sites to the live vocabulary: content/docs/core/enhanced-actions.mdx (Conditional Execution — gate spellings, how the gate is read, branching as separate actions, a retirement note) and content/docs/api/schema-reference.md (ActionSchema property table + the JSON example).
  • minor, per this repo's version-alignment rule reserving major for following @objectstack across a major (AGENTS.md 版本号策略; check-changeset-no-major.mjs enforces it). Same classification as the MobileOverrides retirement (finding(types): MobileOverrides.navigation 发布了三值导航词表,全仓零读点 —— 与 #3985 同族的另一处「声明即全部」 #4919).

One documentation claim is stated more precisely than the ruling's one-liner, because the code says so: a sequentialchain stops at the first step whose condition does not hold (a blocked step returns Action condition not met, and executeChain sequential returns on the first failure — packages/core/src/actions/ActionRunner.ts). So the page teaches per-action conditions on sibling actions for an either/or, and a step condition for gating an optional follow-up. Teaching "put both branches in one sequential chain" would have re-created this card's own failure mode.

Evidence

All gate runs below are on 24d653d5e (this branch's head, working tree clean).

The zod parse verdict flips in both directions — measured, not asserted. A throwaway worktree at origin/main (2aff580b5) parsed the same fixtures through the old schema; the numbers came out of vitest's own failure output, not from prose:

condition valueorigin/main @ 2aff580b5this branch
{ expression, then, else }accepted (success: true)refused (invalid_union on condition)
falserefusedaccepted
'data.amount > 1000'refusedaccepted
'${data.amount > 1000}'refusedaccepted
{ dialect: 'cel', source: … }refusedaccepted

The old schema required expression, so the spelling the runtime has always honoured was the one the schema rejected. Both directions are now pinned in phase2-schemas.test.ts (an it.each over the four live spellings, plus a refusal test asserting the full parse is red and that the issue is on the condition path).

  • pnpm exec vitest run packages/types/Test Files 53 passed (53), Tests 581 passed (581).
  • pnpm --filter @object-ui/types type-check (tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json) → exit 0.
  • Downstream contract sweep: pnpm exec turbo run build $(node scripts/check-doc-snippet-types.mjs --build-filter) --concurrency=2Tasks: 32 successful, 32 total. Every one of those packages is a tsc build of its own src/ against the new @object-ui/typesdist/*.d.ts, so the narrowing breaks no consumer.
  • node scripts/check-doc-snippet-types.mjs → controls live (sentinel … TS2305, resolution landed on packages/types/dist/index.d.ts), Semantic phase: 135 of 135 block(s) judged, 0 failed.
  • node scripts/check-doc-component-types.mjs✅ Every documented component type is registered.
  • node scripts/check-doc-links.mjsLinks are valid across 13 scan roots.
  • node scripts/check-control-bytes.mjs✅ OK (scanned 4951 tracked text file(s)).
  • node scripts/check-changeset-presence.mjs✅ 6 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s); node scripts/check-changeset-no-major.mjs✅ No changeset declares a major bump.
  • pnpm --filter @object-ui/types lint → exit 0 (0 errors; 251 pre-existing no-explicit-any warnings, none of them new — the diff removes one unused import and adds no any).

Declared narrowing: the repo-wide pnpm lint / pnpm test farm was not run locally — CI runs it exactly once regardless. Type-aware linting is not enabled in eslint.config.js (no parserOptions.project / projectService), so this diff cannot move any untouched file's lint verdict; every .ts file it touches was linted directly (6 files, 0 errors).

The "zero consumers" premise, re-verified

Independently re-measured on this tree rather than taken from the card: the nine ActionCondition read points enumerated in the dispatch are exact (the card's crud.ts:212 / crud.zod.ts:86 had drifted to :205 / :84, and the card omitted both test read points — the zod-mirror-parity registry entry is keyed by the schema name and would have failed CI as a stale entry). A repo-wide grep for a consumer of condition.expression|then|else outside packages/types returns zero, and no JSON fixture, example app or e2e schema authors the branch shape. Premise holds; no fork.

Out of scope, filed not fixed

A third teaching site writes the retired spelling — content/docs/guide/schema-overview.md:84 — which neither the card, the ruling nor the fence names. It sits inside a declared doc-snippet fragment, so no gate compiles it. Filed unassigned as #5980 rather than widening this PR's fenced file surface.

Kept in draft per the dispatch contract: the PM seat reviews CI and decides readiness.


Generated by Claude Code

…ma.condition
`condition` was declared as an `{ expression, then, else }` branch DSL with a
zod mirror that accepted it, and two docs pages taught it with worked examples.
Nothing ever read `expression` / `then` / `else`: the only consumer of the key
is `ActionRunner.execute`, which reads it as a PREDICATE gate. A branch object
carries no `source`, so the runner's normalizer read it as "no gate declared"
and executed the action unconditionally — predicate never evaluated, branches
never dispatched, zero diagnostics, `os validate` green.
Retired under enforce-or-remove (maintainer ruling 2026-08-09, route B) so the
key has one reading:
- remove `ActionCondition` and `ActionConditionSchema` (and both barrel exports)
- retype `ActionSchema.condition` to the predicate the runtime honours:
`boolean | string | { dialect?, source }` — the arms `ActionDef.condition`
carries, and the vocabulary `visible` / `disabled` already use
- rewrite both teaching sites to the live vocabulary: `condition` is a gate; a
branch is separate actions with mutually exclusive `condition`s
The zod parse verdict flips both ways, measured against origin/main: the branch
object goes from accepted to refused, and every live predicate spelling goes
from refused to accepted (the old schema required `expression`).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3232.4 KB3990.2 KB
Main entry chunk (gzip)153.6 KB350 KB
Entry fileindex-BbiH2u5J.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)10.13KB3.77KB
app-shell (runtime-config.js)13.57KB4.78KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)504.75KB114.32KB
core (index.js)4.92KB1.97KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)165.30KB45.79KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.35KB34.44KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)244.00KB61.86KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.63KB30.64KB
plugin-gantt (index.js)164.15KB39.88KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.86KB27.22KB
plugin-map (index.js)20.06KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.57KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)52.40KB17.45KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.88KB1.85KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghui
yinlianghui marked this pull request as ready for review August 24, 2026 10:36
@yinlianghui
yinlianghui added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit 6a91586Aug 24, 2026
23 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3917-retire-action-condition-branch branch August 24, 2026 10:37
This was referenced Aug 24, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

文档教作者写的 ActionSchema.condition = { expression, then, else } 全仓零消费者:谓词被忽略、then/else 永不派发、零诊断

2 participants

@yinlianghui@claude