Skip to content

feat(spec,cli): warn the author when a discarded action alias costs them a handler (#3743) - #3838

Merged
os-zhuang merged 3 commits into
mainfrom
claude/action-target-alias-discard-ahpgl5
Jul 28, 2026
Merged

feat(spec,cli): warn the author when a discarded action alias costs them a handler (#3743)#3838
os-zhuang merged 3 commits into
mainfrom
claude/action-target-alias-discard-ahpgl5

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #3743.

What this fixes

#3742 made target beat the deprecated execute alias everywhere and had the ActionSchema transform drop the alias from its output, so "two different scripts for one button" became unrepresentable. What it left behind: an author who declares both slots with different values still loses one of the two handlers they wrote, silently. Per Prime Directive #12 that belongs at authoring time.

New advisory rule — action-target-execute-conflict. An action declaring both slots with different values gets a warning naming both handlers, stating that target wins, and giving the one-line fix (delete execute). Equal values are harmless duplication and stay quiet. It never fails the build: the resulting stack is well-defined — the cost is a handler that never runs, not a broken artifact.

Where the rule had to go — a correction to the issue's premise

#3743 proposed the CLI compile pipeline as the home, in a new pre-parse hook between lowerCallables and safeParse. That window is real, but for the dominant authoring path it is empty:

// packages/spec/src/stack.zod.ts — defineStack()
const normalized = normalizeStackInput(config);
const result = ObjectStackDefinitionSchema.safeParse(normalized, { error: objectStackErrorMap });

return mergeActionsIntoObjects(result.data);   // ← the PARSED stack is what the config module exports

defineStack parses inside the author's own config module, so execute is consumed at module-load time, long before os build calls loadConfig. Confirmed empirically — a defineStack config with target: 'preferredHandler', execute: 'legacyHandler' reaches the CLI as {"target":"preferredHandler"}, no execute key. A CLI-only lint would have been structurally blind on essentially every real app.

So the rule lives in @objectstack/spec (lintDeprecatedAliases, exported from the package root, next to normalizeStackInput) and is wired into both layers that actually perform the discard:

Layer Covers Why it's there
defineStack strict defineStack apps — the dominant path The earliest point the conflict exists; warns on the console before parsing, once per distinct conflict per process (same warn-once shape as warnGenericPasswordFields)
os build / os validate pre-parse pass plain object default-export, defineStack(…, { strict: false }), inline function handlers target is z.string(), so a function handler cannot pass strict defineStack at all — it only ever reaches the pipeline via a non-strict path and is lowered by the CLI

Each layer reports only its own discards — defineStack deliberately stays quiet in strict: false mode, where nothing has been discarded yet — so one authored conflict produces exactly one warning however the stack is compiled. Both CLI commands lint the same normalized input, so they agree by construction (#3782) and validate-build-gate-parity.test.ts covers the new rule with no change.

Behaviour fix in the same contract

#3742 fixed compile-time precedence by probing for a callable target first, which left one combination still resolving the alias's way:

target: 'preferredHandler',                       // canonical, a string
execute: function legacyHandler() {  },          // deprecated alias, a function

lowerCallables fell through to the alias, bundled it, and then overwrote action.target with the resulting ref — the canonical slot the author wrote lost to the alias, the same inversion #3713 fixed for the function/function pair, one slot type over. target now wins in every string/function combination, matching the ActionSchema transform, so the new warning can state one precedence rule and have it be true everywhere. The losing function alias is still deleted (it is not JSON-safe and ActionSchema expects a string), and the warning is what tells the author which handler they lost.

If you relied on an inline execute function winning over a string target, move it into target — the warning names the action.

Changes

  • new packages/spec/src/shared/deprecated-aliases.ts — the rule set for the pre-parse window, pure and side-effect free; exported from the spec root barrel
  • packages/spec/src/stack.zod.tswarnDeprecatedAliases + the call in defineStack's strict path
  • packages/cli/src/commands/compile.ts / validate.ts — the pre-parse pass, severity honoured so a future rule gates the build without rewiring
  • packages/cli/src/utils/lower-callables.ts — the string-target/function-execute precedence fix
  • packages/spec/src/ui/action.zod.ts — TSDoc now points at the warning
  • changeset

Testing

  • packages/spec/src/shared/deprecated-aliases.test.ts (new, 13 cases) — quiet paths (one slot only, equal strings, same function reference, empty string, no actions), object context, top-level/nested dedupe, inline functions, the mixed pair, map-shaped slots, multiple offenders
  • packages/spec/src/stack.test.tsdefineStack warns once naming both handlers, still drops the alias, stays quiet for one-slot/equal-value configs, nags once across repeated definitions, and stays silent in strict: false
  • packages/cli/src/utils/lower-callables.test.ts — a string target survives a function execute, which is dropped, and the lowered stack stays JSON-safe
  • @objectstack/spec 6764 passed · @objectstack/cli 749 passed · all example apps pass, including app-showcase's no-startup-warnings.test.ts — no app in the repo declares both slots, so the new warning adds no noise

End-to-end on both authoring paths (temporary configs, removed before commit):

# defineStack config — warns once from the authoring gate, CLI pass quiet
→ Loading configuration...
defineStack: action 'conflicting': Action declares both 'target' ('preferredHandler') and the
deprecated alias 'execute' ('legacyHandler'). 'target' wins: 'execute' is dropped while the stack
is compiled and never reaches the runtime or a renderer, so 'legacyHandler' never runs.
  Delete 'execute' — it is a deprecated alias of 'target', not a second handler. …
  rule: action-target-execute-conflict

# plain-object config — warns from the CLI pre-parse pass, both pairs
→ Checking deprecated aliases (#3743)...
  ⚠ action 'conflicting_strings': … 'execute' ('legacyHandler') … so 'legacyHandler' never runs.
  ⚠ action 'conflicting_mixed':   … 'execute' (an inline function) … so an inline function never runs.

Emitted artifact for the plain-object config — the canonical slot survives, nothing on the wire:

conflicting_strings | target = "preferredHandler" | execute = undefined
conflicting_mixed   | target = "preferredHandler" | execute = undefined   ← was clobbered before this PR
harmless            | target = "sameHandler"      | execute = undefined

Generated by Claude Code

…hem a handler (#3743)

#3742 made `target` beat the deprecated `execute` alias everywhere and had the
`ActionSchema` transform DROP the alias from its output, so "two different
scripts for one button" became unrepresentable. What it left behind: an author
who declares both slots with different values still loses one of the two
handlers they wrote, silently. Per Prime Directive #12 that belongs at
authoring time.

New advisory rule `action-target-execute-conflict`: an action declaring both
slots with different values gets a warning naming both handlers, stating that
`target` wins, and giving the one-line fix. Equal values are harmless
duplication and stay quiet. It never fails the build — the resulting stack is
well-defined; the cost is a handler that never runs.

The rule has to run PRE-PARSE, because the parse is what consumes the alias:
once `ObjectStackDefinitionSchema` has run there is no `execute` key left to
report. #3743 proposed the CLI compile pipeline as its home, between
`lowerCallables` and `safeParse`. That window is real but nearly always empty:
`defineStack` parses inside the author's own config module, so for a
`defineStack` app — i.e. almost every app — the alias is gone before `os build`
ever loads the module. A CLI-only lint would have been structurally blind on
the dominant authoring path.

So the rule lives in `@objectstack/spec` (`lintDeprecatedAliases`, exported from
the package root) and is wired into both layers that perform the discard:

  - `defineStack` warns on the console before parsing, once per distinct
    conflict per process — the earliest point the conflict exists.
  - `os build` / `os validate` run a new pre-parse pass over `normalized`,
    covering stacks that skip strict `defineStack`: a plain object
    default-export, `strict: false`, and inline function handlers (`target` is
    `z.string()`, so those cannot pass strict `defineStack` and are lowered by
    the CLI instead). Both commands lint the same input, so they agree by
    construction (#3782) and the parity gate covers the new rule.

Each layer reports only its own discards, so one authored conflict yields
exactly one warning however the stack is compiled.

Behaviour fix in the same contract: #3742 fixed compile-time precedence by
probing for a CALLABLE `target` first, which left one combination still
resolving the alias's way — a string `target` beside a function `execute` bound
the alias and then overwrote the canonical ref the author wrote. `target` now
wins in every string/function combination, matching the `ActionSchema`
transform, so the new warning states one precedence rule that is true
everywhere.

Verified end to end on both authoring paths: a `defineStack` config warns once
from the authoring gate and the CLI pass stays quiet; a plain-object config
warns from the CLI pass for both the string/string and string/function pairs,
and the emitted artifact keeps the canonical `target` with no `execute` on the
wire.

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

vercel Bot commented Jul 28, 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 Jul 28, 2026 9:22am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests protocol:ui tooling size/l labels Jul 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/spec.

110 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 packages/cli, @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli, @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 packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/cli, 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 packages/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/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • 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/cli, @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 packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • 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/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/cli, @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/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/cli, @objectstack/spec)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/runtime-capabilities.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/releases/implementation-status.mdx (via @objectstack/cli, @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/cli, @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.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/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)

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.

claude added 2 commits July 28, 2026 09:21
…aw NUL byte

`check:nul-bytes` (#3127) caught two literal U+0000 bytes in the
`warnDeprecatedAliases` key. A raw NUL makes ripgrep treat the whole file as
binary and silently return zero matches, so `stack.zod.ts` would have dropped
out of code search and every grep-based lint. Written as the unicode escape it
is byte-identical at runtime.

Same separator in `deprecated-aliases.ts`'s dedupe key, with a note on why it
must stay an escape, and regenerate the spec API-surface snapshot for the four
new exports (0 breaking, 4 added).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqC9ckaLbmhmytt65Gxi96
…3743)

The objectui actions reference already stated the precedence — "if an action
declares both, `target` wins and `execute` is discarded" — while saying nothing
about the author ever finding out. Name the rule, both surfaces that raise it,
that it never fails the build, and the one-line fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqC9ckaLbmhmytt65Gxi96
@os-zhuang
os-zhuang marked this pull request as ready for review July 28, 2026 09:25
@os-zhuang
os-zhuang merged commit 50616d9 into main Jul 28, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/action-target-alias-discard-ahpgl5 branch July 28, 2026 09:36
os-zhuang added a commit that referenced this pull request Jul 28, 2026
…#3743) (#3854)

#3838 introduced `lintDeprecatedAliases` — the pre-parse pass that reports an
alias the parse itself is about to consume — with one rule, for
`action.execute`. #3743 predicted the pass would earn its keep beyond that rule.
It does: `execute` was never special. The spec has exactly THREE transforms that
fold an alias into its canonical key and then drop it from the parsed output,
and all three share one failure mode — declare both slots with different values
and one is discarded with no signal, invisible to every downstream check because
the parse already erased the evidence.

Two more rules, same shape, same advisory severity, same two surfaces:

  field-requiredwhen-conditionalrequired-conflict — `FieldSchema` folds
  `conditionalRequired` into `requiredWhen` (#3754/#3764); the discarded
  predicate never gates the field. Covers fields on objects AND on object
  extensions, and compares the predicate TEXT so a bare string and the
  `{ dialect, source }` envelope it lowers into read as the same predicate.

  agent-knowledge-sources-topics-conflict — `AIKnowledgeSchema` folds
  `knowledge.topics` into `knowledge.sources` (#1878/#1891); the discarded list
  names RAG sources the agent never recruits from. Compares by SET, so the same
  sources in a different order stay quiet.

The three findings now share one builder, so they phrase the common half
identically (which slot wins, that the alias is dropped, delete it) while each
still names what its own discarded value would have done. No CLI change: both
call sites already loop over whatever the pass returns.

Also corrects `content/docs/ai/agents.mdx`, which documented `knowledge` as
`{ topics, indexes }` and used `topics` in all three examples — teaching the
deprecated alias as the canonical key, and contradicting
`skills/objectstack-ai/SKILL.md`, which already had it right.

Follow-ups filed: #3855 (removal schedule for all three aliases), #3856
(objectui's now-dead `execute || target` fallback).
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:ui size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P3] action target + execute both declared: the alias is discarded silently — no author-facing warning

2 participants