Skip to content

feat(spec): ActionSchema.onSuccess — post-success navigation with ${result.*} scope (#9566, #9474) - #9601

Merged
os-steve merged 2 commits into
mainfrom
claude/issue-9566-onsuccess-navigation
Aug 18, 2026
Merged

feat(spec): ActionSchema.onSuccess — post-success navigation with ${result.*} scope (#9566, #9474)#9601
os-steve merged 2 commits into
mainfrom
claude/issue-9566-onsuccess-navigation

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes #9566
Fixes #9474

One spec PR delivering the contract half of BOTH cards, per the maintainer's 2026-08-18 ruling (recorded on #9566, mirrored on #9474). The ruling, as recorded there:

  1. ActionSchema gains one CLOSED onSuccess key, applying to both api and script action types: navigate (a route/URL template) + openIn: 'self' | 'newTab' (closed enum — no general navigation DSL, no per-surface variants).
  2. The interpolation scope for the navigate template gains ${result.*} — the server response joins ${param.*} / ${ctx.*} (feat(actions): api 型动作需要「成功后跳转到服务端返回的记录」能力(ActionSchema 跳转键 + 响应进入插值作用域) #9474's companion ask).
  3. Defaults, deliberately split by surface: schema-declared onSuccess.navigate defaults openIn: 'self' (new surface, natural SPA-route UX, immune to popup blocking); the shipped handler-return convention { redirectUrl } WITHOUT openIn keeps its current new-tab behavior (17.0.0 compatibility — no silent behavior flip for existing handlers); a handler may return openIn: 'self' explicitly to opt into the same-tab jump.
  4. Contract-first split: this spec half lands first and does not wait on objectui; the console halves (SPA route navigation in consoleServerAction.ts, executeAPI navigation handling, interpolation implementation) are downstream objectui card(s) filed at this card's landing, Blocked-by: these cards.

The maintainer's approval, verbatim and untranslated: 「9566 插队」 then 「9566 同意」.

Key placement

ActionSchema does not discriminate into per-type schemas — it is one strict object (actionObject()) with a type enum key, and per-type conditions are enforced on the refine chain (the target-required and body-on-non-script precedents, #4352). So the ruled placement is:

Default mechanism — materialized .default('self'), justified

The ruling allows either mechanism; measured against the file's own conventions the materialized default wins:

  • it is the file's convention (type: .default('script'), refreshAfter: .default(false), ai.exposed: .default(false), param required: .default(false)), with the ADR-0122 ActionParsed type carrying defaults applied;
  • declared navigation is an explicit author choice (unlike the feat(spec): declare viewMode on GanttConfigSchema (#9463) #9489 seeding case), so materializing cannot mislabel an absent intent;
  • the parse output always carries the resolved member, so the future console branch reads onSuccess.openIn with no fallback of its own — declared = enforced, no consumer-side ?? (Prime Directive Add comprehensive test suite for Zod schema validation #12).

The default-behavior pin asserts the observable: parse output of { navigate } alone carries openIn: 'self'.

Interpolation scope contract

Measured: the scope is documented in spec today only on target's JSDoc, partially (ctx.origin, ctx.recordId, ctx.user.id, ctx.org.id, "etc."); the full member list exists only in objectui (interpolateTarget). Per the ruling, the new key's JSDoc records the full scope as CONTRACT — ${param.*} (params-dialog values), ${ctx.*} (origin / apiBase / user / org / recordId / selection), and ${result.*} (NEW — the action's server response payload: an api action's response body, a script handler's return value), plus the encodeURIComponent rule carried over from target. The interpolation ENGINE stays objectui's; no interpolation is implemented in spec.

The handler-return convention's ruled compat semantics (absent openIn on { redirectUrl } keeps new-tab, 17.0.0 compatibility; explicit openIn: 'self' opts in) are recorded in the key's JSDoc so the contract holds both surfaces. No handler/runtime code changed.

One spelling observation, flagged not deviated: the ruled enum member is 'newTab' (matching the handler-return convention), while the pre-existing top-level openIn key (for type:'url', spec 17.0.0) spells its member 'new-tab'. Implemented exactly as ruled; the enum carries an issue.input-keyed error (the ActionLocationSchema precedent) so an author writing 'new-tab' here is told the member is 'newTab' and why, and both spellings stay loud rather than silently divergent. Harmonizing the shipped 'new-tab' would be a breaking value migration outside this ruling's scope.

Liveness

packages/spec/liveness/action.json records the key at planned strength, drilled per child (navigate, openIn) after the gate's undeclared-container check asked for it — mirroring the #9340 map / #9463 viewMode precedents: the cross-repo measurement (consoleServerAction drives only the { redirectUrl } new-tab convention; executeAPI returns {success,data} and never navigates; interpolateTarget has no result member) and the amend-to-live-on-landing instruction citing the exact reads to look for.

Authoring probes measured in #9474 (redirect, navigate, redirectUrl) now land on guidance prescriptions naming the new shape instead of bare unknown-key rejections.

Verification (all at b53b4fb, the PR head)

  • pnpm --filter @objectstack/spec test410 files / 10957 tests passed (includes the new action-on-success.test.ts: accept pins full/minimal on both types + the registered-metadata-schema door, materialized-default pin, closed-enum refusals incl. the kebab-crossover message, strict-inner-object refusals with alias/guidance messages, four type-scope refusals, inline-not-picked pin).
  • pnpm --filter @objectstack/spec typecheck — green (incl. test-typecheck ledger).
  • pnpm --filter @objectstack/spec check:generated — all 13 artifact gates green after --fix regenerated exactly the 3 proved stale (gen:docs, gen:strictness-ledger, gen:liveness-counts); check:liveness, check:strictness-ledger, check:variant-docs, check:empty-state re-run green individually. The ui/action.zod.ts site-count pin in strictness-ledger.test.ts moved 8 → 9 (the new strictObject site), following that test's own precedent (7 → 8 at drillDown 是 objectui 读的活能力,却不是 spec 声明的任何键 —— #3752 的迁移处方曾指向它 #5022).
  • node scripts/pm/dispatch-gates.mjs <changed paths> — every matched family run green: adr-0087-registration, changeset-no-major, empty-changeset, cross-package-test-inputs (both spellings), dev-prereqs, affected-docs, changeset-gate-self-tests, doc-authoring, docs-audit-scope, docs-redirects, merge-driver, objectui-changeset, quick-reference-counts, role-word, spec-parsed-alias, type-source-resolution, doc-formula-expressions; convention-triggered: query-options-erasure, engine-double-contract, where-matcher, type-check-coverage, type-check-debt --re-measure (33 entries, none above measurement), nul-bytes.
  • Consumer sweep, direction stated: downstream. turbo run build --filter="./packages/*" --filter="./packages/*/*" — 70/70 packages (the whole workspace closure, every consumer of spec's rebuilt .d.ts) build green; @objectstack/example-showcase validate + typecheck green.
  • Changeset: @objectstack/spec minor (accept-set expansion); ADR-0087 marker not-required — nothing renamed, retired or tombstoned, so no conversion to register (check-adr-0087-registration green).

Reverse verification (from committed state)

Predicted before running, then observed exactly: with action.zod.ts reverted to origin/main (pins kept), 17 tests went red — every accept pin, both default pins, every message-bearing refusal pin (the generic unknown-key rejection replaces the specific messages), all four type-scope refusals, both top-level guidance pins, and the site-count pin (9 → 8). The four predicted vacuous survivors stayed green as expected (out-of-vocab openIn, empty onSuccess, inline refusal, no-onSuccess parse — each asserts only success === false, which the unknown-key rejection also satisfies); reported as such rather than counted as coverage. Restored via git checkout HEAD -- <path>; all 36 green again, clean tree.


Generated by Claude Code

claude added 2 commits August 18, 2026 12:06
…esult.*} scope (#9566, #9474)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fs18A2DdXLVN2h8PaaFBcP
…ss container; move the action.zod site-count pin 8→9

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fs18A2DdXLVN2h8PaaFBcP
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 5 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/troubleshooting.mdx (via ActionSchema (symbol))
  • content/docs/protocol/objectui/actions.mdx (via ActionSchema (symbol), openIn (literal))
  • content/docs/ui/actions.mdx (via openIn (literal))
What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json, packages/spec/liveness/action.json, packages/spec/liveness/state-counts.md) — pages documenting those are invisible to this run

Coarse fallback — 113 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json ca2e020e47bbe1121aa7e12aaeeae0f6d402ce5fpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 50e8a14f5912c1501662891bd54f1764f16cd163 — the merge of head b53b4fb706bdf52571eba2e197dfd10909207614 into base ca2e020e47bbe1121aa7e12aaeeae0f6d402ce5f, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 50e8a14f5912c1501662891bd54f1764f16cd163 && git checkout 50e8a14f5912c1501662891bd54f1764f16cd163
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ca2e020e47bbe1121aa7e12aaeeae0f6d402ce5f b53b4fb706bdf52571eba2e197dfd10909207614 && git checkout -B drift-repro ca2e020e47bbe1121aa7e12aaeeae0f6d402ce5f && git merge --no-ff b53b4fb706bdf52571eba2e197dfd10909207614

node scripts/docs-audit/affected-docs.mjs --json ca2e020e47bbe1121aa7e12aaeeae0f6d402ce5f

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs ca2e020e47bbe1121aa7e12aaeeae0f6d402ce5f → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests protocol:ui tooling labels Aug 18, 2026
@os-steve
os-steve marked this pull request as ready for review August 18, 2026 13:50
@os-steve
os-steve added this pull request to the merge queue Aug 18, 2026
Merged via the queue into main with commit 07e630e Aug 18, 2026
26 checks passed
@os-steve
os-steve deleted the claude/issue-9566-onsuccess-navigation branch August 18, 2026 14:15
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 size/m tests tooling

Projects

None yet

2 participants