|
| 1 | +--- |
| 2 | +"@objectstack/spec": minor |
| 3 | +"@objectstack/runtime": patch |
| 4 | +--- |
| 5 | + |
| 6 | +feat(spec,runtime): refuse the doubled post-success navigation channel on a `type: 'script'` action (#11519) |
| 7 | + |
| 8 | +**BREAKING** accept-set narrowing on `ActionSchema`, shipped as `minor` under |
| 9 | +the repo's launch-window convention for breaking changes. |
| 10 | + |
| 11 | +Two independent channels could name a post-success destination for one |
| 12 | +`type: 'script'` action: the declared `onSuccess` block (`{ navigate, openIn }`, |
| 13 | +validated and visible in metadata) and the handler-returned `{ redirectUrl }` |
| 14 | +convention (runtime-only). The spec ruled each surface's default in isolation |
| 15 | +and said nothing about an action carrying both — so the renderer had to pick, |
| 16 | +and the pick lived only in one renderer's implementation (declared `onSuccess` |
| 17 | +wins, objectstack-ai/objectui#5933). Maintainer ruling 2026-08-24: refuse the |
| 18 | +doubled channel; ⛔ no `precedence` contract field. |
| 19 | + |
| 20 | +The measured static-knowability partition: |
| 21 | + |
| 22 | +- **Authoring-time refine (spec):** "the handler can return `redirectUrl`" is |
| 23 | + runtime-only in general (`target` names an opaque registry entry; |
| 24 | + `HookBodySchema` declares no return contract) — but `opensInNewTab: true` is |
| 25 | + a schema-visible declaration of the handler-redirect channel (its contract is |
| 26 | + "pre-open a tab, then drive it to the handler's returned `redirectUrl`"). |
| 27 | + A `type: 'script'` action declaring `onSuccess` beside `opensInNewTab: true` |
| 28 | + is now **rejected at parse time**, with guidance naming both channels and the |
| 29 | + remedy. Previously the pair parsed clean and one declaration was silently |
| 30 | + dead at render. |
| 31 | +- **Dispatch-seam diagnostic (runtime):** the runtime-only remainder — a |
| 32 | + handler that actually returns `{ redirectUrl }` while the action declares |
| 33 | + `onSuccess` — now logs a loud `[action-contract]` warning at both dispatch |
| 34 | + surfaces (the REST `/actions` route and the MCP `run_action` bridge), naming |
| 35 | + the action, both channels, the interim winner and the remedy. Observe-only: |
| 36 | + the wire is untouched and the interim renderer precedence stands until the |
| 37 | + author takes the remedy. |
| 38 | + |
| 39 | +Single-channel declarations are untouched and pinned byte-identically: only |
| 40 | +`onSuccess`, only `opensInNewTab` (with or without `newTabUrl`), and |
| 41 | +`opensInNewTab: false` beside `onSuccess` all parse exactly as before. The |
| 42 | +corpus was measured at zero doubled producers (this repo's examples and |
| 43 | +platform metadata, objectui metadata, and the cloud SSO handoff producers per |
| 44 | +the #11519 measurement), so no shipped metadata is affected. |
| 45 | + |
| 46 | +**Migration.** An action refused by the new refine must pick its one |
| 47 | +destination: keep `onSuccess` and drop `opensInNewTab` (and stop returning |
| 48 | +`redirectUrl` from the handler), or keep `opensInNewTab` + the handler |
| 49 | +redirect and drop `onSuccess`. Which channel is right is an authoring decision |
| 50 | +the metadata cannot make for you, and zero such actions exist in any measured |
| 51 | +corpus. |
| 52 | + |
| 53 | +<!-- adr-0087: not-required (no-migration-prescription) A validity narrowing over a pair of existing keys: no key is removed, renamed or re-shaped, so there is no tombstone and nothing mechanical for `objectstack migrate meta` to rewrite. The refusal is the channel that reaches an affected author, at the parse site, carrying the remedy; choosing which of the two declared destinations to keep is an authoring decision no migration entry can perform on an upgrader's behalf — and the measured population of affected sources is zero in every corpus. --> |
0 commit comments