Repository navigation
Commit b529a5c
feat(spec)!: a page action row's url target is judged on the executor the runner runs, its name leg included (#23289)
Part of #23168
Clause-②: no (narrowing)
The fifth door of ruling A+'s url-target family
(objectstack-ai/objectui#12191: the scheme set "is stated once in the
contract and the runtime fulfils exactly that"), on the card's **name
leg**, the half triage `6139317450` ruled. A page action row's `target`
is now judged against the executor the console's action runner actually
runs. One grammar, `navigation-target.ts`'s `navigationTargetRefusal`,
through the url-target refinement PR #23101 added; no second URL
grammar.
**This PR leaves #23168 open.** The card's other half, carrying
`ActionSchema`'s "`onSuccess` only beside `api` / `script`" to the rows'
effective executor (position 2, `6138882596`), is not in this diff. The
dispatch's assumption 4 has a fork clause, and measurement tripped it:
two objectui producers write a row with `onSuccess` beside a non-`api` /
non-`script` executor. That half goes back to the PM for a decision. See
the section below.
## What changed
- **`effectiveRowExecutor(row, executorKey)`**
(`packages/spec/src/ui/component.zod.ts`) states the runner's resolution
once: the row's executor key, or its `name` when that key is omitted or
empty. It uses `||`, as the runner writes it, not the `??` the triage
text transcribes. An empty `actionType` falls through to `name` at the
click (`'' || 'url'` is `'url'`), so it falls through here too. The
resolution-order pins hold this.
- **`ROW_URL_NAVIGATOR_EXECUTORS`** is `{ 'url', 'navigation' }`, the
two `builtinExecutors` rows that reach `navigateTo`.
- **`refuseUnopenableRowUrlTarget`** judges `target` whenever the
effective executor is one of those two, whichever key spelled it. This
applies on `action:button`, on `action:icon`, and on every
`action:group` / `action:menu` member. The refusal is
`navigationTargetRefusal`'s own text. Only the subject's parenthetical
names the spelling that resolved the executor:
- `actionType: 'url'`, unchanged byte for byte (the control);
- `` `name: 'url'` and no `actionType`, so the runner runs the url
navigator `` on the name leg, which tells the author that declaring the
executor key is how a `name` stops choosing the navigator (the same
teaching #23101's dashboard subject uses for an omitted `actionType`);
- `` `actionType: 'navigation'` ``: that row is still refused at the key
by #23164's alias rule, and its `target` is now judged too, so the
author learns both at once.
- ⛔ **`name` is not refused** as an executor spelling. A row whose
`name` names any other handler (`navigate_edit`, `convert_lead`, `flow`,
…) is untouched, and a declared executor key still outranks the `name`.
- The `name` and `target` describes on the three shapes state the name
leg. `component.mdx` is regenerated.
- Added the D3 entry `ui-page-action-row-name-leg-url-target-judged` and
the changeset `.changeset/23168-page-action-row-name-leg-url-target.md`:
`@objectstack/spec` `minor`, BREAKING (accept-set narrowing,
launch-window convention), `Clause-②: no (narrowing)`, ADR-0087
`registered`, with a FROM → TO table and the one-line fix.
- `os validate`'s props gate (`@objectstack/lint`
`validateComponentProps`) parses `properties` against these same rows,
so it reports the refusal at `properties.target` /
`properties.actions.N.target` with no lint code change.
`validate-component-props-url-target.test.ts` gains the name-leg
readings. `validate-component-props.ts` is untouched, which keeps clear
of PR #23244's severity rework there.
## The `onSuccess` half: the fork the dispatch's assumption 4 names
**Position 2's premise holds.** objectui's runner navigates twice for a
navigating executor that also declares `onSuccess.navigate`. Read at the
`.objectui-sha` pin `1fb1ced36243`, byte-identical at objectui `main`
`866763bd`:
- the first navigation: a `url` row runs `executeUrl` → `navigateTo`
(`ActionRunner.ts:1934`; `navigation` reaches the same navigator through
`executeNavigation`, `:2227`);
- the second: `execute` then calls `handlePostExecution` (`:1418`) →
`readOnSuccessNavigation` (`:1640`) → `navigateOnSuccess` (`:1642`).
A registered host handler takes the same post-hop (`:1384-1387`), so a
host handler that navigates (`navigate`, `navigate_create`,
`navigate_edit`) also navigates twice.
**The fork.** Applied verbatim, the rule refuses `onSuccess` beside
every effective executor other than `api` / `script`. The census found
two producers that write exactly that:
1. **objectui
`packages/components/src/renderers/action/__tests__/action-button-icon-inputs-11168.test.tsx:474-490`**
writes `actionType: 'run'`, the recording host handler `node()` sets at
`:176-183` with `handlers: { run }`. Beside it sits `onSuccess: {
navigate: '/app/records/${result.id}' }`, with and without `openIn:
'newTab'`, on both `action:button` and `action:icon`.
- **At the runner:** `run` answers `{ success: true, data: { id: '42' }
}` (`:113`). The hop then navigates **once**, to `/app/records/42`, and
the test pins that. This is a host handler whose post-success hop works
and reads `${result.*}` from the handler's return.
- This test mounts through the renderer and never parses the node
through the spec.
2. **objectui
`packages/types/src/__tests__/held-public-block-arms-10872.test.ts:224`**
asserts that the installed spec's `action:button` / `action:icon` row
**accepts** `properties: { onSuccess: { navigate: '/tasks' } }`, a row
with no executor key and no `name`. It is the remedy target of that
file's flat-`onSuccess` refusal.
- **At the runner:** the executor resolves to `''`, no handler or
built-in matches, and `executeActionSchema` answers "not executable"
(`:2118-2123`). The result is unsuccessful, so the hop never runs.
- The verbatim rule would refuse it, and this objectui test would go red
at objectui's next spec bump. It would not red `main` here: the Console
Pin Gate builds the console and runs no objectui tests.
The rest of the census found zero rows with `onSuccess` beside a
non-`api` / non-`script` executor:
- this repository's `examples/**`, `packages/cli` and `packages/qa`
(dogfood); its only row `onSuccess` producers are spec and lint tests,
all beside `api`;
- objectui's app metadata at the pin;
- hotcrm at `0f84c9c`.
The dispatch says: ⛔ do not silently narrow to the navigating subset,
and do not refuse the producer. So no `onSuccess` rule is in this diff.
The options and the four-axis analysis are in the dev report on the
card. The ruled half is independent of the fork, so it ships here.
## Mechanism readings (the dispatch's assumptions)
- **A1 held**, with one correction. `ActionRunner.execute` reads
`action.type || action.actionType || action.name || ''` at `:1265`, at
the pin `1fb1ced36243` and unchanged at objectui `main` `866763bd`.
- The renderers forward the executor key as the runner's `type`:
`action-button.tsx:286` `type: schema.actionType`,
`action-icon.tsx:167`, `action-group.tsx:350` `type: action.type`,
`action-menu.tsx:274`.
- The operator is `||`, not `??`, so the helper uses `||`.
- The registrations' `defaultProps` (`actionType: 'script'`) are read
only by the designer palette (`PageDesigner.tsx:216`), never at render,
so an omitted key stays omitted.
- The url navigator `navigateTo` is reached by exactly `url` and
`navigation`. No host registers a handler under either name:
`ConsoleShell.tsx:97` and `useConsoleActionRuntime.tsx:803` register
`api` / `flow` / `script` / `modal`, and `AppContent.tsx` /
`useObjectActions.ts` register `crud_success`, `dialog_cancel`,
`navigate_create`, `navigate_edit`, `create`, `delete`, `navigate` and
`refresh`.
- Handlers win before built-ins. The legacy `action.navigate` fallback
is a key the strict rows refuse.
- **A2 held.** At `4f5679c26`, `actionType: 'navigation'` was refused at
`actionType` (#23164), and `name: 'navigation'` / `name: 'url'` with no
executor key parsed clean with a `javascript:` target, on all four rows.
`actionType: ''` with `name: 'url'` parsed clean too. The pins assert
what the runner does for each spelling.
- **A3 held.** `validateComponentProps` parses `properties` against
`ComponentPropsMap` (`validate-component-props.ts:372`) and files every
non-`unrecognized_keys` issue as `component-props-invalid` at its path.
The lint pins read the new refusal there, from the rebuilt `dist/`.
- **A4 tripped its fork clause** (above).
- **A5 held** (above): two navigations, cited at the pin.
- **A6:** the name leg's refusal is `navigationTargetRefusal`'s, with
#23101's subject form. No new phrasing.
## FROM → TO
| row | before | after |
| --- | --- | --- |
| `action:button` / `action:icon` with no (or an empty) `actionType` and
`name: 'url'` or `'navigation'` | any `target` | `target` judged by the
url scheme set, refused at `properties.target` |
| an `action:group` / `action:menu` member with no (or an empty) `type`
and `name: 'url'` or `'navigation'` | any `target` | `target` judged by
the url scheme set, refused at `properties.actions.N.target` |
| `actionType: 'navigation'` / member `type: 'navigation'` with a
refused `target` | refused at the executor key only | refused at the
executor key (unchanged) and at `target` |
| `actionType: 'url'` / member `type: 'url'` | judged | unchanged, same
message |
**Producers:** none. No page action row in this repository, in objectui
(pin `1fb1ced36243`, `main` `866763bd`) or in hotcrm (`0f84c9c`) is
named `url` or `navigation`.
## Verification record
Readings are at `f1a31cee7` unless stated. That head is this branch
merged with `origin/main` `6d7f4df3c` through
`scripts/pm/os-regen-merge.sh`, with no conflicts. Every heavy run went
through `scripts/pm/os-verify-lock.sh`, and each result is its `VERDICT
command-exit` line.
- **Builds:**
- `@objectstack/spec` build: exit 0.
- `check:generated`: "All 14 generated artifacts are up to date", exit
0. Before `gen:docs` it named only `content/docs/references/**`.
- The dropped-refinements ledger did not move (759 sites / 234 schemas):
this diff changes an existing refinement and adds no site.
- Lint's dependency closure (`'@objectstack/lint^...'`) and
platform-objects' closure: exit 0.
- The workspace build (`turbo run build --filter=!@objectstack/docs
--concurrency=2`): 74 / 74.
- **Typecheck:** `@objectstack/spec typecheck` exit 0 (at `b76c26a99`),
and `@objectstack/lint typecheck` exit 0.
- **Tests:**
- `@objectstack/spec`, full (`vitest run --project local`), at
`b76c26a99`: **668 files, 20254 passed, 1 todo**, exit 0. The merge
since brought nothing into `packages/spec`.
- The new `component-action-row-effective-executor.test.ts` holds 44
tests. With `url-target-doors.test.ts`,
`component-action-row-on-success-executor.test.ts` and
`component-props-unknown-members.pin.test.ts`: 4 files, 163 passed.
- `@objectstack/lint`, full: **138 files, 6686 passed, 5 skipped**, exit
0. `validate-component-props-url-target.test.ts` holds 20 tests (8
before).
- **The importers, narrowed and declared.** The run covered every test
file outside spec and lint that names a page action row or
`ComponentPropsMap` (6 files in 4 packages), plus `platform-objects` in
full:
- `platform-objects`: 70 files, 1096 passed;
- `mcp`: 1 / 11;
- `cloud-connection`: 1 / 16;
- `example-showcase`: 3 files / 14.
The blind spot is a test that builds such a row from computed values. CI
runs the full farm.
- **Ablation**, one-time, at `b76c26a99`, through
`scripts/ablation-replace.mjs` with its restore trap.
`effectiveRowExecutor` lost its name leg (the base behaviour), marked
`ablated-23168-name-leg`.
- **Spec, src leg:** 12 failed / 57 passed. These are exactly the 8
name-leg refusals and the 4 empty-key refusals; every control and all of
`url-target-doors.test.ts` stayed green.
- **Lint, dist leg:** after a spec rebuild, `ablation-dist-preflight`
found the marker in 14 dist files. The lint file then read 4 failed / 16
passed: the four name-leg refusals at the gate.
- **Restore:** blob == HEAD (`a3d90ed32459`) with an empty `git diff
HEAD`. A spec rebuild followed, and `--absent` found the marker in none
of the 232 dist files, with the tree clean.
- **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 111 at `f1a31cee7`. Every exit was
recorded before any pipe.
- 110 ran, and every final exit is 0.
- Eleven first exited 3 or 1 with PREREQUISITE NOT MET. Eight of them
read `packages/spec/dist` while a closure build of mine was rewriting it
("there is none to read"). Three needed the workspace build: spec
`check:skill-examples`, `check:dual-build-cjs-loads` and
`check:lean-entry-closure`. All eleven exit 0 on the rerun at the same
head.
- `check:type-check-debt` is NOT MEASURED (Acceptance notes).
- The `--ran` reconcile reads "111 derived, 110 run, 1 NOT-MEASURED, 0
UNRUN", exit 0.
- `check-adr-0087-registration` registers
`ui-page-action-row-name-leg-url-target-judged` (new here).
- `check:spec-changes` reads 126 converted / 440 migrated.
- `check:objectui-pin-citations`: 55 asserting citations match
`1fb1ced36`.
- `check:nul-bytes` is green, and a raw control-byte scan of the touched
files finds 0 lines.
- **eslint, narrowed and proven.** The population is the 4 TypeScript
files of this diff, read from eslint's own config: the changeset `.md`
and `component.mdx` resolve to no config (`--print-config` answers
`undefined`). `--format json` reads 4 files, 0 errors and 0 warnings.
Invariance: this config enables no type-aware linting (no
`parserOptions.project`; the config says so itself in its comment at
`eslint.config.mjs:327-329`), so this diff cannot move the verdict on
any file it does not touch.
- **Size:** 6 files, +368 / -42 = 410 changed lines.
## Acceptance notes
- **#23168 stays open for the `onSuccess` half.** The merge of this PR
does not close it (`Part of`).
- **`check:type-check-debt` is NOT MEASURED here.** Its `--re-measure`
mode re-runs tsc over every DEBT / TEST_DEBT ledger entry repo-wide.
This diff changes no tsconfig, no `typecheck` script and no ledger
entry, and CI runs it.
- **The spec full suite** was read at `b76c26a99`, before the merge of
`main`. The merge brought `6d7f4df3c` and two earlier commits, which
touch service-mailbox, create-objectstack, `scripts/pm/ci-failure.mjs`,
a workflow, two changesets and the lockfile, and nothing in
`packages/spec`. The row pins and the lint suite were re-read at the
merged head.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01BRQ9Mp1yH7R1az6MaUM18H)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 01d5034 commit b529a5c
6 files changed
Lines changed: 368 additions & 42 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | | - | |
| 31 | + | |
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
41 | | - | |
| 41 | + | |
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
| |||
86 | 86 | | |
87 | 87 | | |
88 | 88 | | |
89 | | - | |
| 89 | + | |
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
| |||
96 | 96 | | |
97 | 97 | | |
98 | 98 | | |
99 | | - | |
| 99 | + | |
100 | 100 | | |
101 | 101 | | |
102 | 102 | | |
| |||
123 | 123 | | |
124 | 124 | | |
125 | 125 | | |
126 | | - | |
| 126 | + | |
127 | 127 | | |
128 | 128 | | |
129 | 129 | | |
| |||
132 | 132 | | |
133 | 133 | | |
134 | 134 | | |
135 | | - | |
| 135 | + | |
136 | 136 | | |
137 | 137 | | |
138 | 138 | | |
| |||
176 | 176 | | |
177 | 177 | | |
178 | 178 | | |
179 | | - | |
| 179 | + | |
180 | 180 | | |
181 | 181 | | |
182 | 182 | | |
| |||
186 | 186 | | |
187 | 187 | | |
188 | 188 | | |
189 | | - | |
| 189 | + | |
190 | 190 | | |
191 | 191 | | |
192 | 192 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
55 | 55 | | |
56 | 56 | | |
57 | 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 | + | |
0 commit comments