Skip to content

fix(lint): dashboard-action-route-unresolved resolves the apps/NAME head and every later segment - #16425

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-16169-dashboard-route-app-segment
Sep 7, 2026
Merged

fix(lint): dashboard-action-route-unresolved resolves the apps/NAME head and every later segment#16425
baozhoutao merged 1 commit into
mainfrom
claude/issue-16169-dashboard-route-app-segment

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #16169

What

URL_COLLECTION_TO_STACK_KEY in packages/lint/src/validate-dashboard-action-refs.ts had no apps entry, so a dashboard header action's url target like /apps/no_such_app_nope/crm_lead was never checked at all — a button pointing at an app that does not exist passed dashboard-action-route-unresolved clean. The resolver loop also returned at the first recognized collection segment, resolved or not, so a bad app name combined with a bad later segment (e.g. a bad dashboard name) reported only the later one, never the app.

This PR:

  • Adds apps'apps' to the collection table (both app/apps spellings, matching the existing singular/plural convention), with known.apps built from stack.apps keyed by .name — the same identity every other rule in this package resolves apps by, and the same one the runtime's /apps/:appName route and REST's GET /meta/apps/:name read by.
  • Changes the scan loop to walk every segment of the path and collect every unresolved <collection>/<name> pair instead of returning at the first recognized one — one finding per unresolved segment.

Why

Card #16169: a dashboard button whose url action points at a nonexistent app rendered and, on click, silently did nothing — exactly the false-affordance category this rule exists to catch (ADR-0049), just missed because apps was absent from the lookup table.

Behavior change (called out per the triage boundary in the card)

Scanning every segment instead of stopping at the first recognized one changes behavior on paths where an EARLIER segment resolves and a LATER one does not — e.g. /dashboards/exec/views/bad_view where exec is a real dashboard but bad_view names no view. Before, the loop returned at dashboards/exec (resolved) and never reached views/bad_view, so this path was silent. It now reports one warning on the views/bad_view segment. This is a genuine, visible change to what a clean lint run reports on such paths, not a pure addition — called out as its own bullet in the changeset.

Testing

  • pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-dashboard-action-refs.test.ts — 20 tests pass (4 new: bad-app-alone, two-bad-segments, the boundary case, fully-resolvable app route; 1 existing fixture updated to declare its app since the app segment is now checked).
  • pnpm --filter @objectstack/lint test — 100 files / 3446 tests pass.
  • pnpm --filter @objectstack/lint typecheck, @objectstack/cli typecheck, @objectstack/metadata-protocol typecheck — all pass.
  • @objectstack/cli unit tier (vitest run --project unit) — 181 files / 2453 passed, 6 expected-fail.
  • @objectstack/metadata-protocol test — 166 files / 2414 passed, 10 skipped.
  • Ran validate on all four example apps (showcase, crm, todo, multi-package) — no new dashboard-action-route-unresolved / dashboard-action-target-undefined findings on any of them (clean baseline unaffected).
  • Positive control: temporarily injected a bad /apps/no_such_app_nope/crm_lead header action into the showcase's ops dashboard, confirmed the new warning fires, reverted by blob hash (git checkout HEAD --, verified git diff HEAD empty and blob hash unchanged).
  • Ablation: reverting only the loop-scanning change (keeping the apps table entry) reproduces the reported bug's control case correctly while losing the multi-segment / boundary coverage — read from the red-first captures in this PR's report.
  • Ran every gate node scripts/pm/dispatch-gates.mjs --commands derives for this diff (54 families) — all pass; reconciled with --ran (54 derived, 54 run, 0 not-measured).

Related context (not fixed here): card 3367 (original rule), card 3959, card 4709, card 3611, hotcrm card 1637.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8


Generated by Claude Code

…ead and every later segment

URL_COLLECTION_TO_STACK_KEY had no `apps` entry, so a dashboard header
action's url target like `/apps/no_such_app_nope/crm_lead` was never
checked at all — a button pointing at an app that does not exist passed
lint clean. The loop also returned at the first recognized collection
segment, resolved or not, so a bad app name combined with a bad later
segment (e.g. a bad dashboard name) reported only the later one.

Resolves the apps/NAME head against stack.apps (keyed by name, the same
identity the runtime's /apps/:appName route and REST's GET /meta/apps/:name
read by), and scans every segment of the path instead of stopping at the
first recognized one — one finding per unresolved <collection>/<name> pair.

Behavior change on paths that used to pass clean: a path where an earlier
segment resolves and a later one does not (e.g. /dashboards/exec/views/
bad_view) now reports the later segment instead of staying silent. Called
out as its own changeset bullet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8
@github-actions github-actions Bot added the size/m label Sep 6, 2026
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 7 documentable anchor(s).

8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/declarative-endpoints.mdx (via /apps/:appName (route, a path literal in KnownTargets))
  • content/docs/concepts/north-star.mdx (via /apps/:appName (route, a path literal in KnownTargets))
  • content/docs/getting-started/build-with-claude-code.mdx (via /apps/:appName (route, a path literal in KnownTargets))
  • content/docs/getting-started/quick-reference.mdx (via /apps/:appName (route, a path literal in KnownTargets))
  • content/docs/permissions/administrator-guide.mdx (via /apps/:appName (route, a path literal in KnownTargets))
  • content/docs/protocol/kernel/http-protocol.mdx (via /apps/:appName (route, a path literal in KnownTargets))
  • content/docs/protocol/objectui/actions.mdx (via /apps/:appName (route, a path literal in KnownTargets))
  • content/docs/ui/setup-app.mdx (via /apps/:appName (route, a path literal in KnownTargets))

2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via validateDashboardActionRefs (symbol, a top-level function))
  • content/docs/releases/v17.mdx (via /apps/:appName (route, a path literal in KnownTargets))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 5 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 7ad2ca00232fdd8aaf1d1765c7ed9c8edab695e2packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5140b6acdd9227cb4d832f830963c5a13ddaba6a — the merge of head 93f2dd2f66a33cd2b8659cadd06c29a2d21d9433 into base 7ad2ca00232fdd8aaf1d1765c7ed9c8edab695e2, 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 5140b6acdd9227cb4d832f830963c5a13ddaba6a && git checkout 5140b6acdd9227cb4d832f830963c5a13ddaba6a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 7ad2ca00232fdd8aaf1d1765c7ed9c8edab695e2 93f2dd2f66a33cd2b8659cadd06c29a2d21d9433 && git checkout -B drift-repro 7ad2ca00232fdd8aaf1d1765c7ed9c8edab695e2 && git merge --no-ff 93f2dd2f66a33cd2b8659cadd06c29a2d21d9433

node scripts/docs-audit/affected-docs.mjs --json 7ad2ca00232fdd8aaf1d1765c7ed9c8edab695e2

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 7ad2ca00232fdd8aaf1d1765c7ed9c8edab695e2 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

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 size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

lint: dashboard-action-route-unresolved never validates the apps/NAME segment — a dashboard button pointing at an app that does not exist passes clean

2 participants