Repository navigation
feat(spec)!: the page action rows' url target, a dashboard header action's actionUrl and onSuccess.navigate apply the shared url-target refusal - #23101
Conversation
…the shared url-target refusal
The two doors judge their value with navigation-target.ts's
navigationTargetRefusal, the one grammar the console's action runner
applies at the click. A header action's actionUrl is judged when
actionType is 'url' or omitted; onSuccess.navigate on every value, a
template that opens with ${ left to the hop.
Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf Co-authored-by: Claude <noreply@anthropic.com>
…efinements ledger Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf Co-authored-by: Claude <noreply@anthropic.com>
…target refusal action:button / action:icon (actionType 'url') and an action:group / action:menu member (type 'url') judge target with navigation-target.ts's navigationTargetRefusal; os validate's props gate reports it at the row, pinned in @objectstack/lint. Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf Co-authored-by: Claude <noreply@anthropic.com>
… in the dropped-refinements ledger Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 140 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d8ac777fba670805e353cbffaffe2458fcbbc2b5 && git checkout d8ac777fba670805e353cbffaffe2458fcbbc2b5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 21c86444ec6e1afb3f3d6e608443fcaae6a10f96 e9ff53c82db9703a18425a4fb2e36fb630ee4a7b && git checkout -B drift-repro 21c86444ec6e1afb3f3d6e608443fcaae6a10f96 && git merge --no-ff e9ff53c82db9703a18425a4fb2e36fb630ee4a7b
node scripts/docs-audit/affected-docs.mjs --json 21c86444ec6e1afb3f3d6e608443fcaae6a10f96
|
Contract reviewServed-tier: Isolated contract-review subagent of ① Derived judgmentsEvery accept-set and public-surface change the diff implies, each judged:
② Semver level
③ Boundary flags
File surface beyond the claim's literal list: the baseline ledger (hand-edited by design, no Implemented-by: VERDICT: PASS Generated by Claude Code |
…l-target-doors packages/spec/dropped-refinements.baseline.json conflicted (both sides add sites). The merge takes main's side whole; the next commit re-derives this branch's sites from build-schemas' own printed entries on the merged tree. Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf Co-authored-by: Claude <noreply@anthropic.com>
…onent reference The merge took main's ledger side whole (228 schemas / 731 sites, #23098's 18 emptyState.link.url sites among them). build-schemas on the merged tree then printed this branch's 21 sites as undeclared or moved: 4 new entries and 17 sites in 13 existing entries. They are applied exactly as printed, and the header totals now match the body: 232 schemas / 752 sites. gen:docs regenerates ui/component.mdx from the merged tree. Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Delta record on the merged head, rendered 2026-10-13T01:55Z by an isolated contract-review subagent of ① Derived judgmentsThe question for a delta record is whether the merged head changes any accept set or public surface relative to what
② Semver levelUnchanged. The delta changes nothing that is published: the changeset ③ Boundary flagsThe merge-round report
File surface and governance are as before: no governed path among the 13 files (no Implemented-by: VERDICT: PASS Generated by Claude Code |
…rom the merged tree origin/main (287fd6e) brought PR #23101's component rows; the branch's retirement removes the view-level api arm from the same page. Regenerated with gen:docs on the merged tree per scripts/pm/os-regen-merge.sh step 4. Claude-Session: https://claude.ai/code/session_01BRQ9Mp1yH7R1az6MaUM18H Co-authored-by: Claude <noreply@anthropic.com>
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, asnavigationTargetRefusalinpackages/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
packages/spec/src/ui/component.zod.ts). One refinement factory,refuseUnopenableRowUrlTarget, is applied toActionButtonPropsSchemaandActionIconPropsSchema(whenactionType: 'url') and to theaction:groupandaction:menumembers (whentype: 'url'). It judgestargetand refuses attarget. On every other executortargetnames a script, flow, page, endpoint or form, so it is not judged, and a row with notargetis not judged either.os validate's props gate (@objectstack/lintvalidateComponentProps) parsespropertiesagainst these sameComponentPropsMaprows, so it now reports the refusal at the row'stargetpath, with the parse's own first sentence. The refusal is acomponent-props-invalidfinding, 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.packages/spec/src/ui/dashboard.zod.ts).DashboardHeaderActionSchemajudgesactionUrlwhenactionTypeis'url'or omitted, and refuses atactionUrl. Onflow/script/modal/api/form,actionUrlis untouched. The refusal's subject names both spellings of the url route ("a url target whenactionTypeis'url'or omitted"), so an author who meant a flow learns that declaringactionTypeis the way out.packages/spec/src/ui/action.zod.ts). TheonSuccessblock judgesnavigateon every value and refuses atonSuccess.navigate.${result.redirectUrl}stays accepted and/apps/crm/tasks/${result.id}stays accepted.tasks/${result.id}is refused, because the runner refusestasks/42after interpolation too.The
describetext 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-closedandui-action-on-success-navigate-scheme-set-closed. There is one changeset,.changeset/23077-url-target-doors-scheme-set.md:@objectstack/specminor, BREAKING, with a FROM → TO table and the one-line fix,Clause-②: no (narrowing), and ADR-0087registeredwith the three ids.Empty values. At the three doors that have no target-required rule (dashboard
actionUrl,onSuccess.navigate, a row'starget), 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 emptyactionUrl,readOnSuccessNavigationdrops an emptynavigate, andexecuteUrlfails with "No URL provided". The changeset's FROM → TO table names this case.The dispatch's mechanism assumptions, as measured
0aef78881.dashboard.zod.ts:109actionUrl: z.string()and the threecomponent.zod.tsrows at:3834/:3931/:4114matched.onSuccess.navigatewas ataction.zod.ts:1785, not:941.:941is the top-levelnavigateguidance key, a different thing.ObjectStackDefinitionSchema.safeParse/defineStackreach pins for the dashboard andonSuccessdoors;actionType). The contract states no default foractionType. Its two readers that assign one both assign'url': objectuiDashboardRendererbuilds the headerActionDefwithtype: (a.actionType) || 'url', and this repo'svalidateDashboardActionRefsusesstrName(action.actionType) ?? 'url'. Pinned reading: an omittedactionTypeis the url route and is judged. The renderer's click handler, read at objectuiorigin/main7af0ab37and at the pin7578323, returns early with a console warning on an omittedactionType. 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.${). Held:${result.redirectUrl}is accepted atonSuccess.navigate, and that is pinned.File surface beyond the claim's literal list (declared)
packages/spec/dropped-refinements.baseline.json.build-schemas.tsrefuses 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). Themeasuredheader totals are updated to match its body (231 schemas / 734 sites), whichscripts/dropped-refinements.test.tspins. No site is removed.check-adr-0087-registrationneeds them: a changeset with a FROM → TO prescription cannot claimno-migration-prescription.content/docs/references/ui/{action,dashboard,component}.mdx, regenerated bygen:docs.check:generatednamed onlycheck:docs.Verification record
Readings are at branch head
ed59283ffunless stated. The branch mergedorigin/main6662e3f59(after #23080 landed asa22e67b3c) throughscripts/pm/os-regen-merge.sh, and the merge commit isf7f23a07d. All heavy runs went throughscripts/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 typecheckandpnpm --filter @objectstack/lint typecheck: exit 0.@objectstack/specfull suite (vitest run --project local): 658 files, 19919 passed, 1 todo, exit 0. The newsrc/ui/url-target-doors.test.tsholds 25 tests.@objectstack/lintfull suite atbeade18b6, which differs from head only by the regeneratedcomponent.mdx: 138 files, 6590 passed, 5 skipped. The newsrc/validate-component-props-url-target.test.tsholds 8 tests.@objectstack/platform-objectsfull suite: 70 files, 1096 passed.@objectstack/runtime, 8 files, 87 passed:app-plugin-converted-collections,app-plugin-converted-readers,dispatcher-plugin.routes,http-dispatcher.actions-doubled-redirect(which writesonSuccess.navigate),notification-schema-conformance,packages-unscoped-environment-binding,standalone-stack-option-b-forward-conversionandstandalone-stack-seeder-declaration-copy;@objectstack/metadata-protocolprotocol.meta-types-unauthorable-columns, 8 passed.actionUrl,onSuccess,navigateor anaction:button/action:icon/action:group/action:menurow. That is 22 files. Most of them are notify-nodeactionUrlor ReactonSuccesscallbacks, which are different keys.actionUrl:,navigate:and url-adjacenttarget: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 invalidate-dashboard-action-refs.test.ts, which the spec never parses (see the Acceptance notes).turbo ls --affectednames 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/cliintegrationtier and the example suites are declared to CI.scripts/ablation-replace.mjswith its restore trap):2be8c3b15: deleted.superRefine(refuseUnopenableHeaderActionUrl)(anchor 1 → 0, blob517bda8662ed→1a0794811edb).url-target-doors.test.tsread 4 failed / 9 passed: both refusals and both reach pins.onSuccessdoor at2be8c3b15: deleted.superRefine(refuseUnopenableOnSuccessNavigate)(anchor 1 → 0, blob05f3a2c73867→984773e97456). It read 3 failed / 10 passed.ed59283ff: replaced the factory's guard with an always-return carrying the markerablated-23077(blob3308f803bfee→3c5a701368c4).pnpm --filter @objectstack/spec build,ablation-dist-preflight.mjsfound the marker in 14 built files. The props-gate test then read 4 failed / 4 passed.git diff HEAD.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) derived 112 commands ated59283ff. 111 run, all exit 0.pnpm check:type-check-debtis NOT MEASURED by dispatch order. Two of them first exited 3 on an unmet prerequisite:check:skill-examples(@objectstack/client-reactunbuilt) andcheck:dual-build-cjs-loads(37 unbuilt packages). Both exit 0 after a workspace build through the lock, at the same head.--ranreconciles: "112 derived, 111 run, 1 NOT-MEASURED, 0 UNRUN", and every line carries its exit code.check:nul-bytesis 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: …)".7578323and atorigin/main7af0ab37.ActionRunner.test.ts,ActionRunner.onSuccessNavigation.test.tsandelement-button-mailto-12191.test.tsx._blank/_topnav attributes and non-url executor names.actionType: 'navigation'on a page action row.origin/mainhas 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.Acceptance notes
objectui's dashboard header click handler bypasses the classifier (read at objectui
7af0ab37,packages/plugin-dashboard/src/DashboardRenderer.tsx:1211-1223). AnactionType: 'url'header action is navigated by the renderer itself, not throughActionRunner/classifyNavigationTarget:location.assignfor^https?://or a//-prefixed target;history.pushStatefor everything else, which throws onmailto:and admits a barethanks;actionTypeis skipped with a warning, while the same file'sheaderActionDefsdefaults 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
onSuccessisz.unknown(), forwarded to the runner'snavigateOnSuccess. Measured through the dist:ActionButtonPropsSchemaand anaction:groupmember acceptonSuccess: { navigate: 'javascript:alert(1)' }, and the props gate answers 0 findings. As a control,ActionSchemarefuses the same value atonSuccess.navigate. This is the same family, but declaring the rows'onSuccessshape 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'sexecuteNavigation, an alias ofurl, and itstargetis not judged here. The row'sactionTypeis 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/yandsome-handlerwithactionType: 'url', anddo_somethingwith 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 foronSuccess.navigatestill says "A relative value is an in-app route". Thedescribetext now states the set.The props gate's generic
component-props-invalidtext ("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