From 7054ff7dafb6066f07ddb8e8802b61751f30b15d Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:34:04 +0800 Subject: [PATCH] chore(desktop): add navigation boundary lint rule (MUL-4741 Phase 2 prereq) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tab Coordinator protocol requires that application code never navigates directly (invariant 1: a Router location change without a Coordinator token is a protocol error). Enforce it statically: - renderer app code may not import useNavigate/Navigate from react-router-dom nor call router.navigate; src/platform is exempt - the five known legacy sites (the RFC §8.1 migration checklist, cross-validated: the rule fires on exactly those and nothing else) carry inline eslint-disable directives tagged MUL-4741 — the Phase 2 migration removes them one by one, and this rule holding with zero disables is the machine check that the migration is complete Co-Authored-By: Claude Fable 5 --- apps/desktop/eslint.config.mjs | 35 +++++++++++++++++++ .../src/components/route-error-page.tsx | 1 + .../src/components/workspace-route-layout.tsx | 1 + .../src/renderer/src/hooks/use-tab-history.ts | 2 ++ apps/desktop/src/renderer/src/routes.tsx | 1 + 5 files changed, 40 insertions(+) diff --git a/apps/desktop/eslint.config.mjs b/apps/desktop/eslint.config.mjs index 3690c9dfda..51f4e97ce9 100644 --- a/apps/desktop/eslint.config.mjs +++ b/apps/desktop/eslint.config.mjs @@ -41,4 +41,39 @@ export default [ "no-restricted-syntax": "off", }, }, + // Navigation boundary (MUL-4741): the tab Coordinator must be the only + // navigation initiator — a Router location change without a Coordinator + // token is a protocol error. Application code must not navigate directly; + // it goes through the navigation adapter / Coordinator in src/platform. + // Remaining legacy sites carry an inline eslint-disable tagged MUL-4741; + // the Phase 2 migration removes them one by one, and this rule holding + // with zero disables is the machine check that the migration is complete. + { + files: ["src/renderer/src/**/*.{ts,tsx}"], + ignores: ["src/renderer/src/platform/**", "src/renderer/src/**/*.test.*"], + rules: { + "no-restricted-imports": [ + "error", + { + paths: [ + { + name: "react-router-dom", + importNames: ["useNavigate", "Navigate"], + message: + "Direct navigation from application code breaks the Coordinator protocol (MUL-4741). Use the navigation adapter from src/platform instead.", + }, + ], + }, + ], + "no-restricted-syntax": [ + "error", + { + selector: + "CallExpression[callee.object.name='router'][callee.property.name='navigate']", + message: + "Direct router.navigate from application code breaks the Coordinator protocol (MUL-4741). Route it through the navigation adapter in src/platform.", + }, + ], + }, + }, ]; diff --git a/apps/desktop/src/renderer/src/components/route-error-page.tsx b/apps/desktop/src/renderer/src/components/route-error-page.tsx index ab37076afc..61343b756f 100644 --- a/apps/desktop/src/renderer/src/components/route-error-page.tsx +++ b/apps/desktop/src/renderer/src/components/route-error-page.tsx @@ -1,4 +1,5 @@ import { useMemo } from "react"; +// eslint-disable-next-line no-restricted-imports -- MUL-4741 Phase 2: error recovery moves to the Coordinator; remove with §8.1 import { useLocation, useNavigate, useRouteError } from "react-router-dom"; import { AlertTriangle, RotateCw, Send, X } from "lucide-react"; import { Button } from "@multica/ui/components/ui/button"; diff --git a/apps/desktop/src/renderer/src/components/workspace-route-layout.tsx b/apps/desktop/src/renderer/src/components/workspace-route-layout.tsx index 959f8b6b7f..e5feee9b46 100644 --- a/apps/desktop/src/renderer/src/components/workspace-route-layout.tsx +++ b/apps/desktop/src/renderer/src/components/workspace-route-layout.tsx @@ -1,4 +1,5 @@ import { useEffect } from "react"; +// eslint-disable-next-line no-restricted-imports -- MUL-4741 Phase 2: workspace redirect + login bounce move to the Coordinator; remove with §8.1 import { Outlet, useNavigate, useParams } from "react-router-dom"; import { useQuery } from "@tanstack/react-query"; import { WorkspaceSlugProvider, paths } from "@multica/core/paths"; diff --git a/apps/desktop/src/renderer/src/hooks/use-tab-history.ts b/apps/desktop/src/renderer/src/hooks/use-tab-history.ts index 1c47fbd36e..0a3a9eb869 100644 --- a/apps/desktop/src/renderer/src/hooks/use-tab-history.ts +++ b/apps/desktop/src/renderer/src/hooks/use-tab-history.ts @@ -27,12 +27,14 @@ export function useTabHistory() { const goBack = useCallback(() => { if (!router || historyIndex <= 0) return; popDirectionHints.set(router, "back"); + // eslint-disable-next-line no-restricted-syntax -- MUL-4741 Phase 2: shell back/forward moves to the Coordinator (invariant 1); remove with §8.1 router.navigate(-1); }, [router, historyIndex]); const goForward = useCallback(() => { if (!router || historyIndex >= historyLength - 1) return; popDirectionHints.set(router, "forward"); + // eslint-disable-next-line no-restricted-syntax -- MUL-4741 Phase 2: shell back/forward moves to the Coordinator (invariant 1); remove with §8.1 router.navigate(1); }, [router, historyIndex, historyLength]); diff --git a/apps/desktop/src/renderer/src/routes.tsx b/apps/desktop/src/renderer/src/routes.tsx index 7c8faa950f..d3eb96175b 100644 --- a/apps/desktop/src/renderer/src/routes.tsx +++ b/apps/desktop/src/renderer/src/routes.tsx @@ -1,6 +1,7 @@ import { useEffect } from "react"; import { createMemoryRouter, + // eslint-disable-next-line no-restricted-imports -- MUL-4741 Phase 2: index redirect moves to the Coordinator; remove with §8.1 Navigate, Outlet, useMatches,