Files
codegraph/docs/design/framework-coverage.md
Colby Mchenry d0bb9be51e fix(react): a component a file declares itself as a value is what its JSX renders (#2420)
A PascalCase name that a .tsx/.jsx file declares as a module-level value is
that declaration wherever the file uses it: a JSX tag, a route's element, a
layout or a TSX type. Its initializer decides what it renders, in order:
- the component of the module its lazy loader loads, or the export the
  loader picks, in either branch of a conditional;
- the function it wraps (`observer(function X() {…})`,
  `forwardRef(Inner) as {…}`);
- else the declaration itself.

Before, the lookup by kind skipped the value and took a lone same-named
component anywhere, often in another app of the repository.

- Pattern 1 asks declaredComponent about every PascalCase reference, not
  only route references. layoutComponent and the jsx-render synthesizer's
  jsxChild ask it too.
- valueLoader reads a value's loader with loaderValue (#2480).
- defaultExportedName reads `export default observer(Login)`.
- lazyValueModules records the modules a file's lazy values load as
  synthesis inputs, so a sync that changes one of them redraws the tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 20:39:08 +00:00

430 lines
32 KiB
Markdown

# Framework & language coverage — what is done, what is left
**Last verified: 2026-08-29** (Angular row: 2026-10-06; React Router row: 2026-10-14) against the build at that date. Re-verify with the
queries in [Checking this file is still true](#checking-this-file-is-still-true)
before trusting a row; this is a snapshot, not a live view.
This file exists to be read cold. It says, for every framework and language the
README claims, **which of the three pictures it can draw today** and what is
missing from the ones it cannot — so a fresh session can pick up the next piece
without re-deriving the map.
---
## The three axes
A framework's support is not one thing. Three separate facts in the graph
unlock three different pictures, and a framework can have any subset:
| Fact in the graph | Unlocks | Produced by |
|---|---|---|
| **`route` nodes** bound to a handler or component | the **Entry points** tab; an endpoint or page can be a Steps anchor | a framework resolver's `extract()` |
| **`navigates` edges** from the code that sends a user somewhere to the route it names | the **Screens** tab — without a single one, `buildScreens` returns `routed: false` and the tab stays hidden | a resolver's `resolve()` (calls) + a synthesizer (markup) |
| **branch-guard rules** for the language | the `WHEN` label on every arrow, in Steps, Screens and `codegraph_explore`'s Flow section | `src/graph/branch-guards.ts` |
The Screens picture is a pure function of the first two: *any* framework that
produces route nodes and `navigates` edges lands on the tab, with no view code
to write. That is why "add a router" is a small, self-contained job.
---
## Routers — routes AND navigation (done)
Seven. Each reads a literal destination and leaves a computed one, a path no
route serves, and a conditional whose arms disagree unresolved rather than
guessed.
| Router | Resolver | Markup synthesizer | Tests | Validated on |
|---|---|---|---|---|
| Expo Router | `frameworks/expo-router.ts` | `expo-router-synthesizer.ts` | `expo-router.test.ts` | — |
| Next.js | `frameworks/nextjs.ts` | `next-router-synthesizer.ts` | `nextjs.test.ts` | next-saas-starter |
| React Router | `frameworks/react-router.ts` | `react-router-synthesizer.ts` | `react-router.test.ts` | proshop (44 edges), proshop-v2 (28), react-redux-realworld (22), react-boilerplate (`styled(Link)`), takenote (v5 `<Redirect>`), bulletproof-react (nested `children`, `lazy` routes, `paths.x.path` constants through an app's own tsconfig alias: 0 → 9 named, linked routes), and its `paths.x.getHref(id)` links and `navigate(paths.x.getHref())` calls (0 → 11 navigates). Route tables another file hands the router (`tableRoutes` in `frameworks/react.ts`, trap 13): the ASP.NET Core React template's `AppRoutes.map(… <Route {...rest}>)` on jasontaylordev/CleanArchitecture's `ClientApp-React` (0 → 6 routes, 11 navigates), replaysMike/Binner (0 → 46, 87), GavinLonDigital/RankingApp, GarrettHays/PaydirtPickem, charlessolar/eShopOnContainersDDD; `useRoutes(routes)` from another file and `[...sessionRoutes]` on uilibrary/matx-react (0 → 7, 16); codedthemes' `createBrowserRouter([MainRoutes, LoginRoutes])` on berry and mantis (9 and 8 routes, no navigation they can resolve); a table `.map`ped inside `<Route path="/dashboard">` on RADeveloping/chickadeeinvest; `useRoutes([...])` in place on minimal-ui-kit/material-kit-react (0 → 7, bound through `lazy(() => import(…))` to each page). JSX index routes and layouts (trap 14): proshop-v2's `createRoutesFromElements` (`/` is HomeScreen inside App, not App; 30 layout edges, the `path=''` guards included; Screens 30 → 74 transitions, the guards' redirects drawn from the screens they guard), chickadeeinvest's table inside `<Route element={<DashboardLayout/>}>` (its 4 routes get the layout), React Router 6.29's own examples and tests, refine's 281 examples (one `/` per app, 191 layout edges, 21 new navigations), crwn-clothing, the vanlife course app, volun-mern (105 layout edges, six levels deep), spotify/vispana, react_persist_login, chushi (`path={"agents"}`: 2 → 6 routes), hathor-explorer (`path=""` at the root), cboard, react-router-auth-v6: 578 of 581 new layout edges name a component of an enclosing `<Route>` (vanlife's 3 inherit a default-import resolution bug that already misbinds its pages), 27 of 27 new navigations precise, no navigation lost, 16 control repos byte-identical. Lazy route modules that are barrels (`exportedComponent` in `frameworks/react.ts`): luci-go's milo/ui (`export * from './x'` index files and `export { X as Component }` clauses: 0 → 51 lazy routes linked; the other 40 load a file under `src/build/`, which the default ignore skips, export only a `loader` or a `handle`, or pick a named export in an async `lazy`), Medusa's admin dashboard (all 193 route-map modules resolve through its `export { X as Component } from` barrels, but its routes are not extracted: `getRouteMap()` returns the table), four more apps (9 routes), 16 control repos byte-identical. Lazy loaders that pick their page (`lazyRouteReference` in `frameworks/react.ts`, trap 18): milo/ui's three tabs (`const { TestTab } = await import('…/tabs')`), 20 apps from a GitHub survey of `createBrowserRouter` + `lazy` (636 picked-page edges, each checked against the target file's own export: 467 by name, 169 as its `export default`; 42 routes written with `async lazy()` now indexed, among them React Router's own `lazy-loading-router-provider` example), an AST oracle on the TypeScript compiler agreeing with the text reader on all 2,538 loaders of 876 surveyed router files, a late-file sync equal to a fresh index on 8 of those apps, and 26 control repos byte-identical, among them ghostfolio, whose 30 Angular lazy routes stay Angular's with React detected in the same project. Barrels that pass on a page they import (`import { UserLayout } from './UserLayout'; export { UserLayout }`, `import Login from './Login'; export default Login`; `exportedComponent` follows the import): Arshia76/DigiShop.Frontend (0 → 27 route and layout edges), 49 other repos byte-identical; `export default withAuth(Page)` is followed nowhere. A page in a folder below the barrel's (`pages/Home/index.tsx` behind `@/pages`) whose route failed waits under the barrel's module key (`module:pages`), which the page's own keys never give: a sync that adds the page, or gives it a definition, walks up the import edges to the barrels that pass it on (`reopenReferencesThroughBarrels` in `resolution/module-importers.ts`) and re-opens what waits under theirs. A late-file sync on DigiShop then equals a fresh index (13 of its routes stayed unlinked before), and 27 other repos sync exactly as before. A name the route file declares itself (trap 19), such as a lazy page or layout, a `Loadable(lazy(…))` or an `observer(…)` value, binds to what that declaration renders wherever the file uses it, in routes, layouts and JSX alike: outline (+31 edges, 7 moved off another file's namesake or onto the screen a lazy declaration loads), mastodon (+20, 9 moved: `lazy(() => import(…).then(({ Emoji }) => …))` values and `let Emoji` locals had gone to other files' components), SigNoz (+7, 4 moved), excalidraw (+3, 1 moved: the Next.js example's `<Excalidraw>` was the docs site's), bulletproof-react (+6), material-kit-react and create-t3-turbo (+1 each), with matx, mantis, proshop, next-saas-starter and berry byte-identical |
| TanStack Router | `frameworks/tanstack-router.ts` | `tanstack-router-synthesizer.ts` | `tanstack-router.test.ts` | TanStack examples, fastapi-template frontend |
| Vue Router / Nuxt | `frameworks/vue-router.ts` (Nuxt file routes: `nuxtResolver` in `frameworks/vue.ts`) | `vue-router-synthesizer.ts` | `vue-router.test.ts` | vue-realworld (23 edges); vue-element-admin (62 routes), vue-admin-template (14), vben (192), halo console (34) — named tables, module files, `children` + layouts; Nuxt: mealie, elk, nuxt/movies |
| SvelteKit | `frameworks/sveltekit-router.ts` | `sveltekit-synthesizer.ts` | `sveltekit-router.test.ts`, `sveltekit-route-names.test.ts` | sveltekit-realworld (31 edges); shadcn-svelte and skeleton (`(group)` layouts: 13 and 23 edges), svelte.dev (74), kit's test apps (47) |
| Angular | `frameworks/angular-router.ts` | `angular-template-synthesizer.ts` | `angular-router.test.ts` | angular-realworld (31 edges, 18 renders, 32 bindings), Ghostfolio (189 edges, 170 renders, 215 bindings), ngx-admin (routes and renders; its menus are config; 13 bindings), CleanArchitecture's `src/Web/ClientApp` (signals; 12 bindings), angular-spotify (Nx libs behind barrels: 14 routes), jira-clone (class-constant paths, mount-only redirects), jhipster (60), ionic-conference (18), Angular-JumpStart (18) |
Shared machinery all seven use, in `frameworks/expo-router.ts`: `RouteTable` /
`RootedRouteTable`, `routesForFile`, `addRouteTo`, `matchRoute`, `appRootFor`,
`parseHrefExpression`, `readHrefViaLocal`, `nthArgumentText`, `readStringAt`,
`toHref`. Plus `pageForHref` in `frameworks/nextjs.ts` (framework-agnostic
despite where it lives) and the object-literal walker in
`frameworks/object-literal.ts`.
Angular is the one whose markup is not indexed: a component's template is a
`templateUrl` file (or an inline `template:` string) read at synthesis time,
which also yields the component tree (`<app-foo>` by element selector) — the
edge a navigation in a child component rides to its screen. A `routerLink:`
field written in a component's class (a tab bar's or a menu's config, bound
in a loop elsewhere) counts as a link from that component. An event binding
(`(click)="save()"`) is a `calls` edge from the component to its own method
carrying `metadata.trigger`, which Steps uses in place of reading a trigger
at the edge's line (the binding is in the template, not the source there).
A property binding, an interpolation, a structural directive or a
control-flow block that calls one of the component's own members
(`[name]="icon()"`, `{{ label() }}`, `*matRowDef="let row; columns:
displayedColumns()"`, `@if (loading())`, `@let total = price();`) is a
`calls` edge too (`angular-binding`, the binding as `via`), with no trigger:
it runs when the template renders, so Steps folds it into the screen rather
than drawing a handler the user fires — a trigger there drew `computed`
signals as user actions. A member read without a call counts only when it is
method-kind: a getter is called, a method handed to a child
(`[displayWith]="displayFn"`, `trackBy: trackById`) is a `references` edge
with `fnRef`, and a field a call filled (`days = Array.from(…)`) a plain
`references`. A property read (`[value]="title"`) links nothing, as in
TypeScript, which records no property reads either; on angular-realworld,
ngx-admin, Ghostfolio and CleanArchitecture it would have added four times
the edges and taken nothing off the Dead code list, which never asks about
properties. A pipe, an object key, a
microsyntax key (`of`, `trackBy:`, `index`), a template's own names (`let
item`, `as user`, `#ref`, `@let x`, `let-row`) and a commented-out binding
link nothing. A route with
`children` is a layout: its component carries a `references` edge marked
`layout: true` from each screen nested in it, and `routeLayouts` in
`route-roots.ts` gives Screens every screen a layout serves. Known limits: a
route with a custom `matcher` has no static address, a relative navigation
(`relativeTo`) is left unresolved, an edit to a template file alone is
picked up at the next sync of any source file (templates are not watched), a
member a component inherits from a base class is not linked from its
template, and a template's local hides a same-named member for the rest of
the template rather than only inside its block.
---
## What is left
Ordered by cost-to-value. Each row says what is missing, not merely that
something is.
### 1. Astro — the last web framework with routes but no navigation
**Has:** `src/pages/` file routes (`.astro` pages + `.ts` endpoints,
`[param]`/`[...rest]`), in `frameworks/astro.ts`.
**Missing:** `navigates` edges. Astro is an MPA — navigation is a plain
`<a href="/about">`, plus `Astro.redirect('/x')` in frontmatter and
`redirect` entries in `astro.config`.
**Size:** smallest job on this list. `sveltekit-synthesizer.ts`'s
`svelteKitLinkEdges` is the same pass over the same tag against a different
table; the resolver half is one `Astro.redirect` reader.
**Validate on:** any `withastro/astro` example, or the Astro docs site.
### 2. Server-rendered frameworks — a redirect is a transition, not just a response
**Fourteen frameworks** have route nodes and no navigation: Django, Flask,
FastAPI, Express, NestJS, Laravel, Drupal, Rails, Spring, Play, Gin/chi/gorilla,
Axum/actix/Rocket, ASP.NET, Vapor.
Be precise about what is missing. `redirect_to`, `HttpResponseRedirect`,
`res.redirect`, PHP's `redirect()` are **already recognised as `response`
effects** (`ui-server/api/effects.ts`), so they draw as a box in the Steps
picture. What is missing is the edge to the page they name — so two pages never
connect on the Screens tab.
For a pure API this is correct and nothing should change: an endpoint is not a
screen. It matters for the **server-rendered** half, where a classic MVC app
gets no Screens picture at all today:
| Framework | The destination to read | Why it is harder than a client router |
|---|---|---|
| Rails | `redirect_to :dashboard`, `redirect_to users_path` | destinations are named helpers (`*_path`/`*_url`) generated from `routes.rb`, not literals |
| Django | `redirect('profile')`, `reverse('profile')` | same — a route *name*, like Vue's `{ name }`, which `vue-router.ts` already shows how to index |
| Laravel | `redirect()->route('home')`, `->view()` | route names again |
| Spring | `"redirect:/x"`, `RedirectView` | a literal inside a string return value |
| ASP.NET | `RedirectToAction("Index", "Home")` | controller + action pair, not a path — needs the route table's reverse mapping |
| Flask | `redirect(url_for('profile'))` | nested call; the name is `url_for`'s argument |
The Vue name-index (`VueAppRoutes.byName`) is the closest existing precedent for
all of these.
### 3. Native UI — no route nodes at all
| Platform | Routes would come from | Navigation would come from |
|---|---|---|
| SwiftUI | `NavigationStack(path:)`, `.navigationDestination(for:)` | `NavigationLink(value:)`, `path.append(…)` |
| Jetpack Compose | `NavHost { composable("route") { … } }` | `navController.navigate("route")` |
| Flutter / Dart | `MaterialApp(routes: {…})`, `GoRouter([...])` | `Navigator.push`, `context.go('/x')` |
All three are named in `scripts/try-repo.sh`'s presets as not modelled
(`icecubes`, `nowinandroid`). Compose and go_router are the most tractable —
both name routes with string literals, which is the same shape every router
above reads.
### 4. ArkTS / HarmonyOS — closest to done of anything here
**Has:** the hard half already. `arkuiRouterEdges` in
`callback-synthesizer.ts` resolves `router.pushUrl('/pages/Detail')` to the
target page struct.
**Missing:** it emits a **`calls`** edge and no `route` node, so it never
reaches the Screens tab.
**Size:** an edge-kind change plus route nodes for `pages/` entries — no new
analysis.
---
## Languages
All ~30 languages in the README have full structural extraction; nothing is
outstanding on that axis. The gap that is language-shaped is the **`WHEN`
label**.
**Guard rules exist** (`RULES_BY_LANGUAGE` in `src/graph/branch-guards.ts`) for:
TypeScript, TSX, JavaScript, JSX, Swift, Python, Java, Kotlin, C#, Go, C, C++,
Objective-C.
(Metal and CUDA parse **as** C++ and ArkTS does **not** parse as TypeScript, so
the first two inherit the C rules and the third has none.)
**No rules** — boxes draw, arrows carry no condition, and no arguments or
trigger labels are read: PHP, Ruby, Rust, Scala, Dart, Erlang, Lua, Luau, R,
Solidity, COBOL, CFML, VB.NET, Nix, Terraform, Pascal/Delphi, Liquid, Razor,
Twig, ArkTS, and the `.svelte` / `.vue` / `.astro` template languages.
A language with no rules yields **nothing**, never a wrong label — that is the
design, so an absent row here is a missing feature, not a bug.
**Ruby and Rust sting most**: both have server frameworks in the README's table
(Rails, Axum/actix/Rocket), so their Steps pictures draw responses and database
calls with no conditions on any arrow. `scripts/try-repo.sh`'s `bookstack`
preset says exactly this for PHP.
---
## Traps a new router will hit
Each of these cost real debugging time; they are not hypothetical.
1. **A `references` edge cannot cross a language family.** `applyLanguageGate`
in `name-matcher.ts` filters `references` candidates to
`sameLanguageFamily`, so a `.js` router config can never name a `.vue`
component — it silently binds to a same-named `.js` function in a store
instead. Bind a route to its component with **`calls`**, which
`route-roots.ts` reads as "the page a screen file exports".
2. **One address, one screen.** A layout and the index route beside it resolve
to the same path (`+layout.svelte` vs `+page.svelte`, `dashboard.route.tsx`
vs `dashboard.index.tsx`, `_auth.invoices.tsx` vs `_auth.invoices.index.tsx`).
Emitting both puts one address on the map twice. Decide **per file** — the
sibling is not visible at extraction time.
3. **The route table must be per app.** A repository with two apps has two `/`
and two `/login`; a global table hands the address to whichever was indexed
first. Measured at **82% of navigations pointing into a different app** on a
477-app monorepo before `RootedRouteTable` / `routesForFile`. The `roots`
list decides only *whether* to resolve.
4. **Read fields from the object, not from a window around one.** A Vue route's
`name` is written above its `path`, so a text window handed every entry its
predecessor's name — silently, for every route in the file. Use
`frameworks/object-literal.ts`.
A lazy view binds by the FILE it imports, never by the import's last
segment: vue-element-admin's views are all `…/index.vue`.
Nuxt's `pages/` convention belongs to a Nuxt app only — a plain Vue app's
`pages/` folder (halo's console) holds components a router config names.
5. **A receiver is required for a generic verb.** `push` and `replace` are two
of the most common method names in JavaScript; claiming a bare one puts every
`paths.push('/tmp/x')` one string-match away from a route.
6. **One component can be several screens.** A listing rendered at `/`,
`/search/:keyword` and `/page/:n` is one component and three addresses;
`screenOfComponent` maps a component to **every** route it serves, and
`collapseSharedChrome` counts distinct screen COMPONENTS, not addresses —
counting addresses collapsed one component's four routes into an origin and
took the navigation away from all of them.
7. **A destination can name several routes.** `parseHrefExpression` returns
one `HrefLiteral` carrying `alternates`, and `destinationsForHref` turns it
into one `{ node, href }` per arm. A synthesizer emits an edge apiece; a
resolver puts the first on the `ResolvedRef` and the rest in `alsoTargets`,
which `createEdges` fans out — the reference still resolves ONCE, so the
pipeline's cleanup and counts are untouched. Label each edge with the arm
that named it, or an edge points at one route while naming another's path.
8. **Read markup with the same reader as calls.** A synthesizer that peeks at
the first character of `to={…}` misses every conditional and template the
`push(…)` path handles. Use `parseHrefExpression` on the balanced brace
contents.
9. **A condition is read the same way for markup as for a call.** The Screens
walk used to skip the `when` on any synthesized edge, so every
`<Link to>` read as *always* while the `push()` beside it carried its guard.
The reader works fine at a markup site — a JSX `{step1 ? <Link/> : …}` is a
ternary like any other — and the site's own verb (`link`, `a`) is the honest
label; `return` belongs only to an edge whose destination came from
elsewhere, which is what `registeredAt` pointing at another line means.
10. **A route is not always a screen.** The Screens picture is about
navigation, so it draws only routes named by a path; a route named with the
HTTP method that reaches it is an endpoint and belongs on Entry points. Nuxt
is the exception that names an endpoint like a page (`/api/users` from
`server/api/`), and is excluded by file path.
11. **Detection runs before any file is indexed.** `declaredDependencies` caches
per file-count for exactly this reason — an earlier version cached the empty
pre-index answer and every framework whose dependency lived one directory
down stayed undetected.
12. **An app's manifest can sit three or more levels down.** An ASP.NET
solution keeps its single-page app in `src/Web/ClientApp/`, and prometheus
keeps its React apps in `web/ui/mantine-ui/` under a workspace root at
`web/ui/` that declares only tooling. `declaredDependencies` reads the root
and the first two levels, then gives the slots left to the directories
above JS/TS code, shallowest first. Read only two levels deep, Angular
Router never ran on jasontaylordev/CleanArchitecture, and every
template-bound handler there was listed as dead code. A new detector that
gates on a dependency should ask `dependsOn`, not read a `package.json`
itself, so it sees the same manifests every other detector does.
13. **A route table can be named only by another file.** The ASP.NET Core
React template keeps its routes in `AppRoutes.js`, a file that never names
the router, and renders them from `App.js` with `AppRoutes.map(({ element,
...rest }) => <Route {...rest} element={element} />)`; codedthemes' admin
kits put one route object per file and list them in
`createBrowserRouter([MainRoutes, LoginRoutes])`. Read on its own, such a
file cannot be told from a menu's `{ path, element }` list, and per-file
extraction cannot see the file that hands it over, nor run again when that
file changes. So React keeps those routes in the cross-file pass
(`FrameworkResolver.crossFileNodes`, reconciled in `runPostExtract` on every
index and sync, removals included): it starts from the files that import
React Router, takes only a `useRoutes` / `create*Router` argument or a
`.map` whose callback renders a `<Route>` from the item's own fields
(`{...route}`, `{...rest}`, `path={route.path}` — never
`path={r.layout + r.path}`), follows the import to the table and the tables
it names in turn, and owns its routes by id (`…:table:<path>`). One limit
it shares with every router: a table rendered by a component that is
itself mounted under another file's `<Route path="/admin/*">` reads as if
at the root. A sync that changes such a route resolves the calls it can
answer again (16), as for any other route.
14. **An index route is at its parent's address, so it needs that address
written down.** `<Route index element={<Home />} />` inside `<Route
path="/" element={<Layout />}>` is the page at `/`, and Layout is the
layout of every route nested in it (trap 2: emitting Layout as a second
`/` page drew the app's chrome as the home screen and its header's links
from `/` alone). A `<Route path>` with no element only groups its children
and yields its address the same way. But a `<Routes>` inside a component
is mounted wherever another file's `<Route path="shop/*" element={<Shop
/>}>` puts it: read as `/`, crwn-clothing's `Shop` index made a second
home page. So an index route at the top of a `<Routes>` is read only
inside the router itself (`createRoutesFromElements(…)`, `<BrowserRouter>`),
and one under a path the file does not spell out (`path={paths.agents}`)
not at all; `path={"agents"}` is spelled out. Reading what an element
renders has three traps of its own: skip elements written in an attribute
unless the attribute hands over a page (`component`, `element`, `page`) —
`<Suspense fallback={<Loader />}>` bound fifteen routes of one app to the
spinner; trim the line break Prettier writes after `element={`, which hid
every long element; and read `<Outlet />` as rendering nothing of its own.
Version 5's `<Route path="" component={NotFound} />` is a catch-all, not
the page at its parent's address. A table written in another file and
mapped inside a `<Route element>` gets no layout edge: the layout's name
would be looked up in the table's file, and the cross-file pass compares
only route names, so a changed layout would never reach the table's routes.
15. **`resolve()` sees only references written in the framework's own
languages** (`getResolvingFrameworks`): Express's `logger` middleware rule
once took etcd's Go `*zap.Logger` result types for a method. A navigation
written in markup (`.svelte`, `.vue`) needs that language in `languages`.
A resolver that reads a language it extracts nothing from lists it in
`resolveLanguages` instead — Svelte's `$lib/…` imports in `.ts` route
modules, ASP.NET's Razor `@model` — because widening `languages` also runs
`extract()` on those files. `claimsReference()` is asked of the same
frameworks. It used to be asked of every detected one, because protobuf's
C++ `::_pbi::…` calls got past the name pre-filter only on the Swift ↔
Objective-C bridge's claim of any name with a `:` in it. The pre-filter
now reads a leading `::` as the global scope.
16. **A navigation call waits for a route, not for a name.** A call indexed
before its route existed is parked as failed (or bound to a catch-all, a
parameter route, the other arm of a conditional), and sync revisits a
failed reference only by matching its last name segment against the names
the synced files define — never `push` or `navigate`. Set the resolver's
`navigation`: `tails`, the method names its calls end in, and `scope`, the
apps whose calls one of its routes can answer (its own app's table, or every
file for a table the whole project shares). A sync that adds, removes or
renames a route — a cross-file table route included — then resolves those
calls again, failed or not; without it the synced index keeps an answer a
full index does not have. `sync-navigation-retry.test.ts` runs all seven
routers through it, and asserts each router's calls end in a declared tail.
17. **A sync revisits a resolved route only by the names it carries.** A route
that renders a same-file `const Docs = lazy(() => import('./pages/Docs'))`
binds to the component that module exports, and to the declaration while
the module is missing or exports none (`declaredComponent`). The reference
resolved, so the failed-ref retry never sees it, and the rebind of the
names a sync defines (CG-33) reaches it only when the module's component
is named like the declaration: on minimal-ui-kit/material-kit-react,
`/user` stayed on its `UserPage` declaration after `src/pages/user.tsx`
(`Page`) came back. A router whose `resolve()` reads modules' content to
answer a route says which ones (`FrameworkResolver.lazyModules`), barrels it
follows included, and a sync that adds or edits one of them puts the
route's reference back for its resolution sweep. The same holds for the
JSX a component renders: a sync that adds a component redraws the
`jsx-render` edges, since a tag in a file it never touched may name it.
A route whose reference FAILED waits under the key of the module it names
(`module:pages` for `@/pages`), and a page in a folder below that barrel
(`pages/Home/index.tsx`) gives only its own folder's and stem's keys. So a
sync that adds a file, or gives one a definition, walks up the import
edges to the barrels that pass it on and re-opens what waits under their
keys (`reopenReferencesThroughBarrels`, after the failed-ref retry made the
barrel's import edge into the added file).
18. **A lazy loader is code, and its page is what it hands over.** A React
Router `lazy` resolves to the route's properties, so `const { TestTab } =
await import('./tabs'); return { Component: TestTab }` renders `TestTab`,
not the module's default export; reading the first `import('…')` in the
loader linked luci-go's tab routes to nothing and other apps' routes to
another page of the same module (`lazyRouteReference` in
`frameworks/react.ts` follows the loader's bindings to its `Component`, or
the one lazily imported export its `element` shows past a guard). The
picked export is named the way Vue Router and Angular name a lazy
component, `import:<path>#<export>`, so sync's module-tail retry covers it
(`MODULE_REFERENCE`), but React is registered before both of them: it
answers only references a React route makes (`isReactRouteRef`: tsx or jsx,
which Vue's and Angular's route references never are), or it takes theirs
first, as an Angular app with React in a `package.json` would let it.
19. **A name a file declares is that declaration, wherever the file uses it.**
JavaScript scoping makes `<RegisterPage />`, a route's `element:
<RegisterPage />`, a `layout:` and a TSX type all the `const` the file
declares under that name, so every one of them asks `declaredComponent`
first (Pattern 1 of `reactResolver.resolve` for any PascalCase reference,
`layoutComponent`, and the `jsx-render` synthesizer's `jsxChild`), and the
lookup by name runs only for a file that declares no such value. A value is
no component node: the lookup by kind skipped it and took a lone
same-named component anywhere, so mantis's vite app rendered its Next.js
app's `RegisterPage`. What the value renders is read from its initializer
(`valueLoader`, by `loaderValue`): the module a lazy loader loads or the
export it picks, in either branch of a conditional (SigNoz's `IS_DEV ?
React.lazy(…) : null`); the function it wraps (`observer(function
Settings() {…})`, SigNoz's `React.memo(TableBody, areEqual)`, mastodon's
`forwardRef(ComboboxWithRef) as {…}`); else the
declaration itself (`const DialogPortal = DialogPrimitive.Portal`,
`createContext(…)`). A module whose default export is `observer(Login)`
hands over `Login` (`defaultExportedName`). A tag's edge now depends on the
modules the file's lazy values load, so those are synthesis inputs
(`lazyValueModules`): a sync that gives one its default export redraws the
tag, as a fresh index would draw it.
---
## The bar for calling one done
Per `CLAUDE.md`'s validation methodology, and what was actually done for the
four routers added on 2026-08-29:
1. **A real repo, not only a fixture.** Every defect in this session's work was
caught by a real repository and none by the fixture written first.
2. **Recall against ground truth.** `grep` every navigation site in the source
and account for each one: resolved, or correctly unresolved because it is
computed.
3. **Precision, site by site.** For every synthesized edge, read the line its
`registeredAt` names and confirm the tag or call there names that
destination. Target: zero false positives.
4. **Controls re-indexed.** Node, edge, route and `navigates` counts on repos
the change should not touch — a change to shared machinery is not done until
they are byte-identical or the difference is explained.
5. **Full suite green**, and a CHANGELOG entry in the user-facing voice.
---
## Checking this file is still true
```bash
# Which frameworks emit navigates edges
grep -rn "edgeKind: 'navigates'\|kind: 'navigates'" src --include="*.ts" | sed 's|:.*||' | sort -u
# Which languages have branch-guard rules
sed -n "/^const RULES_BY_LANGUAGE/,/^\]);/p" src/graph/branch-guards.ts
# Whether a repo's Screens tab is on, and how many transitions it has
scripts/try-repo.sh <preset> # prints the navigation count and says which tab is on
```
```sql
-- In a repo's .codegraph/codegraph.db
select count(*) from edges where kind='navigates';
select name, file_path from nodes where kind='route' order by name; -- duplicates = a layout drawn as a screen
```