Repository navigation
Commit f931e76
feat(spec)!: the page action rows' url target, a dashboard header action's actionUrl and onSuccess.navigate apply the shared url-target refusal (#23101)
Fixes #23077
Clause-②: no (narrowing)
The rest of ruling A+'s url-target family. Ruling A+ on
objectstack-ai/objectui#12191 says the scheme set "is stated once in the
contract and the runtime fulfils exactly that". PR #23076 (`9b37edbd7`)
stated it once, as `navigationTargetRefusal` in
`packages/spec/src/ui/navigation-target.ts`, and applied it at the two
url-action doors. This PR applies the same function, with no second URL
grammar, at the three doors the triage direction (comment 6129646171)
names. A target that opens with `${` keeps the predicate's own rule: it
is left alone at the door and judged by the runner after interpolation.
## What changed
1. **Page action rows** (`packages/spec/src/ui/component.zod.ts`). One
refinement factory, `refuseUnopenableRowUrlTarget`, is applied to
`ActionButtonPropsSchema` and `ActionIconPropsSchema` (when `actionType:
'url'`) and to the `action:group` and `action:menu` members (when `type:
'url'`). It judges `target` and refuses at `target`. On every other
executor `target` names a script, flow, page, endpoint or form, so it is
not judged, and a row with no `target` is not judged either. `os
validate`'s props gate (`@objectstack/lint` `validateComponentProps`)
parses `properties` against these same `ComponentPropsMap` rows, so it
now reports the refusal at the row's `target` path, with the parse's own
first sentence. The refusal is a `component-props-invalid` finding, a
warning, because warning is that gate's only tier (see its module
header). The card puts the gate's upgrade to error out of scope. The
lint package changes no code; one test file pins the gate's reading.
2. **Dashboard header actions**
(`packages/spec/src/ui/dashboard.zod.ts`). `DashboardHeaderActionSchema`
judges `actionUrl` when `actionType` is `'url'` **or omitted**, and
refuses at `actionUrl`. On `flow` / `script` / `modal` / `api` / `form`,
`actionUrl` is untouched. The refusal's subject names both spellings of
the url route ("a url target when `actionType` is `'url'` or omitted"),
so an author who meant a flow learns that declaring `actionType` is the
way out.
3. **Post-success navigation** (`packages/spec/src/ui/action.zod.ts`).
The `onSuccess` block judges `navigate` on every value and refuses at
`onSuccess.navigate`. `${result.redirectUrl}` stays accepted and
`/apps/crm/tasks/${result.id}` stays accepted. `tasks/${result.id}` is
refused, because the runner refuses `tasks/42` after interpolation too.
The `describe` text and TSDoc on each key now state the set. Three D3
entries are added, one per door:
`ui-page-action-row-url-target-scheme-set-closed`,
`ui-dashboard-header-action-url-scheme-set-closed` and
`ui-action-on-success-navigate-scheme-set-closed`. There is one
changeset, `.changeset/23077-url-target-doors-scheme-set.md`:
`@objectstack/spec` `minor`, BREAKING, with a FROM → TO table and the
one-line fix, `Clause-②: no (narrowing)`, and ADR-0087 `registered` with
the three ids.
**Empty values.** At the three doors that have no target-required rule
(dashboard `actionUrl`, `onSuccess.navigate`, a row's `target`), the
predicate judges an empty string like any other value and refuses it.
This keeps the predicate's own rule. The runtime does nothing with an
empty value either: the dashboard renderer warns on an empty
`actionUrl`, `readOnSuccessNavigation` drops an empty `navigate`, and
`executeUrl` fails with "No URL provided". The changeset's FROM → TO
table names this case.
### The dispatch's mechanism assumptions, as measured
- **A1** (lines). Every door was a free string at `0aef78881`.
`dashboard.zod.ts:109` `actionUrl: z.string()` and the three
`component.zod.ts` rows at `:3834` / `:3931` / `:4114` matched.
`onSuccess.navigate` was at `action.zod.ts:1785`, not `:941`. `:941` is
the top-level `navigate` guidance key, a different thing.
- **A2** (red at base). Reproduced through one-time ablations, recorded
below. With each door's refinement removed, which is the base behaviour,
these pins read red:
- the schema-level refusals;
- the `ObjectStackDefinitionSchema.safeParse` / `defineStack` reach pins
for the dashboard and `onSuccess` doors;
- the props-gate pins, where the expected finding is missing because the
gate answered 0 findings.
- **A3** (omitted `actionType`). The contract states no default for
`actionType`. Its two readers that assign one both assign `'url'`:
objectui `DashboardRenderer` builds the header `ActionDef` with `type:
(a.actionType) || 'url'`, and this repo's `validateDashboardActionRefs`
uses `strName(action.actionType) ?? 'url'`. **Pinned reading: an omitted
`actionType` is the url route and is judged.** The renderer's click
handler, read at objectui `origin/main` `7af0ab37` and at the pin
`7578323`, returns early with a console warning on an omitted
`actionType`. So a refused target never works on either reading, and
nothing that works today is refused. The click handler's split is an
Acceptance note below.
- **A4** (deferred `${`). Held: `${result.redirectUrl}` is accepted at
`onSuccess.navigate`, and that is pinned.
### File surface beyond the claim's literal list (declared)
- `packages/spec/dropped-refinements.baseline.json`. `build-schemas.ts`
refuses a published schema that gains a refinement it cannot project
unless the new site is declared in this ledger. Its own prescription is
"Declare it by adding…", and it says "⛔ do not delete or weaken" the
rule. The ledger gains 21 sites: 17 in 13 existing entries, and 4 new
entries (`ui/DashboardHeaderAction`, `ui/DashboardHeader`,
`ui/ActionButtonProps`, `ui/ActionIconProps`). The `measured` header
totals are updated to match its body (231 schemas / 734 sites), which
`scripts/dropped-refinements.test.ts` pins. No site is removed.
- The three D3 entry files. `check-adr-0087-registration` needs them: a
changeset with a FROM → TO prescription cannot claim
`no-migration-prescription`.
- `content/docs/references/ui/{action,dashboard,component}.mdx`,
regenerated by `gen:docs`. `check:generated` named only `check:docs`.
## Verification record
Readings are at branch head `ed59283ff` unless stated. The branch merged
`origin/main` `6662e3f59` (after #23080 landed as `a22e67b3c`) through
`scripts/pm/os-regen-merge.sh`, and the merge commit is `f7f23a07d`. All
heavy runs went through `scripts/pm/os-verify-lock.sh`.
- `pnpm --filter @objectstack/spec build`: exit 0. `check:generated`:
"All 14 generated artifacts are up to date", exit 0.
- `pnpm --filter @objectstack/spec typecheck` and `pnpm --filter
@objectstack/lint typecheck`: exit 0.
- `@objectstack/spec` full suite (`vitest run --project local`): **658
files, 19919 passed, 1 todo**, exit 0. The new
`src/ui/url-target-doors.test.ts` holds 25 tests.
- `@objectstack/lint` full suite at `beade18b6`, which differs from head
only by the regenerated `component.mdx`: **138 files, 6590 passed, 5
skipped**. The new `src/validate-component-props-url-target.test.ts`
holds 8 tests.
- `@objectstack/platform-objects` full suite: 70 files, 1096 passed.
- **Narrowed, declared.** These runs cover only the test files that
author an affected key:
- `@objectstack/runtime`, 8 files, 87 passed:
`app-plugin-converted-collections`, `app-plugin-converted-readers`,
`dispatcher-plugin.routes`, `http-dispatcher.actions-doubled-redirect`
(which writes `onSuccess.navigate`), `notification-schema-conformance`,
`packages-unscoped-environment-binding`,
`standalone-stack-option-b-forward-conversion` and
`standalone-stack-seeder-declaration-copy`;
- `@objectstack/metadata-protocol`
`protocol.meta-types-unauthorable-columns`, 8 passed.
- **The population:** every test file outside spec and lint that names
`actionUrl`, `onSuccess`, `navigate` or an `action:button` /
`action:icon` / `action:group` / `action:menu` row. That is 22 files.
Most of them are notify-node `actionUrl` or React `onSuccess` callbacks,
which are different keys.
- **The static census behind it.** Every `actionUrl:`, `navigate:` and
url-adjacent `target:` literal in this repository, tests included, is
either admitted or not on a url route. The exceptions are this PR's own
pins and three raw-object fixtures in
`validate-dashboard-action-refs.test.ts`, which the spec never parses
(see the Acceptance notes).
- **Blind spot:** a test that builds one of these values from a computed
string.
- `turbo ls --affected` names 76 packages. That set comes from the
package dependency graph, so it is blind to tests that read another
package's source from disk. CI runs the full farm, and the
`@objectstack/cli` `integration` tier and the example suites are
declared to CI.
- **Ablations** (one-time proofs, not kept, each through
`scripts/ablation-replace.mjs` with its restore trap):
- Dashboard door at `2be8c3b15`: deleted
`.superRefine(refuseUnopenableHeaderActionUrl)` (anchor 1 → 0, blob
`517bda8662ed` → `1a0794811edb`). `url-target-doors.test.ts` read **4
failed / 9 passed**: both refusals and both reach pins.
- `onSuccess` door at `2be8c3b15`: deleted
`.superRefine(refuseUnopenableOnSuccessNavigate)` (anchor 1 → 0, blob
`05f3a2c73867` → `984773e97456`). It read **3 failed / 10 passed**.
- Page action rows at `ed59283ff`: replaced the factory's guard with an
always-return carrying the marker `ablated-23077` (blob `3308f803bfee` →
`3c5a701368c4`).
- src leg: **4 failed / 21 passed**, the four row refusals.
- dist leg: after `pnpm --filter @objectstack/spec build`,
`ablation-dist-preflight.mjs` found the marker in 14 built files. The
props-gate test then read **4 failed / 4 passed**.
- restore leg: rebuilt; the marker is absent from all 232 built files;
the tree is clean; the props-gate test read 8/8 passed.
- Every restore put the blob back equal to HEAD with an empty `git diff
HEAD`.
- **Gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (no paths) derived 112 commands at
`ed59283ff`. **111 run, all exit 0. `pnpm check:type-check-debt` is NOT
MEASURED by dispatch order.** Two of them first exited 3 on an unmet
prerequisite: `check:skill-examples` (`@objectstack/client-react`
unbuilt) and `check:dual-build-cjs-loads` (37 unbuilt packages). Both
exit 0 after a workspace build through the lock, at the same head.
`--ran` reconciles: "112 derived, 111 run, 1 NOT-MEASURED, 0 UNRUN", and
every line carries its exit code. `check:nul-bytes` is green, and a raw
control-byte scan of the 13 touched files finds 0 lines.
- `check-adr-0087-registration`: "registered
ui-page-action-row-url-target-scheme-set-closed,
ui-dashboard-header-action-url-scheme-set-closed,
ui-action-on-success-navigate-scheme-set-closed (new here: …)".
- **Producers measured: none refused outside tests.** This repository
was read at its head; objectui at its pin `7578323` and at `origin/main`
`7af0ab37`.
- The only refused literals are in tests that exercise the runner's own
refusal: objectui `ActionRunner.test.ts`,
`ActionRunner.onSuccessNavigation.test.ts` and
`element-button-mailto-12191.test.tsx`.
- The other refused-looking hits are `_blank` / `_top` nav attributes
and non-url executor names.
- No producer writes `actionType: 'navigation'` on a page action row.
- Size: 13 files, **+559 / −21 = 580 changed lines** (generated files
included).
- `origin/main` has moved two commits past the merge (`1badd3183`,
`2d21b9c89`). Their file set is disjoint from this branch's, and CI's
merge-ref run covers the join.
- Merge round (seat order, state note 6133884874): merged origin/main
4a87d09 (#23098 = 55828c5) at e6d8238; the one conflict,
packages/spec/dropped-refinements.baseline.json, took main side whole,
and build-schemas re-derived this branch's 21 sites (e9ff53c): 232
schemas / 752 sites, #23098's 18 sites kept. At e9ff53c: spec suite
660 files / 20010 passed / 1 todo; lint suite 138 files / 6617 passed /
5 skipped; dispatch-gates --ran 112 derived, 111 run, 1 NOT-MEASURED
(check:type-check-debt, by order), 0 UNRUN.
## Acceptance notes
- **objectui's dashboard header click handler bypasses the classifier**
(read at objectui `7af0ab37`,
`packages/plugin-dashboard/src/DashboardRenderer.tsx:1211-1223`). An
`actionType: 'url'` header action is navigated by the renderer itself,
not through `ActionRunner` / `classifyNavigationTarget`:
- it calls `location.assign` for `^https?://` **or a `//`-prefixed**
target;
- it calls `history.pushState` for everything else, which throws on
`mailto:` and admits a bare `thanks`;
- an omitted `actionType` is skipped with a warning, while the same
file's `headerActionDefs` defaults it to `'url'`.
This is a second URL grammar on the runtime side of ruling A+. Reported
to the seat with evidence in the dev report; it is not fixed here
(objectui).
- **A page action row's `onSuccess` is `z.unknown()`**, forwarded to the
runner's `navigateOnSuccess`. Measured through the dist:
`ActionButtonPropsSchema` and an `action:group` member accept
`onSuccess: { navigate: 'javascript:alert(1)' }`, and the props gate
answers 0 findings. As a control, `ActionSchema` refuses the same value
at `onSuccess.navigate`. This is the same family, but declaring the
rows' `onSuccess` shape is a contract choice and not a mechanical fix,
so it is reported to the seat and not fixed here.
- `actionType: 'navigation'` on a page action row reaches the runner's
`executeNavigation`, an alias of `url`, and its `target` is not judged
here. The row's `actionType` is a free string, and no producer writes
it. This is a residual of the family; the card and the claim scope the
rows to `'url'`.
- Three fixtures in
`packages/lint/src/validate-dashboard-action-refs.test.ts`
(`//cdn.example.com/y` and `some-handler` with `actionType: 'url'`, and
`do_something` with it omitted) feed the gate raw objects. They stay
green, but they now pin inputs that the stack parse refuses before the
gate sees them.
- `action.form.ts`'s help text for `onSuccess.navigate` still says "A
relative value is an in-app route". The `describe` text now states the
set.
- The props gate's generic `component-props-invalid` text ("nothing
refuses it today, so the renderer receives the props as written") reads
loosely for these rows, because the runner does refuse at the click.
---
_Generated by [Claude
Code](https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 0d43004 commit f931e76
13 files changed
Lines changed: 559 additions & 21 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 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
161 | 161 | | |
162 | 162 | | |
163 | 163 | | |
164 | | - | |
| 164 | + | |
165 | 165 | | |
166 | 166 | | |
167 | 167 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
41 | | - | |
| 41 | + | |
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
| |||
89 | 89 | | |
90 | 90 | | |
91 | 91 | | |
92 | | - | |
| 92 | + | |
93 | 93 | | |
94 | 94 | | |
95 | 95 | | |
| |||
125 | 125 | | |
126 | 126 | | |
127 | 127 | | |
128 | | - | |
| 128 | + | |
129 | 129 | | |
130 | 130 | | |
131 | 131 | | |
| |||
172 | 172 | | |
173 | 173 | | |
174 | 174 | | |
175 | | - | |
| 175 | + | |
176 | 176 | | |
177 | 177 | | |
178 | 178 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
139 | 139 | | |
140 | 140 | | |
141 | 141 | | |
142 | | - | |
| 142 | + | |
143 | 143 | | |
144 | 144 | | |
145 | 145 | | |
| |||
155 | 155 | | |
156 | 156 | | |
157 | 157 | | |
158 | | - | |
| 158 | + | |
159 | 159 | | |
160 | 160 | | |
161 | 161 | | |
| |||
| 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 | + | |
0 commit comments