Uh oh!
There was an error while loading. Please reload this page.
feat(spec,runtime): refuse the doubled post-success navigation channel on script actions - #11841
Conversation
#11519) A type:'script' action could carry two post-success destinations — the declared ActionSchema.onSuccess block and the handler-returned { redirectUrl } — with the spec ruling neither, leaving renderer-side precedence to decide silently (interim: declared wins, objectui#5933). Maintainer ruling 2026-08-24: refuse the doubled channel; no precedence field. Measured static knowability partitions the fix: - Statically knowable half: opensInNewTab: true is the schema-visible marker of the handler-redirect channel, so onSuccess beside it on a script action is refused at authoring time by a new refine, with guidance naming both channels and the remedy. - Runtime-only remainder: a handler that returns redirectUrl with no marker is diagnosed loudly at the dispatch seam (doubledPostSuccessNavigationWarning), wired at both surfaces that hold the declaration and the handler result — the REST /actions route and the MCP run_action bridge. Observe-only: the wire is untouched and the interim renderer precedence stays the decider until the author takes the remedy. Single-channel cases (only onSuccess, only opensInNewTab, opensInNewTab + newTabUrl) stay accepted byte-identically, pinned; the corpus was measured at zero doubled producers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rxnd8cyFnoU8V5y21PaTsy
) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rxnd8cyFnoU8V5y21PaTsy
The runtime TEST_DEBT ledger entry is frozen at 227 raw tsc errors with the test exclusion removed; the new test file initially owed 8 (TS18048 x2, TS7006 x6). Annotated the spy-call lambdas and optional-chained the dispatcher response so the scoped re-measure reads exactly 227 with zero attributed to this file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rxnd8cyFnoU8V5y21PaTsy
📓 Docs Drift CheckThis PR changes 2 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 132 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 05cc1989b2899d88531ebb3f13201b7aa892b995 && git checkout 05cc1989b2899d88531ebb3f13201b7aa892b995
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0392a120d5114d1e0389cec8de6c3932c7307df3 45c0d4473cf216429b7a9b6b39311bc3b1fc87f2 && git checkout -B drift-repro 0392a120d5114d1e0389cec8de6c3932c7307df3 && git merge --no-ff 45c0d4473cf216429b7a9b6b39311bc3b1fc87f2
node scripts/docs-audit/affected-docs.mjs --json 0392a120d5114d1e0389cec8de6c3932c7307df3
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11519
Ruling carried out
Maintainer ruling 2026-08-24 (issue comment 5393507405), batch acceptance verbatim: 「四维分析一致的,接手你的建议。」 ⇒ refuse the doubled channel; ⛔ no new
precedencecontract field. Branch chosen by measurement of static knowability, recorded below. The interim renderer behaviour (declaredonSuccesswins, objectstack-ai/objectui#5933) stands until this lands; no renderer code is touched.Clause-②: yes — the PR stays draft, the contract-review chain runs before enqueue.
The measurement — is "the handler can return
redirectUrl" statically knowable at parse time?In general: NO. A
type: 'script'action's handler is one of:target— a string naming a handler registered at runtime viaengine.registerAction(...); the schema sees only the name, and dispatch resolves it from a registry (packages/runtime/src/domains/actions.ts:419-423,executeRegisteredActioninpackages/runtime/src/action-execution.ts). The return value is runtime-only.body—HookBodySchema(packages/spec/src/data/hook-body.zod.ts) declareslanguage,source, and the L2 knobscapabilities/timeoutMs/memoryMb. No declared return contract exists. Grepping thesourcetext for the word would be a heuristic (false positives on mentions, false negatives on computed/spread keys), not a declared shape.But a declared marker DOES exist on the action itself:
opensInNewTab(packages/spec/src/ui/action.zod.ts, the flag documented as "pre-opens the tab synchronously on click … then drives the tab to the handler's returnedredirectUrl"). An author who declaresopensInNewTab: truehas declared, in schema-visible metadata, that the handler-redirect channel is in use.So the measured property partitions, and each region lands in its pre-approved branch:
Branch 1 (authoring-time superRefine) for the statically-knowable subset: a
type: 'script'action declaringonSuccessbesideopensInNewTab: trueis refused at parse time, with guidance naming both channels, the interim winner, and the remedy. (packages/spec/src/ui/action.zod.ts, new refine on theActionSchemachain.)Branch 2 (loud runtime diagnostic) for the runtime-only remainder — a handler that actually returns
{ redirectUrl }with no marker declared (the shipped convention honours it regardless: objectuipackages/app-shell/src/utils/consoleServerAction.ts, theredirectUrlbranch below the spec: SettingsManifest 的 SpecifierSchema 新增valueDomain闭合枚举 —— 声明存在时标准域为执行边界,options 退化为 UI 便利列表(#5712 裁决的 spec 半边) #5933-marked precedence block). The seam lives in this repo, not only in objectui — no cross-repo stop needed: the script dispatch holds both the resolved declaration (withonSuccess) and the handler's return value, at two surfaces sharing one helper:/actionsroute:packages/runtime/src/domains/actions.ts(script dispatch, before the [17.0.0-rc] /actions reports business failures as HTTP 200 inside a double envelope — the only route of 12 that does; unify on real status codes before GA #3962 single-wrap success return);run_actionbridge:invokeBusinessActioninpackages/runtime/src/action-execution.ts.The new
doubledPostSuccessNavigationWarninghelper returns the[action-contract]warning text (string-or-null, theactionPermissionErrorconvention); both seamsconsole.warnit. Observe-only: the wire is untouched, and the interim declared-wins precedence stays the decider — the warning says so and names the remedy.warn, noterror, per the degradation-log-level rule (nothing claimed-persisted is lost).Declared surface extension (per the dispatch instruction): the diagnostic's landing site is
packages/runtime/src/action-execution.ts+ the two dispatch call sites above — measured, not assumed.Pins
packages/spec/src/ui/action-doubled-redirect.test.ts(direct + throughgetMetadataTypeSchema('action')).onSuccess(parse output deep-equal, materialized defaults included), onlyopensInNewTab,opensInNewTab+newTabUrl, andopensInNewTab: falsebesideonSuccessall stay accepted.type: 'api'stays accepted — the ruled sentence is about script actions; recorded as a deliberate boundary.packages/runtime/src/http-dispatcher.actions-doubled-redirect.test.ts(dispatcher seam + 10 predicate pins).Reverse verification (both legs recorded)
Test Files 1 failed | 420 passed (421) · Tests 3 failed | 11223 passed (11226).4 files / 204 tests passedacrossaction-doubled-redirect,action-on-success,action,inline-action; then full spec suite421 files / 11226 tests passed.expected [] to have a length of 1 but got +0), 13 predicate/silent pins green.14/14, and the neighbouring dispatch suites (actions-type-dispatch,actions-global-key,actions-identity-addressing,actions-validation-envelope)64/64.Verification (readings quoted from head
45c0d4473; the final commit touched only the runtime test file's type annotations)@objectstack/spectests: 421 files / 11226 passed (verdict line:os-verify-lock: VERDICT command-exit 0).@objectstack/spectypecheck +check:test-typecheck: OK — ledger byte-identical (55 files / 263 errors held).@objectstack/runtimetests: 187 files / 2765 passed; typecheck exit 0 (re-run at head).@objectstack/linttests 81 files / 2291 passed;@objectstack/example-showcasetypecheck clean + 26 files / 362 tests passed — the narrowing reds nothing, matching the measured zero doubled producers.pnpm --filter @objectstack/spec check:generated: 13/14 current; the one stale (test-typecheck-debt.json) regenerated byte-identical and its own gate reads OK.dispatch-gates: gate list derived from the tree of 'objectstack-ai/objectstack' at commit 45c0d4473 (/home/user/objectstack-11519). All 24 path-matched families run locally green (changeset-gate-self-tests, cross-package-test-inputs, doc-formula-expressions, empty-state, liveness, merge-driver, objectui-changeset, published-files, route-envelope, slot-lookup, spec-parsed-alias, strictness-ledger, test-source-alias, type-source-resolution, variant-docs, adr-0087-registration, changeset-no-major, dev-prereqs, empty-changeset, plugin-teardown-shape, affected-docs, release-rehearsal self-test) plus the convention-triggered set (query-options-erasure, type-check-coverage, engine-double-contract, where-matcher — re-run at head45c0d4473after the test-file edit, all exit 0).check:type-check-debt --re-measure(re-runs tsc over all 13 DEBT + 19 TEST_DEBT entries) exceeded the container's 10-minute foreground cap twice (SIGTERM at ~10m; its coverage half completed OK both times). Scoped measurement instead: runtime — the only ledgered package this diff adds test code to — probed with its test exclusion removed (tsc --noEmitoverextends-config): 227 raw errors, exactly the frozen ledger number, 0 attributed to the new file (the file initially owed 8 and was annotated clean in45c0d4473). Spec's per-file ledger re-recorded byte-identical. CI runs the full re-measure regardless.@objectstack/specminor (launch-window breaking-as-minor; accept-set narrows) +@objectstack/runtimepatch, with the ADR-0087 dispositionnot-required (no-migration-prescription)— zero doubled producers measured in every corpus, and the channel choice is an authoring decision no migration entry can perform.Out of scope, deliberately
precedencefield, no new authorable keys (ruled).type: 'api'pair and thenewTabUrl-requires-opensInNewTabco-constraint are outside the ruled sentence and untouched.Generated by Claude Code
Generated by Claude Code