Repository navigation
Commit 1623faf
feat(spec)!: a page action row's onSuccess is refused beside a runner built-in other than api / script, and beside no executor (#23331)
Fixes #23168
Clause-②: no (narrowing)
The `onSuccess` half of #23168, under triage's fork answer **C**
(comment `6148462268`). The ruled half (the name leg of the url-target
judgment) landed as `b529a5c23` (PR #23289, `Part of #23168`); this PR
closes the card.
## What changes
**A page action row's `onSuccess` is judged beside the executor the
console's action runner runs.** The rows are `action:button` and
`action:icon` (`properties`) and each `action:group` / `action:menu`
member (`properties.actions[]`). The rule reads the rows' effective
executor through `effectiveRowExecutor` (landed in `b529a5c23`): the
executor key, or the row's `name` when that key is omitted or empty,
with the runner's own `||`.
- Refused beside the runner built-ins `url`, `navigation`, `modal`,
`flow` and `form`.
- Refused on a row with no effective executor (no executor key, or an
empty one, and no `name`).
- Accepted beside `api` and `script`.
- ⛔ A name outside the runner's built-in table is a host-registered
handler and stays open (`actionType: 'run'`, `name: 'clone_task'`, …).
The runner hops after every host handler, and only the host knows
whether its handler navigates. The accepted trade-off stands: a host
handler that itself navigates, plus `onSuccess`, still navigates twice.
**`ActionSchema`'s `onSuccess` refusal now gives a true reason.** Its
reach is unchanged: `type` is the closed `ActionType` enum with a
`script` default, so `url`, `modal`, `flow` and `form` are refused, as
before. The old text said the key "would parse clean and never run" and
that "no renderer reads a post-success hop today". Both were false,
because the runner performs the hop after every executor that succeeds.
The action and the rows now print **one refusal text** from one module,
`packages/spec/src/ui/action-on-success-executor.ts` (relative import
only, outside the `ui` barrel, for the reason `action-target-aliases.ts`
gives). The subject names the action's `type`, or the row and the
spelling that resolved its executor. The reason is stated per executor:
| executor | the reason the text gives | prescription |
| --- | --- | --- |
| `url`, `navigation` | the action is itself the navigation, so the hop
navigates a second time | the destination in `target`, the tab in
`openIn`; drop `onSuccess` |
| `form` | the action routes to the form view's `/forms/` page, so the
hop leaves the form before it is filled in | drop `onSuccess`; declare
where a submitted form goes in the form view's `submitBehavior` |
| `modal` | the dialog's success is whatever its host answers, a result
the contract does not declare | drop `onSuccess`, or move the hop onto
an `api` / `script` action |
| `flow` | the console answers success when the run starts, even for a
run paused at its first screen or ended refused, so the hop fires while
that screen is open | the same |
| no executor (rows only) | the row names none | declare `actionType:
'api'` / `'script'` (`type` on a member), or drop `onSuccess` |
Every refusal's second sentence reads: "`onSuccess` is declared beside
`api` and `script` only, whose success carries the result `${result.*}`
reads: the call's response, the handler's return."
**The describes' wording nit** from the landing record: the `name` and
`target` describes said "no `actionType`" (or "no `type`"), while the
refinement also covers an empty key. They now say "no or an empty". The
three `onSuccess` describes state the executor scope.
`content/docs/references/ui/component.mdx` is regenerated by `gen:docs`.
**Where it lives.** It is a sibling refinement,
`refuseRowOnSuccessBesideExecutor`, chained after the url-target and
alias refinements on all four rows. Its predicate `rowOnSuccessRefused`
sits beside `effectiveRowExecutor`. The built-in table is
`ActionType.options` plus `ROW_URL_NAVIGATOR_EXECUTORS`, which carries
the `navigation` alias, so the vocabulary is read from the contract and
never re-spelled. The issue lands at `onSuccess`. `os validate`'s props
gate reports it at `properties.onSuccess` /
`properties.actions.N.onSuccess` with no change to `packages/lint`.
**ADR-0087.** The D3 entry
`ui-page-action-row-on-success-beside-executor-refused` is a new file
under `migrations/entries/semantic/`. The changeset is `minor`, BREAKING
(accept-set narrowing), and carries the FROM → TO table and the marker.
## Measured before writing it (dispatch Zone 2)
1. **The built-in table.** At the `.objectui-sha` pin `1fb1ced36243`,
`ActionRunner.builtinExecutors`
(`core/src/actions/ActionRunner.ts:1140-1152`) is exactly `script`,
`url`, `modal`, `flow`, `api`, `form`, `navigation`. The file is
byte-identical at objectui `main` `72594f29`. That is `ActionType` plus
`navigation`, with nothing missing and nothing extra. **Holds.**
2. **One resolution.** The rule calls `effectiveRowExecutor` once per
row: `||`, the empty key included. "No effective executor" means it
resolves to `undefined`. **Holds.**
3. **Where it lives.** The url-target judgment is a factory chained per
row, not one shared refinement object. The `onSuccess` rule is a sibling
factory, chained identically on all four rows. The props gate reports it
with no lint change, as the lint pins below show. **Holds, with that
shape.**
4. **The message.** One text for both, with a true reason and a
prescription. **Partly corrected by measurement:**
- `modal` does not hop "a second time" in the console. `useActionModal`
resolves when the dialog closes: a saved form answers with the record,
an OK with nothing, a dismissal as a failure. So the hop *follows* the
dialog's answer, which the contract does not declare. With no host modal
handler, the runner answers `{ success: true, modal }` at once.
- `flow` is answered at the run's start. `judgeFlowLaunch` (app-shell
`utils/flowResponse.ts`) returns `{ success: true, silent: true }` for a
paused screen run and for a refused end.
- The text says what each executor does, not one blanket reason.
5. **Producers.**
- Zero in this repository: examples, `packages/qa`, `packages/cli` and
docs have no row `onSuccess` (the `onSuccess` hits there are React
callbacks).
- Zero in hotcrm at `0f84c9c`.
- objectui (at the pin and at `main` `72594f29`): one site goes red,
named below. `check-adr-0087-registration` asked for the D3 marker, so a
NEW entry file was added.
## objectui sites this turns red (no objectui edit here; the seat files
the companion)
-
`packages/types/src/__tests__/held-public-block-arms-10872.test.ts:224`:
`judge({ type, properties: { onSuccess: { navigate: '/tasks' } }
}).success` is asserted `true` for `action:button` and `action:icon`, on
both faces (`FACES`). Both faces reach the spec row by reference
(`public-blocks.zod.ts`). Remedy, one line: give the row `actionType:
'api'` (and a `target`), as triage named.
- Measured NOT red:
- `action-button-icon-inputs-11168.test.tsx:474-490` (`actionType:
'run'`, a host handler: open, and not parsed through the spec).
- `action-onSuccess-forward.test.tsx` (`type: 'api'`, not parsed through
the spec).
- `apps/console/src/__tests__/registry-inputs-spec-parity.test.ts`: its
object-arm and member probes (`{}`, `{ en, zh-CN }`, `{
__objectui_member_probe__: … }`) all fail inside the `onSuccess` block
first, so the row's object-level check never runs, and the verdicts are
unchanged. Simulated against this branch's rows.
## Tests
Read at `592e1bd8a` (the branch merged with `origin/main` `404fc477e`)
unless stated. Heavy runs went through `os-verify-lock.sh`; the verdicts
quoted are its VERDICT lines.
- `packages/spec` full `vitest run --project local` at `a2bfa8b7a`:
**669 files, 20370 passed, 1 todo**, VERDICT command-exit 0. The merge
brought nothing into `packages/spec`.
- `pnpm --filter @objectstack/spec typecheck` at `a2bfa8b7a`: exit 0
(tsc, scripts typecheck, test-typecheck held 52 files / 246 / 135).
- `@objectstack/lint` full at `a2bfa8b7a`: **138 files, 6694 passed, 5
skipped**; `typecheck` exit 0.
`validate-component-props-row-on-success.test.ts`: 32 tests (16 before).
- `check:generated`: "All 14 generated artifacts are up to date". The
dropped-refinements ledger is unmoved at 759 / 234. `check:api-surface`
is unmoved (the new module is not exported).
- New pins in `component-action-row-on-success-executor.test.ts`
(section 3), on all four rows, each with a negative control:
- each refused built-in through the key;
- `navigation` (alias refusal plus `onSuccess` refusal);
- the name leg (`name: 'form'`);
- no executor, and an empty key;
- accepted: `api`, `script`, host `run`, name-leg `api` / `script` /
`clone_task`, and a declared `api` key outranking `name: 'url'`;
- **ONE TEXT**: each row's text after its subject equals
`ActionSchema`'s for the same executor.
`action-on-success.test.ts` now asserts code, path and subject in place
of `toContain("'api'")`.
- **Ablation** at `a2bfa8b7a` (one-shot, through
`scripts/ablation-replace.mjs`, WRAP mode with a trap restore).
`rowOnSuccessRefused`'s body was replaced by `return executor ===
'ablated-23168-onsuccess-row';`.
- Mutation on disk: anchor 1 → 0, blob `42ffdc3eb2c1` → `b98144cf5270`.
- spec src leg: **52 failed / 65 passed**, exactly section 3's refusal
pins. Every accept and control pin passed.
- spec rebuilt; `ablation-dist-preflight` found the marker in 14 dist
files (exit 0).
- lint dist leg: **8 failed / 24 passed** (the two refusal pins × four
rows).
- Restore: blob == HEAD `42ffdc3eb2c1`, `git diff HEAD` empty. Rebuild,
then `--absent`: the marker is absent from all 232 dist files and the
tree is clean. Lint pins 32 / 32.
- **Gates** at `592e1bd8a`: `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack` (no paths) derived 111
commands. Each exit code was recorded before any pipe.
- Reconcile: `Run reconciliation — 111 derived, 110 run, 1 NOT-MEASURED,
0 UNRUN.` (exit 0). Every one of the 110 exits 0.
- Six first read `PREREQUISITE NOT MET` (exit 3) on an unbuilt
workspace: lint `check:doc-formula-expressions` and
`check:doc-security-posture`, spec `check:skill-examples`,
`check:docs-transcript-drift`, `check:dual-build-cjs-loads` and
`check:lean-entry-closure`. All six exit 0 after `turbo run build` (74 /
74).
- **NOT MEASURED:** `pnpm check:type-check-debt`. My own 900 s
per-command timeout killed it (exit 124) while `--re-measure` re-ran the
DEBT ledger's one entry, the workspace root program (`scripts/` and the
top-level configs). This diff touches nothing in that program, and no
tsconfig, typecheck script or ledger entry.
- Notable lines:
- `check-adr-0087-registration`: registered
`ui-page-action-row-on-success-beside-executor-refused` (new here);
- `check:spec-changes`: 126 converted / 442 migrated;
- `check:objectui-pin-citations`: 55 asserting citations match
`1fb1ced36`;
- `check:nul-bytes`: exit 0, and a raw control-byte scan of the touched
files finds 0 lines.
- eslint, narrowed:
- population: the 7 TS files of the diff (the `.md` and `.mdx` resolve
to no config; `--print-config` prints `undefined`);
- `--format json`: 7 files, 0 errors, 0 warnings;
- invariance: no type-aware linting (no `parserOptions.project`;
`eslint.config.mjs:327-329`), so this diff cannot move a verdict on an
untouched file.
## Acceptance notes (observations, not filed)
- **`operation: 'update'` rows.** The runner reads `operation` before
any executor (`ActionRunner.ts:1384-1388`). So a row with `operation:
'update'` and no executor key runs its declarative write and then hops.
Under C it is refused as a no-executor row. The prescribed `actionType:
'script'` changes nothing at run time, and `ActionSchema` itself
materialises `type: 'script'` for such an action. Conversely,
`ActionSchema` refuses `onSuccess` beside `operation: 'update'` on any
`type`, while the rows (whose `operation` is `z.unknown()`) never
carried that rule. That is outside this card's reach. Carrier: none.
- `component.zod.ts`'s rows-wide "VALUE posture" docblock still lists
`onSuccess` among the `z.unknown()` keys, which has been stale since the
rows took the block by reference. Carrier: the next PR to touch that
docblock.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01BRQ9Mp1yH7R1az6MaUM18H)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 14a13bc commit 1623faf
9 files changed
Lines changed: 482 additions & 56 deletions
File tree
- .changeset
- content/docs/references/ui
- packages
- lint/src
- spec/src
- migrations/entries/semantic
- ui
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
76 | 76 | | |
77 | 77 | | |
78 | 78 | | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
147 | 147 | | |
148 | 148 | | |
149 | 149 | | |
150 | | - | |
151 | | - | |
152 | | - | |
153 | | - | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
154 | 160 | | |
155 | 161 | | |
156 | 162 | | |
| |||
0 commit comments