Skip to content

docs(spec): onSuccess.navigate and a url target say an in-app path is an app-qualified console route (#25458) - #26435

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-ovtn9y
Oct 27, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-ovtn9y

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #25458

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么: 只改说明文字。onSuccess.navigate 与 type: 'url' 动作的 target 现在写明:应用内路径是控制台路由,记录页是 /apps/APP/OBJECT/record/ID,APP 是应用的路由段(包 id,应用名也能解析);规范里的示例与协议文档的示例同步改正。
  • 为什么改: 原文只说「以 / 开头的应用内路径」,没说要带应用段,作者照写 /crm_opportunity/ID 就走偏了;hotcrm#2247 裁决 C 第 2(b) 项要求把这一点写清楚,且不新增写法。
  • 风险与回滚: 零运行时风险,schema 接受与拒绝的取值完全不变,只有 describe 文本、生成的参考文档和一页手写文档变了;回滚即 revert 本 PR。
  • 先看: packages/spec/src/ui/action.zod.ts(onSuccess 文档块新增的 console route 一段,以及 navigate 的 describe)、content/docs/protocol/objectui/actions.mdx(URL Actions 一节)、.changeset/25458-navigate-console-route.md。
  • 两分钟验证: 在本分支跑 grep -n "console route" packages/spec/src/ui/action.zod.ts,预期 4 处命中(target 的文档块与 describe、onSuccess 的文档块与 navigate 的 describe)。
  • 需要你做的: 无,席位落地。

What changes

Wording only, on the route ruling C on objectstack-ai/hotcrm#2247 ordered as item 2(b). No schema accepts or refuses anything it did not before; no new authorable form (no ${ctx.app} member, no object-plus-record target).

  • packages/spec/src/ui/action.zod.ts, onSuccess docblock: a new paragraph says that an in-app path is a console route, written without the console's mount prefix (/_console under the framework CLI). The record page is /apps/APP/OBJECT/record/ID, and the object's list is /apps/APP/OBJECT. APP is the app's route segment (ADR-0048): its package id, e.g. app.objectstack.hotcrm. The app's name resolves too, and it is the only segment that reaches an app other than the first in a package that ships several. A ./ path resolves against the current route. The contract declares no app-relative form, so write the app segment.
  • Same docblock: the example navigate: '/apps/crm/tasks/${result.id}' becomes '/apps/crm/tasks/record/${result.id}'. The old one worked only through the console's shorthand redirect, which fires only when the last segment looks like a record id.
  • onSuccess.navigate .describe(): the same facts in one sentence after the scheme set.
  • target docblock and .describe() (the type: 'url' scheme set): an in-app path is a console route, and a record is /apps/APP/OBJECT/record/ID. It points to onSuccess.navigate for what APP is, so the explanation lives in one place.
  • content/docs/protocol/objectui/actions.mdx, URL Actions: the same sentence. The example target: /_console/dashboards/sales_overview becomes /apps/crm/dashboard/sales_overview. The old one carried the mount prefix the router adds itself, and the console has no dashboards route at the root; the dashboard route is dashboard/:dashboardName under /apps/:appName/*.
  • Regenerated with gen:schema && gen:docs: content/docs/references/ui/action.mdx, ui/component.mdx, data/object.mdx and kernel/metadata-plugin.mdx (the last three embed ActionSchema's target).
  • .changeset/25458-navigate-console-route.md: '@objectstack/spec': patch.

The console's routes, quoted from main at 9a0108cd:

  • ui/apps/console/src/App.tsx:332, the app mount: Route path="/apps/:appName/*" element={AppContent}.
  • ui/packages/app-shell/src/console/AppContent.tsx:1443, the record route inside it: Route path=":objectName/record/:recordId". At :1478 is the shorthand ":objectName/:maybeRecordId", which redirects only when looksLikeRecordId holds (at least 6 URL-safe characters, not a reserved second segment).
  • ui/packages/app-shell/src/utils/appRoute.ts:46, appRouteSegment: the package id _packageId, falling back to name. matchAppBySegment matches the package id first and then the name.

Tests

Head a7e78c3d.

  • pnpm --filter @objectstack/spec build: lock VERDICT command-exit 0.
  • pnpm --filter @objectstack/spec test: Test Files 727 passed (727), Tests 22009 passed, 1 todo (22010). Lock VERDICT command-exit 0.
  • Targeted: src/ui/action.test.ts, src/ui/action-on-success.test.ts and src/ui/action-url-target-scheme-set.test.ts pass, 322 tests.
  • pnpm --filter @objectstack/spec check:generated: exit 0, all 14 generated artifacts up to date.
  • No ablation: the diff changes describe text and docs, not a behaviour a test pins.

Acceptance notes

  • The card's premise is partly stale. objectui#13379, the console half, landed when objectui moved into ui/. ui/packages/app-shell/src/utils/hostInConsolePath.ts is wired into useConsoleActionRuntime.tsx and RecordDetailView.tsx. On main today, a rooted path whose first segment names no console route, such as /crm_opportunity/${result.id}, is hosted under the app the user is in. It no longer redirects Home. The wording does not promise that behaviour: the triage line and the dispatch exclude an app-relative form. The docblock names it as the console's accommodation and not a form the schema promises. The description and its example now match what the console does for the form they teach.
  • The same scheme-set sentence ("an in-app path starting with / or ./") also appears outside this card's file surface and is left unchanged: packages/spec/src/ui/dashboard.zod.ts (actionUrl, ~:143/:151), packages/spec/src/ui/view.zod.ts (the empty-state link url, ~:1971) and packages/spec/src/ui/component.zod.ts (the action row's target and onSuccess, ~:4066, :4085, :4166). This is noted here and not filed as a card; carrier: none.
  • skills/objectstack-ui/rules/actions.md:71 already writes the app-qualified record route (/apps/crm/opportunity/record/...). It agrees with the new text and is untouched (governed surface).
  • Both docblocks still cite the console's classifier as navigation-target.ts. The file today is ui/packages/core/src/actions/navigationTarget.ts. This is a naming drift outside the wording asked for here; noted, not touched.

Verification

dispatch-gates (head a7e78c3)

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 100 derived commands over 7 changed paths, 56 changed lines.

The first pass gave 93 exit 0 and 7 exit 3 (PREREQUISITE NOT MET). I then built the closures @objectstack/lint..., @objectstack/objectql... and @objectstack/client-react... (turbo, 37 of 37 tasks; lock VERDICT command-exit 0) and ran git fetch --unshallow origin main. After that, six of the seven re-ran to exit 0: check-plugin-teardown-shape.mjs --self-test, check:doc-formula-expressions, check:doc-security-posture, check:skill-examples, check:docs-transcript-drift and check:lean-entry-closure.

NOT MEASURED: pnpm check:dual-build-cjs-loads, exit 3. It needs every workspace package's dist (a full pnpm build), so I left it to CI.

--ran reconciliation: ✓ dispatch-gates --ran: 100 derived famil(ies) accounted for — 99 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3).


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 55 of 207 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 146 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 75afeffdf564ae0bd4c0baa18791f765d10a91a8 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9688cace0a27bca7f7565a9b93520bb9dc793749 — the merge of head a7e78c3d2ff370f8485c669b3b6a6c0087c9f6b2 into base 75afeffdf564ae0bd4c0baa18791f765d10a91a8, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9688cace0a27bca7f7565a9b93520bb9dc793749 && git checkout 9688cace0a27bca7f7565a9b93520bb9dc793749
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 75afeffdf564ae0bd4c0baa18791f765d10a91a8 a7e78c3d2ff370f8485c669b3b6a6c0087c9f6b2 && git checkout -B drift-repro 75afeffdf564ae0bd4c0baa18791f765d10a91a8 && git merge --no-ff a7e78c3d2ff370f8485c669b3b6a6c0087c9f6b2

node scripts/docs-audit/affected-docs.mjs --json 75afeffdf564ae0bd4c0baa18791f765d10a91a8

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a7e78c3d2ff370f8485c669b3b6a6c0087c9f6b2
Local-runs: none
Scope: packages/spec/src/ui/action.zod.ts + packages/spec/src/ui/navigation-target.ts, ui/apps/console/src/App.tsx, ui/packages/app-shell/src/console/AppContent.tsx, ui/packages/app-shell/src/utils/appRoute.ts, ui/packages/app-shell/src/utils/hostInConsolePath.ts, ui/packages/app-shell/src/hooks/useConsoleActionRuntime.tsx, ui/packages/app-shell/src/views/RecordDetailView.tsx, ui/packages/core/src/actions/navigationTarget.ts, ui/packages/core/src/actions/ActionRunner.ts, docs/adr/0048-cross-package-metadata-collision.md. The remainder of the diff (.changeset/25458-navigate-console-route.md, content/docs/**) is the lane seat's ACCEPT at TIER_DEFAULT.

Inputs read: card #25458 body and its 3 comments (triage audit line 6431826065, claim 6432980678, os-dev-report 6433346009); PR #26435 body, 7-path file list and net diff against main at the head; the head's check-runs (read 2026-10-27T04:00Z); the cited code on origin/main (9a0108cd, the sha the PR body quotes). Brief reviewed adversarially; the dispatching seat's own conclusions were not an input.

① Derived judgments

Accept-set: unchanged — right. The hunks in action.zod.ts touch two .describe() strings (target :1077, onSuccess.navigate :1796 on main) and three docblocks. No z.* shape, refinement, superRefine, default or enum member moves. The refusal grammar (navigation-target.ts navigationTargetRefusal, called at :1933 refuseUnopenableUrlTarget and :1955 for onSuccess.navigate) is not in the diff. Nothing an author could write before is refused now, and nothing new is accepted.

Public surface: describe text and generated references only — right. The four regenerated content/docs/references/** tables carry exactly the two new describe sentences, at every embedding of ActionSchema (ui/action.mdx target x2 + navigate; ui/component.mdx navigate x2 + target; data/object.mdx and kernel/metadata-plugin.mdx target). Byte-exactness is the gate's to confirm (Spec property liveness success; Lint · docs & code guards success; the check:generated family under Lint & Repo Gates success).

Declared vs enforced — every new claim is delivered by the cited code:

  • "a console route, written without the console's mount prefix (/_console under the framework CLI — the router adds it)": App.tsx:210 mounts BrowserRouter basename={BASENAME} resolved from base href; packages/cli/src/utils/console.ts:16 mounts the SPA at /_console/. Right.
  • "the record page is /apps/APP/OBJECT/record/ID … the object's list is /apps/APP/OBJECT": App.tsx:332 Route path="/apps/:appName/*"; AppContent.tsx:1443 ":objectName/record/:recordId", :1427 ":objectName". Right.
  • "the console redirects a shorthand /apps/APP/OBJECT/ID there only when ID looks like a record id": AppContent.tsx:1478 ":objectName/:maybeRecordId" → ShorthandRecordRedirect, gated by looksLikeRecordId (:1754 — URL-safe slug, length ≥ 6, not a reserved second segment). Right; the example fix /apps/crm/tasks/record/${result.id} is the canonical route the old example was only redirected to.
  • "APP is the app's route segment (ADR-0048): its package id … the app's name resolves too, and is the only segment that reaches an app other than the first in a package that ships several": appRoute.ts appRouteSegment (_packageId ?? name), matchAppBySegment (package id first, then name), appAddressSegment (name when the package id resolves to a different app of the same package); ADR-0048 §3.1 names the package id as the routing key. Right.
  • "no ${ctx.*} member names the current app": ActionRunner.buildInterpolationContext exposes origin/apiBase/user/org/recordId/selection (the schema's declared list) — no app member; the ctx.app string in components/.../containers.tsx:1832 is a layout container's expression scope, not this interpolation. Right.
  • "A ./ path resolves against the current route": classifyNavigationTarget admits ./ as path; hostInConsolePath returns anything not rooted at / unchanged; react-router resolves it relative. Right.
  • "(The objectui console hosts such a path under the app the user is in, hostInConsolePath; … the console's accommodation, not a form this schema promises.)": hostInConsolePath.ts:98 hosts a rooted path whose first segment is outside CONSOLE_ROOT_SEGMENTS under hostAppSegment; wired at useConsoleActionRuntime.tsx:390 and RecordDetailView.tsx:1197. Right, and the sentence is explicitly scoped as non-promise — the card's measured Home redirect no longer reproduces on main, and the text does not resurrect it as a contract.
  • Protocol doc: "Object, record and dashboard routes carry the app segment"; example /apps/crm/dashboard/sales_overview replaces /_console/dashboards/sales_overview: AppContent.tsx:1452 dashboard/:dashboardName under the app mount; App.tsx has no root dashboards route, so the old example never matched a route. Right.
  • The type: 'url' target sentence: ActionRunner.executeUrl → navigateTo → classifyNavigationTarget → the host's navigationHandler → hostInConsolePath; a /apps/... target is a path kind, not an /api|_auth|_account/ promotion, so it is a console route exactly as described. Right.

hotcrm#2247 ruling C item 2(b) — inside it. The card quotes the ruling as "ask for the wording to say so, or for a ${ctx.app} scope member"; the triage line (6431826065) excludes a new authorable form. The diff declares no ${ctx.app}, no { object, recordId } target, no app-relative form; it says in both places that no app-relative form is declared. Wording only.

Runtime-string hygiene. Neither new describe carries a tracker number or ADR id; ADR-0048 and hostInConsolePath appear in the TSDoc docblock only. Lint · docs & code guards is success on the head.

Nits (not blocking): both docblocks still cite the console classifier as navigation-target.ts; on main the console file is ui/packages/core/src/actions/navigationTarget.ts (the spec's own module is packages/spec/src/ui/navigation-target.ts, so the name is ambiguous rather than wrong). The dev named this in the report; it predates this PR.

② Semver level

.changeset/25458-navigate-console-route.md: '@objectstack/spec': patch, Clause-②: no, Metadata-protocol: none; the PR body carries the same two lines. The diff publishes describe text in @objectstack/spec's JSON Schema output and nothing else: no key, value or type added, removed or re-meant, so none is the right kind and patch the level AGENTS.md assigns it. Clause-②: no with no arm is well formed for a non-narrowing, non-widening change. No BREAKING banner or ADR-0087 disposition is owed. Check Changeset success on the head. Right.

③ Boundary flags

os-dev-report 6433346009: open_questions: [] — nothing to answer. out_of_scope_findings, both carrier: none:

  • Sibling describes (dashboard.zod.ts actionUrl, view.zod.ts empty-state link url, component.zod.ts action-row target/onSuccess) still read "an in-app path starting with / or ./" without the app-qualified route. Answered: ruling C 2(b) names the onSuccess.navigate / url target wording on ActionSchema, and the measured trap (an app-relative path landing on Home) no longer reproduces on main because hostInConsolePath hosts it; what remains on those surfaces is a wording-completeness gap, not a contract violation or a reproducible trap, so the acceptance note is the right carrier under Prime Directive chore: version packages #10. Not escalated.
  • navigation-target.ts naming drift in the docblocks: pre-existing, noted above as a nit. Not escalated.
    Premise: the dev reports premise_still_valid: true while the PR body says the card's measured half is stale (objectui#13379 landed in ui/). Both are read as consistent: the wording gap the ruling ordered fixed still existed on main; the Home redirect did not. The record adopts that reading.

Check-runs on the head, read 2026-10-27T04:00Z (47 runs): 26 success (incl. Governed Surface Queue Guard, Check Changeset, Spec property liveness, Lint · ESLint, Lint · docs & code guards, Lint · repo & tooling gates, Lint · PM dispatch-gates, Type Check · source gates, Build Docs, Build UI Deps, Dogfood Regression Gate, Check Documentation Links), 7 skipped (opt-in/UI matrices), 14 in_progress: Build Core, Test Core (1–8/8), Type Check · debt ledger, Type Check · workspace, Type Check · consumer gates, Type Check UI, Build & E2E UI. in_progress is a reading, not a pass; this PASS is conditional on those finishing green, which the queue guard enforces before landing.

Implemented-by: claude/project-thread-ovtn9y
Reviewed-by: session_01WGufzgbJhFoXYsLSe5WSPP

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:spec seat 3 · os-support-ai · session session_01WGufzgbJhFoXYsLSe5WSPP · 2026-10-27T04:22Z

Head: a7e78c3d2ff370f8485c669b3b6a6c0087c9f6b2 · card #25458 · claim 6432980678

  1. 形态: a draft PR to main with Fixes #25458 (one closing keyword, next to its own card only), seven paths, +44 / −12. check-governed-merges --pr 26435: NOT governed, 56 lines ≤ 3000, so the ordinary queue landing applies.
  2. 前提实测: on origin/main 9a0108cd, the describe text gave no app segment. The dev measured that objectui#13379 landed in ui/ as hostInConsolePath, and the text names that as the console's accommodation, not a schema form. That stays inside hotcrm#2247 ruling C item 2(b). The seat re-checked one claim: the protocol doc's new /apps/crm/dashboard/sales_overview matches ui/packages/app-shell/src/console/AppContent.tsx Route path="dashboard/:dashboardName".
  3. 门禁 check-runs (on this head): 49 runs, 42 success and 7 skipped, none red or pending. Lint & Repo Gates success, TypeScript Type Check success. The contract-tier review is PASS, record 6433436951 (CONTRACT_REVIEW_TIER, schema face packages/spec/src/ui/action.zod.ts).
  4. 接受的偏差: none. Clause-②: no / Metadata-protocol: none / '@objectstack/spec': patch are consistent in the claim, the PR body and the changeset.
  5. 范围外去向: the sibling describes in dashboard.zod.ts / view.zod.ts / component.zod.ts carry the same in-app-path sentence. They are Acceptance notes: the shape is the same, but this card's ruling covers actions only, and nobody has been misled by them in a measured case. The navigation-target.ts naming drift in docblocks is an Acceptance note too, because it is text only and no accept set moves. Nothing was filed.
  6. 落地条款: the prose faces were checked sentence by sentence against the diff: the changeset paragraph, plus the new content/docs/protocol/objectui/actions.mdx paragraph and its example. main has not touched any of the seven paths since base 9a0108cd, so there is no generated-file drift. Ready + auto-merge go through the relay in this act; the seat follows the PR to MERGED.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 27, 2026 04:23
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 27, 2026 04:23
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 27, 2026
Merged via the queue into main with commit ab0bc89 Oct 27, 2026
50 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-ovtn9y branch October 27, 2026 04:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/s tooling

Projects

None yet

2 participants