Repository navigation
feat(spec)!: a page action row's url target is judged on the executor the runner runs, its name leg included - #23289
Conversation
… the runner runs, its name leg included The console's action runner dispatches on `type || actionType || name`, so a row with no executor key whose `name` is `url` or `navigation` runs the url navigator. `effectiveRowExecutor` states that resolution once, and the rows' url-target refinement judges `target` whenever it resolves to the url navigator, whichever key spelled it. `name` stays open for every other handler. Claude-Session: https://claude.ai/code/session_01BRQ9Mp1yH7R1az6MaUM18H Co-authored-by: Claude <noreply@anthropic.com>
…ated component reference Claude-Session: https://claude.ai/code/session_01BRQ9Mp1yH7R1az6MaUM18H Co-authored-by: Claude <noreply@anthropic.com>
…w-effective-executor
📓 Docs Drift CheckThis PR changes 1 package(s): 2 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 — 141 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 a90495f0e51647a7e7b470c7ba7ebe347cb0d870 && git checkout a90495f0e51647a7e7b470c7ba7ebe347cb0d870
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 99e944daf26ac1d2d50e90c5b631349ab45f664f f1a31cee757da6e3f2de04b9968cebc5ca110ac7 && git checkout -B drift-repro 99e944daf26ac1d2d50e90c5b631349ab45f664f && git merge --no-ff f1a31cee757da6e3f2de04b9968cebc5ca110ac7
node scripts/docs-audit/affected-docs.mjs --json 99e944daf26ac1d2d50e90c5b631349ab45f664f
|
Contract reviewIsolated contract-review subagent of Served-tier: Shape. Draft PR against Check-runs on the head, at this record's clock. ① Derived judgmentsEvery accept-set and public-surface change the net diff implies, judged against objectui's runner at the pin.
② Semver level
③ Boundary flagsReport
Implemented-by: VERDICT: PASS Generated by Claude Code |
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
6139317450ruled. A page action row'stargetis now judged against the executor the console's action runner actually runs. One grammar,navigation-target.ts'snavigationTargetRefusal, 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 "onSuccessonly besideapi/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 withonSuccessbeside a non-api/ non-scriptexecutor. 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 itsnamewhen that key is omitted or empty. It uses||, as the runner writes it, not the??the triage text transcribes. An emptyactionTypefalls through tonameat the click ('' || 'url'is'url'), so it falls through here too. The resolution-order pins hold this.ROW_URL_NAVIGATOR_EXECUTORSis{ 'url', 'navigation' }, the twobuiltinExecutorsrows that reachnavigateTo.refuseUnopenableRowUrlTargetjudgestargetwhenever the effective executor is one of those two, whichever key spelled it. This applies onaction:button, onaction:icon, and on everyaction:group/action:menumember. The refusal isnavigationTargetRefusal'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 navigatoron the name leg, which tells the author that declaring the executor key is how anamestops choosing the navigator (the same teaching feat(spec)!: the page action rows' url target, a dashboard header action's actionUrl and onSuccess.navigate apply the shared url-target refusal #23101's dashboard subject uses for an omittedactionType);`actionType: 'navigation'`: that row is still refused at the key by feat(spec)!: a page action row's onSuccess is ActionSchema's own block, and its executor key refuses the 'navigation' alias #23164's alias rule, and itstargetis now judged too, so the author learns both at once.nameis not refused as an executor spelling. A row whosenamenames any other handler (navigate_edit,convert_lead,flow, …) is untouched, and a declared executor key still outranks thename.nameandtargetdescribes on the three shapes state the name leg.component.mdxis regenerated.ui-page-action-row-name-leg-url-target-judgedand the changeset.changeset/23168-page-action-row-name-leg-url-target.md:@objectstack/specminor, BREAKING (accept-set narrowing, launch-window convention),Clause-②: no (narrowing), ADR-0087registered, with a FROM → TO table and the one-line fix.os validate's props gate (@objectstack/lintvalidateComponentProps) parsespropertiesagainst these same rows, so it reports the refusal atproperties.target/properties.actions.N.targetwith no lint code change.validate-component-props-url-target.test.tsgains the name-leg readings.validate-component-props.tsis untouched, which keeps clear of PR feat(lint)!: every component-props finding is an error at os validate, os build and os lint #23244's severity rework there.The
onSuccesshalf: the fork the dispatch's assumption 4 namesPosition 2's premise holds. objectui's runner navigates twice for a navigating executor that also declares
onSuccess.navigate. Read at the.objectui-shapin1fb1ced36243, byte-identical at objectuimain866763bd:urlrow runsexecuteUrl→navigateTo(ActionRunner.ts:1934;navigationreaches the same navigator throughexecuteNavigation,:2227);executethen callshandlePostExecution(: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
onSuccessbeside every effective executor other thanapi/script. The census found two producers that write exactly that:packages/components/src/renderers/action/__tests__/action-button-icon-inputs-11168.test.tsx:474-490writesactionType: 'run', the recording host handlernode()sets at:176-183withhandlers: { run }. Beside it sitsonSuccess: { navigate: '/app/records/${result.id}' }, with and withoutopenIn: 'newTab', on bothaction:buttonandaction:icon.runanswers{ 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.packages/types/src/__tests__/held-public-block-arms-10872.test.ts:224asserts that the installed spec'saction:button/action:iconrow acceptsproperties: { onSuccess: { navigate: '/tasks' } }, a row with no executor key and noname. It is the remedy target of that file's flat-onSuccessrefusal.'', no handler or built-in matches, andexecuteActionSchemaanswers "not executable" (:2118-2123). The result is unsuccessful, so the hop never runs.mainhere: the Console Pin Gate builds the console and runs no objectui tests.The rest of the census found zero rows with
onSuccessbeside a non-api/ non-scriptexecutor:examples/**,packages/cliandpackages/qa(dogfood); its only rowonSuccessproducers are spec and lint tests, all besideapi;0f84c9c.The dispatch says: ⛔ do not silently narrow to the navigating subset, and do not refuse the producer. So no
onSuccessrule 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)
ActionRunner.executereadsaction.type || action.actionType || action.name || ''at:1265, at the pin1fb1ced36243and unchanged at objectuimain866763bd.type:action-button.tsx:286type: schema.actionType,action-icon.tsx:167,action-group.tsx:350type: action.type,action-menu.tsx:274.||, not??, so the helper uses||.defaultProps(actionType: 'script') are read only by the designer palette (PageDesigner.tsx:216), never at render, so an omitted key stays omitted.navigateTois reached by exactlyurlandnavigation. No host registers a handler under either name:ConsoleShell.tsx:97anduseConsoleActionRuntime.tsx:803registerapi/flow/script/modal, andAppContent.tsx/useObjectActions.tsregistercrud_success,dialog_cancel,navigate_create,navigate_edit,create,delete,navigateandrefresh.action.navigatefallback is a key the strict rows refuse.4f5679c26,actionType: 'navigation'was refused atactionType(feat(spec)!: a page action row's onSuccess is ActionSchema's own block, and its executor key refuses the 'navigation' alias #23164), andname: 'navigation'/name: 'url'with no executor key parsed clean with ajavascript:target, on all four rows.actionType: ''withname: 'url'parsed clean too. The pins assert what the runner does for each spelling.validateComponentPropsparsespropertiesagainstComponentPropsMap(validate-component-props.ts:372) and files every non-unrecognized_keysissue ascomponent-props-invalidat its path. The lint pins read the new refusal there, from the rebuiltdist/.navigationTargetRefusal's, with feat(spec)!: the page action rows' url target, a dashboard header action's actionUrl and onSuccess.navigate apply the shared url-target refusal #23101's subject form. No new phrasing.FROM → TO
action:button/action:iconwith no (or an empty)actionTypeandname: 'url'or'navigation'targettargetjudged by the url scheme set, refused atproperties.targetaction:group/action:menumember with no (or an empty)typeandname: 'url'or'navigation'targettargetjudged by the url scheme set, refused atproperties.actions.N.targetactionType: 'navigation'/ membertype: 'navigation'with a refusedtargettargetactionType: 'url'/ membertype: 'url'Producers: none. No page action row in this repository, in objectui (pin
1fb1ced36243,main866763bd) or in hotcrm (0f84c9c) is namedurlornavigation.Verification record
Readings are at
f1a31cee7unless stated. That head is this branch merged withorigin/main6d7f4df3cthroughscripts/pm/os-regen-merge.sh, with no conflicts. Every heavy run went throughscripts/pm/os-verify-lock.sh, and each result is itsVERDICT command-exitline.Builds:
@objectstack/specbuild: exit 0.check:generated: "All 14 generated artifacts are up to date", exit 0. Beforegen:docsit named onlycontent/docs/references/**.'@objectstack/lint^...') and platform-objects' closure: exit 0.turbo run build --filter=!@objectstack/docs --concurrency=2): 74 / 74.Typecheck:
@objectstack/spec typecheckexit 0 (atb76c26a99), and@objectstack/lint typecheckexit 0.Tests:
@objectstack/spec, full (vitest run --project local), atb76c26a99: 668 files, 20254 passed, 1 todo, exit 0. The merge since brought nothing intopackages/spec.component-action-row-effective-executor.test.tsholds 44 tests. Withurl-target-doors.test.ts,component-action-row-on-success-executor.test.tsandcomponent-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.tsholds 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), plusplatform-objectsin 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, throughscripts/ablation-replace.mjswith its restore trap.effectiveRowExecutorlost its name leg (the base behaviour), markedablated-23168-name-leg.url-target-doors.test.tsstayed green.ablation-dist-preflightfound the marker in 14 dist files. The lint file then read 4 failed / 16 passed: the four name-leg refusals at the gate.a3d90ed32459) with an emptygit diff HEAD. A spec rebuild followed, and--absentfound the marker in none of the 232 dist files, with the tree clean.Gates:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 111 atf1a31cee7. Every exit was recorded before any pipe.packages/spec/distwhile a closure build of mine was rewriting it ("there is none to read"). Three needed the workspace build: speccheck:skill-examples,check:dual-build-cjs-loadsandcheck:lean-entry-closure. All eleven exit 0 on the rerun at the same head.check:type-check-debtis NOT MEASURED (Acceptance notes).--ranreconcile reads "111 derived, 110 run, 1 NOT-MEASURED, 0 UNRUN", exit 0.check-adr-0087-registrationregistersui-page-action-row-name-leg-url-target-judged(new here).check:spec-changesreads 126 converted / 440 migrated.check:objectui-pin-citations: 55 asserting citations match1fb1ced36.check:nul-bytesis 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
.mdandcomponent.mdxresolve to no config (--print-configanswersundefined).--format jsonreads 4 files, 0 errors and 0 warnings. Invariance: this config enables no type-aware linting (noparserOptions.project; the config says so itself in its comment ateslint.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
name: 'url'(or'navigation') reaches objectui's url navigator through the runner's name leg, so itstargetis judged only at the click — the fifth door of ruling A+'s url-target family #23168 stays open for theonSuccesshalf. The merge of this PR does not close it (Part of).check:type-check-debtis NOT MEASURED here. Its--re-measuremode re-runs tsc over every DEBT / TEST_DEBT ledger entry repo-wide. This diff changes no tsconfig, notypecheckscript and no ledger entry, and CI runs it.b76c26a99, before the merge ofmain. The merge brought6d7f4df3cand two earlier commits, which touch service-mailbox, create-objectstack,scripts/pm/ci-failure.mjs, a workflow, two changesets and the lockfile, and nothing inpackages/spec. The row pins and the lint suite were re-read at the merged head.Generated by Claude Code