chore(desktop): add navigation boundary lint rule (MUL-4741 Phase 2 prereq)

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 <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing
2026-07-15 13:34:04 +08:00
parent 9f837624af
commit 7054ff7daf
5 changed files with 40 additions and 0 deletions

View File

@@ -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.",
},
],
},
},
];

View File

@@ -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";

View File

@@ -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";

View File

@@ -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]);

View File

@@ -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,