Repository navigation
feat(spec,lint): declare the SDUI deep-link "navigate = run action" as a validatable nav runAction reference (#4848) - #7253
Conversation
… action reference (#4848) Promote the SDUI deep-link 'navigate = run action' (?runAction=<actionName>, cloud#844) from a private cross-repo string convention to a spec-declared contract, per the maintainer's 2026-08-06 ruling on #4848 — spec half first. - ObjectNavItemSchema gains an optional runAction action-name reference; runAction+recordId is parse-rejected (list-surface semantics). - defineStack's cross-reference walk rejects a runAction naming no defined action (size-gated like the neighboring nav checks). - validate-action-name-refs gains a nav runAction arm (error severity, did-you-mean, reference-integrity suite coverage on validate/lint/compile). - Liveness ledger: planned + authorWarn (enforce-or-mark) until the objectui consumer half lands; cloud#1048's pin stays the transitional guard. - Pins: accept resolving references, reject nonexistent names at both layers. - Regenerated authorable-surface/ui.json and docs reference rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01784GYbgz5DuYj4gdemmHiF
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 2 package(s): 106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
…ctions.mdx (#4848) The name-bound-surfaces list in actions.mdx is the one the validate-action-name-refs header cites, and apps.mdx enumerates the object nav target fields — both gain the new surface so the hand-written docs don't drift from the schema on day one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01784GYbgz5DuYj4gdemmHiF
…fact, and eleven drifted rows are reconciled (objectstack-ai#7377) (objectstack-ai#7455) Closes objectstack-ai#7377. Two stages, in order — the reconciliation first, because doing the mechanism first would have made CI red with no path to green. STAGE 1 — per-row reconciliation. The table declares its own counting method (`check-liveness.mts --json`, fixed in objectstack-ai#4488) and says the count columns are never hand-edited. Nobody re-ran the snippet: 9 of 30 rows disagreed with the gate when objectstack-ai#7377 was filed, and PR objectstack-ai#7425 added two more. Every delta is measured and explained rather than absorbed: - field/action/hook/page/seed/webhook — the ADR-0010 protection envelope entered each schema's walked shape as the objectstack-ai#4001 campaign closed it (objectstack-ai#4514/objectstack-ai#4530/objectstack-ai#4531/objectstack-ai#4533/objectstack-ai#4974); the gate auto-classifies those keys `live`. - flow — +1 dead: errorHandling.retryDelayMs, tombstoned by objectstack-ai#4964's rename to `backoffMs`. The Note said "4 tombstone entries"; it says 5. - action — +1 live beyond the envelope: `description` (objectstack-ai#7367 / PR objectstack-ai#7430). - view — objectstack-ai#4001 batch 6e (objectstack-ai#4534) declared three container-level keys the Note never mentioned: `object` live, `name`/`label` dead. The hand-enumerated dead set was 4 against a real 6. - app — dead 9 still reconciles exactly; +1 live `_unpublished` (objectstack-ai#4829 / PR objectstack-ai#6942) and the type's first `planned`, `navigation.runAction` (objectstack-ai#4848 / PR objectstack-ai#7253). - job — zero dead under objectstack-ai#7425. First row in the table where that holds WITH the ADR-0033 exemption still in force; the Note says so. - translation — zero dead under objectstack-ai#7425. Its cell had published `dead 2` beside a sentence naming one key, and that key had already been removed in objectstack-ai#4667: the number was right and the prose false, in the same cell, on the day it was written. Recorded as measured history. The other 24 Notes are byte-identical. Five rows carried a SEVENTH cell against a six-column header, which GFM drops — that prose rendered nowhere, and the 2-column table folds it back into the Notes cell losslessly. STAGE 2 — the mechanism, on objectstack-ai#5107's precedent. The numbers move to `packages/spec/liveness/state-counts.md` (`merge=os-regen`, registered in `scripts/regen-artifacts.mjs` and `.gitattributes`); the Notes prose stays hand-written, because regenerating a Note manufactures a verdict. `gen:liveness-counts` spawns the gate rather than re-implementing its walk, and keeps objectstack-ai#7257's skeleton row. `check:liveness` gains three legs in `readme-table.mts`: artifact freshness by byte comparison, README-vs-artifact row sets in both directions, and a count column reappearing in the README — which the other two cannot see. Reverse verification (predicted, then run against a mutated `--ledger-root` copy): artifact deleted => MISSING, red; `view` live skewed 80->81 => STALE naming that line, red; a count column re-added to the `object` row => red naming the line; the `qa` row deleted => red on both the objectstack-ai#7257 heading and the new row-set heading. Verbatim copy green. Gates: liveness script tests 9 files / 186 tests; check-generated ledger tests 8/8; `check:liveness` green; `check:generated --reconcile-only` green; `git-merge-regen.mjs --self-test` green (12 paths); scripts typecheck clean; check-nul-bytes OK. Claude-Session: https://claude.ai/code/session_01PJn5BD9LY9ZCECPs25oiDb Co-authored-by: Claude <noreply@anthropic.com>
Closes #4848
Ruling executed (quoted verbatim)
Maintainer ruling, 2026-08-06 (comment 5203605940), confirmed dispatchable by triage 2026-08-07:
This PR is the spec half. The concrete schema shape was deferred to implementation by the ruling; the shape chosen and the alternatives considered are below — special-inspection section for the PM.
Premise check
packages/service-tenant/src/pages/welcome.page.ts:304-322documents the${environmentsRoute}?runAction=create_environmentdeep-link contract and names both load-bearing cloud-owned names; cloud#1048's pin test (welcome.page.test.ts) is in place. The objectui consumers (CloudOnboardingNext.tsxconcatenation,EnvironmentListToolbar.tsxuseAutoRunCreate) were verified via the card's citations and cloud#1048's pin (objectui repo not attached to this session).runActionhit in this repo was a runtime action-execution method (runtime/domains/actions.ts, MCP tools, service routes) — no nav/link declaration slot. Not landed already; proceeding was correct.Shape chosen (special inspection)
ObjectNavItemSchema.runAction: z.string().optional()— a bare action-name reference on theobjectnav branch: "after landing on this object's list surface, auto-run this declared action once."Why this shape:
objectnavigation target, so the slot lives on theobjectnav branch — not a new navtype, not a URL annotation.stack.actions+ any object'sactions) — exactly the scope every existing name-bound action surface uses (validate-action-name-refs: rowActions/bulkActions, quick-actions bars, navactionDef.actionName). The consumer also matches by bare literal name today. No new reference grammar is introduced.runAction+recordIdis parse-rejected via the existingobjectNavTargetExclusivityrefinement (a record detail has no list toolbar; the dead combination is unrepresentable, ADR-0053 style). It composes withviewName/filters/ default view.Alternatives considered
<object>.<action>reference grammar (floated in the 2026-08-06 on-hold comment)type: 'deep_link'/ reusetype: 'action'type: 'action'runs an action instead of navigating; the deep link navigates and runs. A tenth nav variant would duplicate the wholeobjectbranch payload for one extra key.paramsbag on the object branch (params: { runAction: … })?runAction=as a wire contract only (no schema slot)validate-action-name-refsscopes itself to "defined anywhere" and names location/affinity checking as a distinct class (zero-false-positive posture, ADR-0072 D1). Same scope note applied here; the consumer's dispatch behavior owns placement.No system-wide-consequence fork was hit: the bare-name shape is the one that avoids committing the repo to a new reference grammar, so no STOP was warranted.
Changes
packages/spec/src/ui/app.zod.tsObjectNavItemSchema.runAction(optional, documented with the #4848 / cloud#844 provenance);objectNavTargetExclusivityrejectsrunAction+recordIdwith a self-prescribing messagepackages/spec/src/stack.zod.tsvalidateCrossReferencesnav walk: collects action names (global + object-embedded), rejects an unresolvablerunAction— size-gated like the neighboring dashboard/page/report checks (a stack declaring no actions may reference a plugin's; lint still reports it)packages/lint/src/validate-action-name-refs.ts{ type: 'object', runAction }arm — error severity, near-miss "did you mean", full defined-actions list in the hint; runs onos validate/lint/compilevia the reference-integrity suite (already a member)packages/spec/liveness/app.jsonrunActionrow:planned+authorWarn(enforce-or-mark, mirroringobject.externalSharingModel's P1) — validation is live, but no shipped shell reads the declared slot yet;authorHinttells authors auto-run still fires only via the transitional URL parampackages/spec/authorable-surface/ui.json,content/docs/references/ui/app.mdxcheck:generatedgreen).changeset/nav-runaction-declared-contract.md@objectstack/specminor,@objectstack/lintminorPins
Honest framing: this is a new slot, so all pins are green-only post-change — the refusal pins are the contract.
stack.test.ts"detect a nav runAction referencing an undefined action"runAction: 'ghost_action'→ throwsdeep-link references action 'ghost_action' (via runAction)stack.test.ts"accept … global or object-embedded action"stack.test.ts"tolerate … no actions at all"app.test.ts"accept runAction composed with the list-surface landings"viewName/filterscompositions acceptedapp.test.ts"rejects runAction combined with recordId"validate-action-name-refs.test.tsnew describe (3 tests)areas[]walk); non-objectnav carryingrunActionignoredGates
@objectstack/spectests: 360 files / 9402 passed@objectstack/linttests: 69 files / 1807 passed (first run's 17 suite failures were a missing@objectstack/formuladist in the fresh worktree — env, not this change; green after building workspace deps)pnpm check:generated: all 11 artifacts up to date (spec built before regen)pnpm check:liveness: green (new row classified, evidence conventions respected)tsc --noEmit+ scripts + test-layer debt gate) and lint greencheck:generatedconfirmsapi-surface//export-origins/unchanged — dual-snapshot rule not triggered)docs/adr/**,content/docs/releases/**: untouchedFollow-ups filed (unassigned, unrouted)
runActionslot — retire the private?runAction=string convention (#4848 follow-up) #7250 — objectui half: shell consumes the declared slot;CloudOnboardingNextstops hand-concatenating; ledger row flips toliverunActionslot; retire cloud#1048's transitional pin (#4848 follow-up) #7251 — cloud half: welcome deep link declared via the slot; cloud#1048's pin stays as the transitional guard until then (per the ruling)🤖 Generated with Claude Code
https://claude.ai/code/session_01784GYbgz5DuYj4gdemmHiF
Generated by Claude Code