fix(lint): dashboard-action-route-unresolved resolves the apps/NAME head and every later segment - #16425
Conversation
…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
📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 5 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 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
|
Fixes #16169
What
URL_COLLECTION_TO_STACK_KEYinpackages/lint/src/validate-dashboard-action-refs.tshad noappsentry, so a dashboard header action'surltarget like/apps/no_such_app_nope/crm_leadwas never checked at all — a button pointing at an app that does not exist passeddashboard-action-route-unresolvedclean. 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:
apps→'apps'to the collection table (bothapp/appsspellings, matching the existing singular/plural convention), withknown.appsbuilt fromstack.appskeyed by.name— the same identity every other rule in this package resolves apps by, and the same one the runtime's/apps/:appNameroute and REST'sGET /meta/apps/:nameread by.<collection>/<name>pair instead of returning at the first recognized one — one finding per unresolved segment.Why
Card #16169: a dashboard button whose
urlaction 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 becauseappswas 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_viewwhereexecis a real dashboard butbad_viewnames no view. Before, the loop returned atdashboards/exec(resolved) and never reachedviews/bad_view, so this path was silent. It now reports one warning on theviews/bad_viewsegment. This is a genuine, visible change to what a cleanlintrun 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/cliunit tier (vitest run --project unit) — 181 files / 2453 passed, 6 expected-fail.@objectstack/metadata-protocol test— 166 files / 2414 passed, 10 skipped.validateon all four example apps (showcase, crm, todo, multi-package) — no newdashboard-action-route-unresolved/dashboard-action-target-undefinedfindings on any of them (clean baseline unaffected)./apps/no_such_app_nope/crm_leadheader action into the showcase's ops dashboard, confirmed the new warning fires, reverted by blob hash (git checkout HEAD --, verifiedgit diff HEADempty and blob hash unchanged).appstable 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.node scripts/pm/dispatch-gates.mjs --commandsderives 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