Unclassified

components/l1 → app/ Alignment Spec

Status: Proposed
Scope: apps/web/src/components/l1 (240 files, 75,448 LOC) and apps/web/src/app
Question answered: which parts of components/l1 are route concerns that belong in the App Router tree, and which are components that should stay put.


1. The sorting rule

One test decides every file:

app/ answers "what URL am I at?" — components/ answers "what does this look like?"

A file belongs in app/ if it reads or writes location, maps a path to a view, or owns chrome that persists across a URL change. Everything else is a component, regardless of how large or important it is.

Today components/l1 contains all four App Router primitives — route table, layout, template, loading — implemented as client components. That is the entire misalignment. Nothing is missing; it is all in the wrong place, so Next.js cannot see it.


2. Current state

2.1 Two routing worlds coexist

World A — already Next-native (the reference pattern).
/domains/[domain]/[view] resolves through domain-route-manifest.ts (server-safe) into domain-route-shell.tsx, which uses next/navigation and drives navigation by URL. This is correct and should be the template for everything else.

World B — SPA (everything else).
Thirteen entry points in app/ return the identical component:

// app/settings/page.tsx, app/admin/page.tsx, app/knowledge/page.tsx,
// app/governance/page.tsx, app/administration/page.tsx, app/customer-zero/page.tsx,
// app/app/page.tsx, app/app/[...segments]/page.tsx,
// app/workspace/[department]/page.tsx, .../work/[module]/page.tsx,
// app/workbench/[department]/page.tsx, .../work/[module]/page.tsx
  return <WorkbenchRoute />;
}

WorkbenchRoute → ClientProviders (mounts <BrowserRouter>) → AppShell (ssr: false) → WorkspaceShellNew → ContentRouter. Next.js sees one indistinguishable page at seven distinct URLs. The real routing happens in content-router.tsx reading useLocation().

Consequences: no nested layouts (one layout.tsx exists in the whole app), no loading.tsx/error.tsx, no per-route metadata, no streaming, and all 75k LOC of l1 sits behind a single ssr: false boundary that must resolve before first paint.

2.2 Four registries describe the same route set

content-router.tsx · domain-view-registry.tsx (774) + its hand-synced twin domain-route-manifest.ts · platform-registry.tsx · config/os-shell-registry.ts · app/_lib/workbench-routes.ts

The manifest file's own header — "Keep in sync with domain-view-registry.tsx" — is the cost being paid. The App Router filesystem is the registry; these collapse into it.

2.3 The shim points backwards

shims/next-navigation.ts maps next/navigation → react-router, commented "Used during migration from Next.js to Vite + React Router." The repo has since reversed direction. 37 files import react-router; 16 use next/navigation. The shim now actively holds the codebase in the world it is trying to leave.


3. Classification

3.1 MOVE to app/ — route resolvers (delete on completion)

These are routers wearing component costumes. The App Router replaces them with directories.

FileLOCBecomes
workspace/content-router.tsx794route segments + generateStaticParams
l1-module-content.tsx908route segments
domains/domain-view-registry.tsx774filesystem routes (retires domain-route-manifest.ts with it)
workspace/animated-content-router.tsx384template.tsx — Next remounts templates on navigation, which is precisely the transition semantic this file hand-rolls
workspace/platform-registry.tsx118folded into segments

3.2 PROMOTE to layout.tsx — persistent chrome

Stay as presentational components; get mounted by a layout instead of wrapping their own children. This is what stops sidebars remounting on every navigation.

FileLOCBecomes
admin/admin-shell.tsx350app/admin/layout.tsx — already does isActive(pathname, href) with react-router Link; a mechanical swap to next/link + usePathname
domains/domain-route-shell.tsx214app/domains/[domain]/layout.tsx; page.tsx renders only the view
workspace/workspace-shell-new.tsx1436split: chrome → app/(workbench)/layout.tsx, nav data → server module, view state → segments
navigation/global-nav-bar.tsx342layout-mounted
workspace/loader-phase1.tsx463loading.tsx

3.3 CONVERT to page.tsx — files already named "page" that are not pages

admin/pages/* — 17 files, 3,154 LOC, each 1:1 with a URL that already exists in admin-shell's nav:

actions · admin-today · audit · billing · connectors · governance · iam · knowledge-governance · observability · provisioning · registry · roles · schema · tenancy · tools · usage · users

→ app/admin/<name>/page.tsx, each with its own metadata, loading.tsx, and code-split boundary.

3.4 STAY in components/ — genuine components

All of domains/*/views/* (~45k LOC), workbench/* primitives, views/*, twin/, knowledge/, notifications/, command-palette/, ooda/, surfaces/. These render; they do not route. They move only if the l1/ folder name is retired (§6).


4. Target tree

app/
├── layout.tsx                        # root: html, providers, fonts
├── (marketing)/                      # route group — public
│   ├── layout.tsx                    # ← legacy-pages/marketing/MarketingShell
│   └── [slug]/page.tsx
├── (workbench)/                      # route group — authenticated
│   ├── layout.tsx                    # ← workspace-shell-new chrome
│   ├── template.tsx                  # ← animated-content-router
│   ├── loading.tsx                   # ← loader-phase1
│   ├── error.tsx
│   ├── domains/[domain]/
│   │   ├── layout.tsx                # ← domain-route-shell (sidebar persists)
│   │   ├── page.tsx
│   │   └── [view]/page.tsx
│   └── admin/
│       ├── layout.tsx                # ← admin-shell
│       └── {users,roles,audit,…}/page.tsx    # ← admin/pages/* ×17
└── onboarding/
    ├── layout.tsx                    # ← OnboardingShell chrome
    └── [step]/page.tsx

The seven duplicate WorkbenchRoute URL families collapse into one (workbench) group.


5. Sequence

Ordered by evidence per unit of risk — each phase proves the pattern before the next inherits it.

#MoveRiskWhy first
1admin/pages/* → app/admin/*/page.tsx + admin-shell → layout.tsxLow17 files already 1:1 with URLs; isolated subtree; zero product-surface impact. Proves the pattern end to end.
2domain-route-shell → layout.tsx; delete domain-route-manifest.tsLowAlready next/navigation. Removes the hand-synced registry and stops sidebar remount.
3Collapse 7 WorkbenchRoute families into (workbench)MediumRedirects from retired paths; decide canonical vocabulary first (§6).
4content-router + l1-module-content → segmentsHighOnly safe once 1–3 have established the layout contract.
5Delete shims/next-navigation.ts; convert remaining react-router importsHighLast. Deleting the shim is the completion signal, not the starting move.

Phases 1–2 are independently shippable and reversible.


6. Two decisions this surfaces

Route vocabulary. /app, /workspace, /workbench, and /domains currently mount the same shell. CLAUDE.md says "The Workbench is one surface." Four URL families for one surface is naming drift with a routing cost. Recommend /workbench/* canonical, the rest permanent redirects.

The l1/ folder name. CLAUDE.md forbids L1/L2/L4/L7 in user-facing UI. The prohibition is about UI, so the folder is not a violation — but layers/, l1/, l2/, l4/, l7/, and _deprecated/l2-removed/ keep retired vocabulary alive in the paths every contributor reads daily. Moving route concerns out shrinks l1/ naturally; renaming the remainder to components/workbench/ is a follow-on, not part of this work.


7. Definition of done

  • shims/next-navigation.ts deleted; zero react-router imports in apps/web/src
  • <BrowserRouter> removed from client-providers.tsx
  • Every persistent chrome surface is a layout.tsx; no shell wraps its own children
  • Route set is derivable from the filesystem alone — no parallel registry
  • ssr: false count reduced from 22 to only genuinely browser-only widgets
  • Each route family has loading.tsx, error.tsx, and metadata