Repository navigation
Commit 4d1e4a8
feat(spec)!: a page action row's onSuccess is ActionSchema's own block, and its executor key refuses the 'navigation' alias (#23164)
Fixes #23103
Clause-②: no (narrowing)
The fourth 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"), and the folded
`'navigation'` position (`6133636513`), on the four page action row
shapes: `action:button`, `action:icon`, and each `action:group` /
`action:menu` member. One grammar,
`packages/spec/src/ui/navigation-target.ts`'s `navigationTargetRefusal`;
no second shape and no second URL grammar.
- **`onSuccess`** on all four rows is now `ActionSchema.onSuccess`'s own
block, by reference: the accessor `actionOnSuccessBlock` returns
`ActionSchema.in.shape.onSuccess.unwrap()`, read off the object half of
the action's pipe. It is the same instance, so a row's verdict on any
`onSuccess` value is the action's. `navigate` is judged by
`refuseUnopenableOnSuccessNavigate` (since #23101). The block's rename
prescriptions and its closed `openIn` come with it. `action.zod.ts` is
unchanged, so what `ActionSchema` accepts is unchanged, and no export is
added.
- **The executor key** (`actionType` on a node, `type` on a member)
refuses `'navigation'`, the console runner's local alias of `'url'`, at
that key, with the rename onto `'url'`. Every other value stays open:
the runner dispatches host-registered handler names first, and this
package's released guidance prescribes `actionType: 'navigate_edit'`.
## Mechanism readings (the dispatch's A1 to A3)
- **A1 held.** On `main` at `f931e765b`, the three rows declared
`onSuccess: z.unknown()`. objectui's runner reads only `ActionSchema`'s
block from a row's forwarded `onSuccess` (pin `1fb1ced36243`).
`handlePostExecution` runs after every executor, a host handler
included. It calls `readOnSuccessNavigation`, which needs a non-array
object with a non-empty string `navigate`, and then `navigateOnSuccess`,
which classifies the interpolated URL and reads `openIn === 'newTab'`.
**No key outside the block is read, so there is no fork.** Sharing needs
no import cycle (`component.zod.ts` already imports `action.zod.ts`) and
no new export: the pipe's `in.shape` is the pattern
`buildObjectFormSectionFieldEntry` already uses for `FormFieldSchema`.
- **A2, partly falsified.** `ACTION_TARGET_ALIASES` and
`ACTION_NODE_ALIASES` alias KEYS (`url` to `target`, `type` to
`actionType`), not executor VALUES. So the contract declares no
executor-value set for the rows beyond the row's own describe: six
built-ins, plus "a handler registered under another name is dispatched
too". The runner's built-in table is the spec's six plus `navigation`,
and `navigation` is its only url-like alias. `executeNavigation` reads
`to || target || redirect` and hands off to `navigateTo`, the navigator
`executeUrl` calls with `target || redirect`. On a row, the alias's own
keys are not row keys, so the two spellings run the same navigation on
the same `target`. The only contract-side mention of `'navigation'` is
`normalizeInlineAction`'s fold on the `element:button` inline action, a
bridge kept for cloud tenant pages.
- **The A2 choice: the alias is refused; the key is not closed.**
Closing the key to `ActionType` would refuse real producers. The console
registers `navigate_create`, `navigate_edit`, `create`, `delete`,
`navigate`, `refresh`, `crud_success` and `dialog_cancel`, and documents
`actionType: 'navigate_create'` on an `action:button`. This package's
released CHANGELOG prescribes `actionType: 'navigate_edit'`, and
`component-action-member-params-array-only.pin.test.ts` pins it. So that
route is a fork, not a fix. Between judging `'navigation'` as a url and
refusing it, the four axes give:
- **Real demand:** zero rows write the alias, and the rename is
lossless.
- **Long-term fit:** one spelling per executor. The contract does not
encode a renderer-local alias as accepted.
- **AI error-proofing:** an AI cannot write the alias, and it gets a
loud prescription instead.
- **Startup scope:** an alias with no named producer retires at once,
with no bridge.
All four agree, so the alias is refused.
- **A3 held.** `os validate`'s props gate parses `properties` against
these same rows, so both refusals reach it with no lint code change.
`validate-component-props-row-on-success.test.ts` pins this.
## Producer census
Objectstack at `bb41814b8`, objectui at the pin `1fb1ced36243`, tests
included:
- **`onSuccess` on a row:** no row anywhere writes an `onSuccess`
outside the block.
- objectui's row tests write `{ navigate: '/app/records/${result.id}',
openIn: 'newTab' }`, `{ navigate: '/app/records/${result.id}' }` and `{
navigate: '/tasks' }`; all three are admitted.
- The callback shapes (`{ type: 'notify' }`, arrays) appear only in
objectui runner tests on `ActionDef`, never on a row.
- Objectstack has no row `onSuccess` outside `ActionSchema` contexts.
- **`'navigation'` as a row executor:** zero.
- Every `type: 'navigation'` hit is an `element:button` inline action,
an `InlineActionSchema` test, or an objectui runner or hook test.
- In this repo, the only page action rows outside tests are the D2
conversion fixtures in `conversions/registry.ts` (`actionType: 'api'`).
- **Executor values outside the six built-ins on rows:**
`navigate_edit`, in a spec pin test and in a released CHANGELOG
prescription. These stay accepted.
- **NOT MEASURED:** cloud, hotcrm and deployed metadata. Neither
checkout is in this container.
## FROM → TO
| key | before | after |
| --- | --- | --- |
| `onSuccess` on `action:button` / `action:icon` / `action:group` member
/ `action:menu` member | any value (`z.unknown()`) | `ActionSchema`'s `{
navigate, openIn? }` block. `navigate` is an `http(s)://` URL, a
`mailto:` address, an in-app path starting with `/` or `./`, or a
template that opens with `${`. `openIn` is `'self'` (materialised
default) or `'newTab'`. Unknown keys get the block's renames (`url`,
`redirectUrl`, `to`, … onto `navigate`). |
| `actionType` on `action:button` / `action:icon` | any string | any
string except `'navigation'`, which is refused at `actionType` with the
rename onto `'url'`; `os migrate meta --write` and the stored-row seam
rewrite it (D2 `page-action-row-navigation-executor-to-url`) |
| `type` on an `action:group` / `action:menu` member | any string | any
string except `'navigation'`, which is refused at `type` with the rename
onto `'url'`; `os migrate meta --write` and the stored-row seam rewrite
it (D2 `page-action-row-navigation-executor-to-url`) |
The changeset carries the per-value FROM → TO table and the one-line
fix. It is
`.changeset/23103-page-action-row-on-success-and-executor.md`:
`@objectstack/spec` minor, BREAKING under the launch-window convention.
It registers two ADR-0087 D3 entries,
`ui-page-action-row-on-success-shape-closed` and
`ui-page-action-row-navigation-executor-refused`. The executor leg is
lossless, so it also lands as the D2 conversion
`page-action-row-navigation-executor-to-url`: `retiredFromLoadPath:
true`, `retiredAfter: '17.7.0'`, in `MAJOR_18_CONVERSIONS` at order 63.
The D3 entry names it in `conversionIds`. The `onSuccess` leg stays D3
only, because a refused address has no lossless rewrite.
## Verification record
### Fix round, at `f4d39d7aa`
All readings are at `f4d39d7aa` unless stated. That head is this branch
merged with `origin/main` `d889448b9` through
`scripts/pm/os-regen-merge.sh`; three later `main` commits touch none of
these 13 files. Every heavy run went through
`scripts/pm/os-verify-lock.sh`, and each result is its `VERDICT` line.
- **Builds:**
- The workspace build: 74 / 74, exit 0.
- `@objectstack/spec check:generated`: "All 14 generated artifacts are
up to date", exit 0.
- The ledger moved only by main's own sites: 234 / 759 after the merge,
with this branch's four sites kept.
- **Typecheck:** `@objectstack/spec typecheck` and `@objectstack/lint
typecheck`, exit 0.
- **Tests:**
- Spec, full: 663 files, 20149 passed, 1 todo.
- Lint, full: 138 files, 6680 passed.
- Five spec files, 313 passed: the conversion's own test,
`conversions.test.ts`, `retired-after.census.test.ts`, the row pin and
`expression-scope-data-root.pin.test.ts`.
- The lint row pin: 16 / 16.
- `driver-turso`: 88 files, 2373 passed, 33 skipped.
- **Ablations:**
- Round 1's two, (A) the rows' `onSuccess` block and (B) the alias
refusal, stand as recorded at `bb41814b8`.
- Round 2 adds two src-only ablations of the conversion, through
`scripts/ablation-replace.mjs`, at `de990e9bf`. The node leg's guard was
compared to `ablated-23103-conv-node`, and the member leg's container
set was emptied.
- Each read 3 failed / 261 passed over the conversion test plus
`conversions.test.ts`: the fixture pair, and that leg's two stored-row
pins.
- Each was restored to blob == HEAD with an empty `git diff HEAD`.
- **Gates:** `dispatch-gates --commands --repo
objectstack-ai/objectstack` derived 111 at `f4d39d7aa`.
- 110 ran, all exit 0.
- `check:type-check-debt` is NOT MEASURED, by order.
- The `--ran` reconcile reads "111 derived, 110 run, 1 NOT-MEASURED, 0
UNRUN".
- `check-adr-0087-registration` registers both D3 ids.
`check:spec-changes` reads 125 converted / 436 migrated.
- **Size:** 13 files, +899 / -37 = 936 changed lines.
### Round 1, at `bb41814b8` (kept as recorded)
All readings are at `bb41814b8` (this branch merged with `origin/main`
`d206d08ca` through `scripts/pm/os-regen-merge.sh`) unless stated. Heavy
runs went through `scripts/pm/os-verify-lock.sh`, and each result is the
`VERDICT command-exit` line it printed.
- **Builds:**
- `@objectstack/spec` build: exit 0.
- The workspace build (`turbo run build --filter=!@objectstack/docs
--concurrency=2`): 74 / 74 tasks, exit 0.
- `@objectstack/spec check:generated`: "All 14 generated artifacts are
up to date", exit 0. It named only `content/docs/references/**` as stale
before `gen:docs`.
- **Ledger:** `build-schemas` refused the four new sites and printed the
corrected entries. Those four are the whole ledger diff (232 / 752 to
232 / 756).
- **Typecheck:** `@objectstack/spec typecheck` (tsc, scripts, test
layer) and `@objectstack/lint typecheck`: exit 0.
- **Tests:**
- `@objectstack/spec`, full (`vitest run --project local`): 661 files,
20046 passed, 1 todo, exit 0. The new
`component-action-row-on-success-executor.test.ts` holds 25 tests.
- `@objectstack/lint`, full: 139 files, 6670 passed, exit 0. The new
`validate-component-props-row-on-success.test.ts` holds 16 tests.
- `@objectstack/platform-objects`, full: 70 files, 1096 passed.
- **The affected closure, narrowed and declared:** `turbo ls --affected`
names 82 packages. That set comes from the package graph, so it is blind
to tests that read another package's source from disk. The run here is
spec, lint and platform-objects in full, plus every other test file in
the repo that names an action row, `ComponentPropsMap` or `onSuccess`: 9
files in 6 packages, all green. They are `example-showcase` (3 files /
14 tests), `client-react` 1 / 13, `cloud-connection` 1 / 16,
`create-objectstack` 1 / 37, `mcp` 1 / 11 and `runtime` 1 / 14. The
blind spot is a test that builds such a row from computed values. CI
runs the full farm.
- **Ablations, one per new refusal mechanism.** Each went through
`scripts/ablation-replace.mjs` with its restore trap. Each restore ended
at blob == HEAD (`fd59b06cce9e`) with an empty `git diff HEAD`. Each
mutation leg rebuilt the spec JS dist with `tsup`, because `gen:schema`
would refuse the moved ledger sites under the ablation.
`ablation-dist-preflight` then found the marker in 6 dist files before
the dist leg ran.
- **(A) The rows' `onSuccess` block:** the accessor became
`z.unknown()`, marked `ablated-23103-onsuccess`. Spec, src leg: 13
failed / 12 passed. That is every `onSuccess` refusal, accepted control,
verdict-parity and identity pin, while the executor pins stay green.
Lint, dist leg: 4 failed / 12 passed. With the block ablated back to the
base's `z.unknown()`, all four rows answer 0 findings at
`onSuccess.navigate`, which is the red half.
- **(B) The executor alias refusal:** the guard compared to
`ablated-23103-alias`. Spec, src leg: 4 failed / 21 passed. Lint, dist
leg: 4 failed / 12 passed.
- **Restore leg:** a full spec build, then both markers absent from all
232 dist files, the tree clean against HEAD, spec 94 / 94 (the new file
plus `component-props-unknown-members.pin.test.ts`) and lint 16 / 16.
- **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 111 at `bb41814b8`.
- 110 ran, every exit recorded before any pipe, all exit 0.
- Six first exited 3 on PREREQUISITE NOT MET (unbuilt packages) and
exited 0 after the workspace build at the same head. They are lint
`check:doc-formula-expressions` and `check:doc-security-posture`, spec
`check:skill-examples`, and `check:docs-transcript-drift`,
`check:dual-build-cjs-loads` and `check:lean-entry-closure`.
- `check:type-check-debt` is NOT MEASURED, by order.
- The `--ran` reconcile reads "111 derived, 110 run, 1 NOT-MEASURED, 0
UNRUN".
- `check-adr-0087-registration` registers both D3 ids (new here).
`check:objectui-pin-citations` reports 55 asserting citations matching
`1fb1ced36`. `check:nul-bytes` is green, and the raw control-byte scan
of the 9 touched files finds 0 lines.
- **Size:** 9 files, +528 / -37 = 565 changed lines.
## Acceptance notes
- **Rider: #23036's `component.zod.ts` TSDoc twin (`6137432578`), with
the addendum from #23146's contract review (`6137629159`, ③).** This is
declared, from the `domain:spec` seat. The `page:tabs` item
`visibleWhen`:
- The TSDoc now says the component-node evaluator mounts no `data` root
(objectui#9308 ruling B; `SchemaRenderer.tsx` at `1fb1ced36243`). Its
cite moves to `containers.tsx:642-649` at `1fb1ced36243`.
- The `.describe()` parenthetical "(not the data-source adapter)" became
"(the component-node evaluator mounts no `data` root)". `component.mdx`
was regenerated by `gen:docs`.
- #23146 landed (`02bec6ca86`), and
`expression-scope-data-root.pin.test.ts` gains the TSDoc-face assertion
for this `page:tabs` twin.
- **Files beyond the claim's literal list:**
- `component-props-unknown-members.pin.test.ts`: the `onSuccess` line
leaves its `runner` ledger. Its §1 second half refuses a line whose
member is no longer `z.unknown()`.
- The four ledger sites `build-schemas` named. `ui/ActionButtonProps`
and `ui/ActionIconProps` gain `onSuccess`; `ui/ActionGroupProps` and
`ui/ActionMenuProps` gain `actions.element.onSuccess`. Header 232 / 756.
- The regenerated `component.mdx`, and two D3 entries.
- From the fix round on contract review `6138862349`: the D2 entry in
`conversions/registry.ts`, its test
`page-action-row-navigation-executor-to-url.test.ts`, and its step-18
rationale fragment in `migrations/registry.ts.template`.
- **A row's parsed `onSuccess` now carries `openIn: 'self'` when
omitted, as an action's does.** The runner reads only `=== 'newTab'`, so
behaviour is unchanged. objectui composes these rows through
`stripImportedDefaults`, which drops the default on its side.
- **Not carried, by design:** `ActionSchema`'s rules ABOUT `onSuccess`
are refinements of the action, not of the block, so they stay where they
are:
- `onSuccess` is refused beside a `type` other than `api` / `script`;
- `onSuccess` is refused beside `opensInNewTab: true`.
The rows' executor set is open and the runner hops after every executor.
So `actionType: 'url'` plus `onSuccess` navigates twice, ending at
`navigate`, and the same goes for a host handler. This was read from
source, not executed. Whether rows should refuse it is a contract
question for its own card. The accessor's TSDoc records this.
- **The runner's `name` leg.** `ActionRunner.execute` reads the executor
as `type || actionType || name`. The node renderers forward `type:
schema.actionType` and the containers forward `type: action.type`, so a
row with no executor key and `name: 'url'` (or `'navigation'`) reaches
the url navigator, and its `target` is judged only at the click. That
runtime half was read from source, not executed. Measured at
`bb41814b8`: the props gate answers 0 findings for an `action:button`
with `{ name: 'url', target: 'javascript:alert(1)' }`, and for the same
with `name: 'navigation'`; the `actionType: 'url'` control answers 1.
This is the same family, carried by #23168, and it is not fixed here.
`refuseUnopenableRowUrlTarget`'s docblock now scopes its guarantee to
the executor key.
- **objectui `useObjectActions`' host `navigate` handler** reads
`params.url` with its own prefix rule (`startsWith('/')`, else under the
app base). This is a host handler outside the contract's executor set,
read only, with no producer found.
- **`check:type-check-debt` is NOT MEASURED, by order.**
- **Owed:** a contract-tier review before the queue (Clause-② suspect on
`packages/spec/src/**`). No governed path is touched.
---
_Generated by [Claude
Code](https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 76815b9 commit 4d1e4a8
13 files changed
Lines changed: 899 additions & 37 deletions
File tree
- .changeset
- content/docs/references/ui
- packages
- lint/src
- spec
- src
- conversions
- 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 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
0 commit comments