Last verified: 2026-08-29 (Angular row: 2026-10-06; React Router row: 2026-10-19; Vue Router / Nuxt row: 2026-10-22) against the build at that date. Re-verify with the queries in 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.
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.
Eight, counting the fork's Astro resolver. 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, monorepo-app-frameworks.test.ts, monorepo-app-frameworks-sync.test.ts |
upstream: evanbacon.dev (+api endpoints), react-native-true-sheet (nearest manifest decides the app) |
| Next.js | frameworks/nextjs.ts, frameworks/next-proxy.ts |
next-router-synthesizer.ts |
nextjs.test.ts, nextjs-proxy.test.ts |
upstream: next-saas-starter; the proxy (trap 22): next-saas-starter (8 routes), taxonomy (6), ai-chatbot (19), dub's web app (80) |
| React Router / Remix | frameworks/react-router.ts |
react-router-synthesizer.ts |
react-router.test.ts, react-router-framework.test.ts, remix-routes.test.ts, react-router-returned-tables.test.ts |
proshop (44 edges), proshop-v2 (28), react-redux-realworld (22), react-boilerplate (styled(Link)), takenote (v5 <Redirect>); pinned official framework config and flat filenames; bulletproof-react: nested children, lazy routes and paths.x.path constants through per-app aliases; paths.x.getHref() links and navigate calls read literal or template destinations; upstream route-table and JSX index/layout regressions cover imported route tables, nested layouts and index pages; fork recheck: 11 navigates on bulletproof-react 9506629 (2026-10-01), all destinations checked against config and route registrations. 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, reading the export from binding rows; upstream: 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 (pages/Home/index.tsx behind @/pages) whose route failed waits under the barrel's module key: a sync that adds the page, or gives it a definition, re-opens it (reopenReferencesThroughBarrels in resolution/module-importers.ts; upstream: a late-file sync on DigiShop equals a fresh index). A name the route file declares itself (trap 19) binds to what that declaration renders in routes, layouts and JSX alike; upstream: outline (+31 edges), mastodon (+20), SigNoz (+7), excalidraw (+3), bulletproof-react (+6), with matx, mantis, proshop, next-saas-starter and berry byte-identical. Upstream's 2026-10-19 measurements, taken on its own resolver and not re-measured on the fork's: a table a function returns handed over through a variable (Medusa's admin dashboard, trap 13), <Outlet /> written in a route object (trap 14), and the baseUrl / "*" imports of matx, mantis and berry (trap 21). A table behind useMemo, and every return of a route-building function rather than the first (trap 13), are upstream's 2026-10-19 change too (tableRoutes in frameworks/react.ts, ported to the fork's top-level table reader; upstream: tg-tt-bot-stage, bewild-clone, SpaceR, BANKFORM1; not re-measured here). |
| TanStack Router / Start | frameworks/tanstack-router.ts |
tanstack-router-synthesizer.ts |
tanstack-router.test.ts, tanstack-start.test.ts |
TanStack examples, fastapi-template frontend; pinned Start server-handler syntax |
| Vue Router / Nuxt | frameworks/vue-router.ts (Nuxt file routes: nuxtResolver in frameworks/vue.ts) |
vue-router-synthesizer.ts |
vue-router.test.ts, nuxt-navigate-to.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. Nuxt's navigateTo (upstream's 2026-10-22 measurements, taken on its own resolver and not re-measured on the fork's): mealie 13 of its 14 calls, nuxt.com 17 of 24, elk 9 of 10 outside the plugin that declares its own, atidone 2 of 2, pages and route middleware alike; the rest are computed, an API endpoint ({ external: true }), or a page of a Nuxt layer (extends: ['./layers/nuxi']), whose pages sit in a route table of their own. nuxt/ui's docs: 0 of 4, two computed and two whose only page is a catch-all ([...slug].vue, named *slug) or an optional parameter ([[tag]].vue, :tag?), which matchRoute doesn't read yet, for markup links either |
| 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-routes.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) |
| Astro | frameworks/astro.ts |
— | astro-routes.test.ts |
pinned endpoint fixture; exact page components and source navigation |
| RedwoodSDK | frameworks/redwood.ts |
— | redwood-routes.test.ts |
pinned 1.7.3 typed-routes worker; exact page and API roots |
| Analog | frameworks/analog.ts |
— | analog-routes.test.ts |
pinned 2.7.1 sign-up page; filename/layout and registration sync controls |
| Solid Router | frameworks/solid-router.ts |
— | solid-router.test.ts |
pinned 0.16.3 README lazy example; exact component roots, nested paths and fresh workers |
| SolidStart | frameworks/solid-start.ts |
— | solid-start.test.ts |
pinned 2.0.4 About/API fixtures; page/API coexistence, file hierarchy and config sync |
| Vike | frameworks/vike.ts |
— | vike-routes.test.ts |
pinned 0.4.266 About/product examples; nearest route inheritance, config guards, sync and workers |
| Qwik City | frameworks/qwik-city.ts |
— | qwik-city.test.ts |
pinned 1.20.0 page/API sources; anonymous component roots, body-call ownership, sync and workers |
| Waku | frameworks/waku.ts |
— | waku-routes.test.ts, waku-programmatic.test.ts |
pinned 1.0.0-rc.0 home/Cloudflare/createPages fixtures; exact roots, staticPaths, config sync and workers |
Waku programmatic coverage recognizes default/Cloudflare adapter registration of createPages (imported from waku or waku/router/server) in the default server module, including a local const binding. Direct calls to its injected createPage helper (including aliases) are read from synchronous/async callbacks without executing them. Literal paths, render modes, static paths and exactPath are supported; roots bind exact named local functions or directly imported named/default function exports. The official typegen fixture supplies the registration pattern. Conditional calls, nested helpers, arbitrary registration wrappers, computed options, component wrappers, re-exports and anonymous imported targets remain unsupported. createLayout/createRoot/createSlice/createApi do not become pages; unstable_skipBuild does not remove runtime routes.
Waku filesystem coverage targets 1.0.0-rc.0, with default src/pages discovery or a literal default/Cloudflare adapter around fsRouter(import.meta.glob('./pages/**/*.{tsx,ts}')) (default extension subsets are honored). Official home, Cloudflare entry. Local default functions, including anonymous functions, bind exact component roots. Pages default to static rendering; literal getConfig return objects may set render and staticPaths, with static parameters expanded only for declared values. Dynamic parameters, mixed slug segments, terminal catchalls and groups are supported. _root, _layout, top-level _slices/_interceptors/_api, and _actions/_components/_hooks directories are excluded. Custom roots/globs/options, component/path overrides, unknown config, optional parameters, wrappers and re-exports are unsupported; no navigation is inferred. A waku.config that only adds Vite plugins, as the official templates do, keeps discovery on; srcDir, basePath or any other Vite option turns it off.
Vike recognizes local named default or Page ES-module exports in JS/TS +Page files when a default-root Vite config registers option-free vike(). Filesystem URLs remove complete pages, src, index, renderer and group segments; configuration inheritance removes only pages/renderer. The nearest inherited literal +route wins and @ parameters become named graph parameters. About source, product override. Canonical extends: vikeReact and literal arrays from vike-react/config are accepted; its 0.6.26 config affects rendering rather than paths. Dynamic/ambiguous route overrides, configured Page/root overrides, unknown extensions and metadata do not receive guessed paths; onBeforeRoute blocks app-wide inference. Custom roots/plugin options, inherited Page-only targets, anonymous/wrapped/re-exported components and non-JS/TS template files are unsupported. No navigation is inferred.
Qwik City recognizes default src/routes/**/index.{js,jsx,ts,tsx} with option-free qwikCity() in literal Vite configuration, including direct-return synchronous/async config callbacks. Pages additionally require imported QwikCityProvider/RouterOutlet in the default root component. Named functions, named component$ bindings and anonymous default component$ calls have exact roots; anonymous component symbols own only their callback's existing file-level references. Official page, API fixture. Groups, legacy __ directories, dynamic/mixed parameters, catchalls and default trailing slashes follow 1.20.0. Index method exports create endpoints without implicit HEAD. Layout methods and onRequest remain middleware, not independent endpoints. Custom roots/options, route rewrites, layout-override index names, optional parameters, Markdown/MDX, re-exports and arbitrary component wrappers are unsupported. No navigation is inferred.
SolidStart supports default src/routes/**/*.{js,jsx,ts,tsx} with option-free solidStart() in a literal Vite config. Page discovery additionally requires imported FileRoutes directly inside the imported Router in the default app component. Named local default functions and method exports bind exact targets. Raw file hierarchy determines page layouts before route groups are removed; API-only descendants do not turn pages into layouts. Dots stay literal, optional page parameters and named catchalls are preserved, and GET supplies HEAD unless explicitly exported. Official page, API fixture, route construction. Version 2.0.4 excludes OPTIONS-only APIs and rejects optional API parameters. Custom roots, plugin options, dynamic configuration, page route overrides, non-default routers, anonymous/re-exported handlers and Markdown are unsupported. No navigation is inferred.
Solid Router reads imported Router/Route JSX and registered literal or module-constant config trees. It composes bases and nested paths, removes parent splats, and emits only leaf pages. Components bind through imports or same-file declarations; static solid-js lazy imports bind named default exports. Pinned example, path composition. Tests cover shadowed imports, mutated tables, exact targets, edit/delete sync and fresh workers.
Route tables in their own file are read when they are exported top-level consts typed RouteDefinition[] (annotation, satisfies or as, with RouteDefinition imported from @solidjs/router), as in the official template. Their routes carry a solid-table: marker in qualifiedName. The resolver's postExtract follows each import of the table to where it is registered: a <Router base> child, a <Route path> child, or children: table in a route object, through [...table] array spreads, local const tables and tables that are themselves registered from another file. Uses of a name an inner parameter or declaration shadows are not registrations. A table nested in another exported table of its own file is read through that table only. One distinct prefix renames the table's routes; none, several, a non-literal path, or props spread onto the registering element leave the paths as the table declares them. Deleting the registering file restores them on the next sync. Lazy components also accept @/, ~/ and #/ sources, which bind when the project's path aliases resolve them, and an async () => await import() callback. Validation (routes before → after; every new route resolves to its page component):
| Repository | Pinned | Routes | Shape |
|---|---|---|---|
| solidjs/solid-site | 0028cfd5 | 0 → 14 | routes.ts table registered by <Router>{routes}</Router> in App.tsx |
| weblink | d51f9f72 | 3 → 6 | satisfies RouteDefinition[] default export; @/routes/... lazy aliases |
| CatColab | 8f94f126 | 10 → 14 | help/routes.ts nested under path: "/help" in App.tsx's local table |
| Pentive | 91b50ee4 | 7 → 15 | lazy(async () => await import(...)); registered through a service container, so no prefix |
Explore probes that name the route path (/blog/:slug, /help/guides/:id, /share) return the page component; question-phrased probes can miss it, a ranking gap that is not specific to Solid. CatColab's lazyMdx(...) pages stay unlinked, since the wrapper is project-specific.
Other router variants, untyped cross-file arrays, re-exported tables, local function config bindings, dynamic declarations, object spreads inside route entries, inline/anonymous components and lazy re-exports remain unsupported. No navigation is inferred.
Analog recognizes src/app/pages/**/*.page.ts named default classes when a root Vite config registers the platform plugin and source registers option-free provideFileRouter(). Directory hierarchy determines layouts before dots become URL separators; index/pathless names, parameters and catchalls follow the pinned conventions. Official page fixture, route construction. Fresh workers and config/file add/edit/delete refresh existing pages, including after reopening. Custom roots, extra route directories, app/routes, metadata overrides, router options, optional catchalls, Markdown, anonymous defaults and re-exports remain unsupported. No navigation is inferred. The Vite config may be an object or a defineConfig callback that directly returns one, as the create-analog templates write it; prerender.routes is allowed.
RedwoodSDK follows imported defineApp registrations with literal/local-constant arrays, route, index, render, layout and prefix. Handler arrays bind only their last handler. Standard method tables remain method-qualified; ordinary handlers remain ANY unless the handler returns JSX. Pinned worker fixture, router semantics. Tests cover handler classification changes/deletion and fresh workers. Cross-file route arrays, custom methods, mutated builders, dynamic paths, duplicate/computed/spread method tables and wrapped/anonymous exported components remain unsupported; no navigation is inferred.
Remix default app/routes/ conventions and React Router configs registering an imported, option-free flatRoutes() call support JS/TS pages and immediate folder/route modules. The call may carry one satisfies/as type (export default flatRoutes() satisfies RouteConfig;, the official template form; supabase's apps/lite-studio writes it too). Pinned filename parser: dot nesting, index/pathless segments, parameters, optional segments, splats and bracket escapes. Resource-only and direct Outlet-only defaults are excluded. Config-only full/scoped sync and reopening an index refresh existing pages. Custom configuration, folder index fallback, Markdown/MDX, anonymous defaults and re-exports remain unsupported. A ... spread in a root remix.config, react-router.config or vite.config file counts as custom configuration and turns file routes off, except a spread of a call (...mdx(…)) inside a plugins list, which only builds a plugin and carries no route settings; a spread of anything else there, and one inside a plugin call's arguments, still counts. React Router's own RSC playground (...mdx(…) in a Vite plugin entry) therefore gets its 12 JS/TS page modules (7 default exports, 5 ServerComponent-only) linked to their components, from 0 before; its two pathless layouts and one MDX route stay without one.
A route module renders its default export, or, in RSC framework mode, its ServerComponent export when it has no default (export function ServerComponent, export const ServerComponent = …, export { Page as ServerComponent }). This holds for file routes and for app/routes.ts modules alike; a module with both keeps its default, and a ServerComponent re-exported from another module is not followed.
React Router framework mode reads default exported literal arrays in app/routes.ts or app/routes.js, using imported route, index, layout, and spread prefix helpers. Module paths bind named default (or RSC ServerComponent) components; nested index pages take precedence over their parent. Official source fixture: seven expected pages, verified through indexing and navigation. Tests also cover module/config sync and a fresh compiled process using parse/resolver workers. Custom app directories, computed arrays, relative, anonymous defaults, and re-exports remain unsupported.
TanStack Start reads imported createFileRoute calls assigned to exported const Route: literal server.handlers tables, including ANY, and the destructured createHandlers callback form. Named handlers and direct inline calls bind through the existing HTTP reader. Page/API combinations retain both nodes; server-only routes do not become pages. Pinned official handler syntax, executable middleware fixture. Computed/spread tables, member handlers, custom factories and server update chains remain unresolved. createServerFn has no declared public route and gets no fabricated endpoint.
Shared machinery all eight 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.
A routes array is read where it is declared, so an untyped array in a file
that does not import @angular/router is not one, even when another file
hands it to provideRouter. The fork adds to upstream's reader: provideRouter
and RouterModule imported under an alias ($-prefixed ones included) still
open a routes array, and an entry with a named outlet (/(side:x), not a
path) or a ...spread (fields the scan cannot see) names no screen.
angular-routes.test.ts covers those, sync after reopening, and fresh parse
workers. Since that verification, a lazy NgModule's routes are also found
through a routing module in the module's own directory, a barrel in front of
it, or the routes file it hands forChild. angular-router.test.ts covers
that on fixtures; no corpus has been re-checked, so the date above stands.
Added 2026-09-06 in frameworks/http-routing.ts: Hono, Elysia, Fastify,
Hyper-Express, Koa router, H3, Bun, Effect v4 and option-free Vixeny builders.
These produce method-qualified endpoint nodes, named-handler references and
direct calls from inline handlers. They do not add Screens navigation.
http-routing.test.ts covers all nine through full indexing and imported-handler
resolution, as well as false-positive controls, prefixes and same-file mounts.
Nuxt file routing in frameworks/vue.ts includes root index pages, index
folders anywhere in the path, Nuxt 4 route groups, server method suffixes,
server/routes/ and server catch-all segments. Each page route calls the component
extracted from the same file. page-component.ts resolves that exact file's
component, including scriptless pages, without choosing a same-named component
elsewhere. Astro pages use the same call contract. nuxt-routes.test.ts checks
extraction and imported-handler resolution; the existing Next Pages/App Router
and Vue navigation tests remain controls. The older coverage rows above retain
their recorded measurements.
Untouched official source checks (routes and indexing, no application execution):
| Framework | Pinned source | Scope |
|---|---|---|
| Hono | examples basic | 16 registrations, including 3 mounted paths |
| Fastify | winston logger | GET /hello |
| Elysia | CORS example | POST / |
| H3 | router example | 6 explicit methods at / |
| Bun | serve route tests | Extraction of 9 declarations starting in lines 1–135 |
| Effect v4 | HTTP server tests | Extraction of 3 declarations starting in lines 1–90 |
These are bounded fixtures, not whole-framework recall measurements. Hyper-Express, Koa and Vixeny have synthetic indexing tests only: the inspected official fixtures import relative framework source, which the package-provenance reader deliberately does not infer. Dynamic paths, cross-file mounts, runtime mutation and plugin factories remain outside coverage, except for Fastify plugin files.
Fastify plugin files (added 2026-09-28). A default-exported function
(export default, module.exports =, or a top-level const or function
declaration either one names) is a plugin when its first parameter is
fastify or the file names a Fastify package or FastifyInstance/FastifyPlugin*
type. Routes declared on that parameter carry a fastify-plugin: marker in
qualifiedName; postExtract then applies literal register(AutoLoad, { dir })
registrations, following @fastify/autoload 6.5.0
(source):
directory prefixes, index files hiding their siblings, autohooks files, the
default ignore of dot-named entries,
literal options.prefix, routeParams, appendAutoPrefix,
dirNameRoutePrefix: false and literal autoPrefix, prefixOverride,
autoConfig.prefix and autoload = false exports. Options forwarded from the
caller (opts, { ...opts }) are taken to carry no prefix. fastify-plugin
wrapped exports, route-object exports, filter/pattern/maxDepth/encapsulate
options, a prefix on the context that registers autoload, and dir values
other than join(__dirname | import.meta.dirname, 'literal'…) are not modelled,
nor is an export { autoPrefix } clause; those files keep their in-file paths.
The file scan for registrations records content-only skips, so a sync re-reads
only files that changed, and deleting the registering file restores the paths. Files autoload does not load keep theirs too,
since postExtract can rename a node but not remove it.
| Repository (pinned) | Routes before → after |
|---|---|
| fastify/demo | 1 → 15 |
| riccardoperra/codeimage | 4 → 16 (/api/v1/project/:id/clone from options.prefix and routeParams) |
| jamcalli/Pulsarr | 12 → 173 (159 route calls under src/routes) |
| pingcap/ossinsight | 71 → 106; 8 GET / routes renamed to their mounted paths |
Explore probes: a bare route name (DELETE /v1/approval/requests/:id on
Pulsarr) and the demo's upload question return the route file first. In
ossinsight, Next.js handlers named GET outrank a full route name in
unfiltered search; that ranking gap is shared by every framework's routes. Vixeny options require terminal-operation
dataflow and are omitted. See the route guide
for the supported declaration shapes and Nuxt configuration limits.
Ordered by cost-to-value. Each row says what is missing, not merely that something is.
Default .astro pages bind exact same-file components; .ts/.js exported HTTP methods bind handlers (ALL becomes ANY). Literal/bound anchor destinations and Astro.redirect link to local pages. External URLs, non-href attributes, type-only exports and underscore-prefixed paths are excluded. Pinned endpoint fixture, extension rules.
Remaining: custom roots/base/config redirects, Markdown/MDX pages, cross-file re-exports, client transition calls, and navigation to rest routes. These are not inferred from default file conventions.
Fifteen frameworks have route nodes and no navigation: Django, Flask, FastAPI, Express, NestJS, Laravel, CodeIgniter 3, Drupal, Rails, Spring, Play, Gin/chi/gorilla, Axum/actix/Rocket, ASP.NET, Vapor.
Route reading for three of them was widened on 2026-09-30 (upstream #2154 to
#2156): Flask reads add_url_rule(…) and a project helper that hands a list of
paths and a view_func= over (__tests__/flask-url-rules.test.ts; flaskbb 0
→ 103 routes); Rails reads routes.rb as nested blocks, so namespace,
scope, nested resources and member / collection add their paths and
controller modules, and a namespaced controller#action reaches its own
module's controller (__tests__/rails-scoped-routes.test.ts; solidus 0 →
319 route links); Play detects a conf/routes in any subdirectory
(__tests__/play-nested-projects.test.ts; play-samples 0 → 139 routes). The
numbers are upstream's measurements. None of these adds navigation.
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 |
| CodeIgniter 3 | redirect('users/show') |
a controller path, which routes.php or the convention maps to a route |
| 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.
| 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.
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.
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.
Each of these cost real debugging time; they are not hypothetical.
- A
referencesedge cannot cross a language family. The kernel's language gate (apply_language_gateincodegraph-kernel/src/resolve/names.rs) filtersreferencescandidates tosameLanguageFamily, so a.jsrouter config can never name a.vuecomponent — it silently binds to a same-named.jsfunction in a store instead. Bind a route to its component withcalls, whichroute-roots.tsreads as "the page a screen file exports". - One address, one screen. A layout and the index route beside it resolve
to the same path (
+layout.sveltevs+page.svelte,dashboard.route.tsxvsdashboard.index.tsx,_auth.invoices.tsxvs_auth.invoices.index.tsx). Emitting both puts one address on the map twice. Decide per file — the sibling is not visible at extraction time. - 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 beforeRootedRouteTable/routesForFile. Therootslist decides only whether to resolve. - Read fields from the object, not from a window around one. A Vue route's
nameis written above itspath, so a text window handed every entry its predecessor's name — silently, for every route in the file. Useframeworks/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'spages/convention belongs to a Nuxt app only — a plain Vue app'spages/folder (halo's console) holds components a router config names. - A receiver is required for a generic verb.
pushandreplaceare two of the most common method names in JavaScript; claiming a bare one puts everypaths.push('/tmp/x')one string-match away from a route. - One component can be several screens. A listing rendered at
/,/search/:keywordand/page/:nis one component and three addresses;screenOfComponentmaps a component to every route it serves, andcollapseSharedChromecounts 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. - A destination can name several routes.
parseHrefExpressionreturns oneHrefLiteralcarryingalternates, anddestinationsForHrefturns it into one{ node, href }per arm. A synthesizer emits an edge apiece; a resolver puts the first on theResolvedRefand the rest inalsoTargets, whichcreateEdgesfans 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. - Read markup with the same reader as calls. A synthesizer that peeks at
the first character of
to={…}misses every conditional and template thepush(…)path handles. UseparseHrefExpressionon the balanced brace contents. - A condition is read the same way for markup as for a call. The Screens
walk used to skip the
whenon any synthesized edge, so every<Link to>read as always while thepush()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;returnbelongs only to an edge whose destination came from elsewhere, which is whatregisteredAtpointing at another line means. - 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/usersfromserver/api/), and is excluded by file path. - Detection runs before any file is indexed.
declaredDependenciescaches 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. - 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 inweb/ui/mantine-ui/under a workspace root atweb/ui/that declares only tooling.declaredDependenciesreads 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 askdependsOn, not read apackage.jsonitself, so it sees the same manifests every other detector does. - 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 fromApp.jswithAppRoutes.map(({ element, ...rest }) => <Route {...rest} element={element} />); codedthemes' admin kits put one route object per file and list them increateBrowserRouter([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 inrunPostExtracton every index and sync, removals included): it starts from the files that import React Router, takes only auseRoutes/create*Routerargument or a.mapwhose callback renders a<Route>from the item's own fields ({...route},{...rest},path={route.path}— neverpath={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. A variable that holds a call's result stands for the table the function returns: Medusa'sconst routes = getRouteMap({ … })inside a class'srender(),export const routes = buildRoutes(),export default buildRoutes(),await loadRoutes(). Which declaration a name means is the one in scope where it is used, decided by the graph's functions: another function'sconst routes = getMenu()is that function's own, and an exported name is a top-level one. A function-local declaration has no node, so the declarations are read from the text and only the scope comes from the graph. What the function's parameters bring in (Medusa's...coreRoutes, from plugins) is the caller's to fill and stays unread. What a function returns is everyreturnof its own, an earlyif (!user) return [];and each branch alike, and never one of a function written inside it: the firstreturn [anywhere in the body was a.forEachcallback's[]in devanshthakker15/BANKFORM1'sgenerateRoutes, and could be a helper's menu. A function that returns another's table (return buildRoutes()) is followed, and so isuseMemo(useMemo(() => getRoutes(user), [user]),useMemo(getRoutes, [])). The body is scanned as text, so a quote right after a word is JSX text's apostrophe (<p>Don't</p>), not a string. - 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'sShopindex 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 afterelement={, which hid every long element; and read<Outlet />as rendering nothing of its own, in a route object too (element: <Outlet />,Component: Outlet, alazyloader handing back{ Component: Outlet }), as a route with no element renders one: taking it for the page made every route inside alayout:Outletthat links nothing, and a loader returning it fell back to the module it loads for itsloader, drawing Medusa's product page around its variants. Such an object is still a route at its own address, inside the layouts around it, unless a page inside claims the address or it sits at its parent's (path: ''), where it only groups its children. 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. resolve()sees only references written in the framework's own languages (getResolvingFrameworks): Express'sloggermiddleware rule once took etcd's Go*zap.Loggerresult types for a method. A navigation written in markup (.svelte,.vue) needs that language inlanguages. A resolver that reads a language it extracts nothing from lists it inresolveLanguagesinstead — Svelte's$lib/…imports in.tsroute modules, ASP.NET's Razor@model— because wideninglanguagesalso runsextract()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.- 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
pushornavigate. Set the resolver'snavigation:tails, the method names its calls end in, andscope, 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.tsruns all seven routers through it, and asserts each router's calls end in a declared tail. - 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,/userstayed on itsUserPagedeclaration aftersrc/pages/user.tsx(Page) came back. A router whoseresolve()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 thejsx-renderedges, 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:pagesfor@/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). - A lazy loader is code, and its page is what it hands over. A React
Router
lazyresolves to the route's properties, soconst { TestTab } = await import('./tabs'); return { Component: TestTab }rendersTestTab, not the module's default export; reading the firstimport('…')in the loader linked luci-go's tab routes to nothing and other apps' routes to another page of the same module (lazyRouteReferenceinframeworks/react.tsfollows the loader's bindings to itsComponent, or the one lazily imported export itselementshows 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 apackage.jsonwould let it. - A name a file declares is that declaration, wherever the file uses it.
JavaScript scoping makes
<RegisterPage />, a route'selement: <RegisterPage />, alayout:and a TSX type all theconstthe file declares under that name, so every one of them asksdeclaredComponentfirst (Pattern 1 ofreactResolver.resolvefor any PascalCase reference in a.tsxor.jsxfile,layoutComponent, and thejsx-rendersynthesizer'sjsxChild), 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'sRegisterPage. What the value renders is read from its initializer (valueLoader, byloaderValue): the module a lazy loader loads or the export it picks, in either branch of a conditional (SigNoz'sIS_DEV ? React.lazy(…) : null); the function it wraps (observer(function Settings() {…}), SigNoz'sReact.memo(TableBody, areEqual), mastodon'sforwardRef(ComboboxWithRef) as {…}); else the declaration itself (const DialogPortal = DialogPrimitive.Portal,createContext(…)). A module whose default export isobserver(Login)hands overLogin(defaultExportedName); whether a module's default export is a binding it imports (export default Page;) is read from its binding rows. A tag's edge depends on the modules the file's lazy values load; the fork re-runs synthesis on every sync that changes a file, so a sync that gives one its default export redraws the tag, as a fresh index would draw it. Type positions use TypeScript's type namespace. Intype User = …; const User = lazy(…), a type annotation binds to the type alias while a JSX tag follows the lazy value. A lexical type declaration shadows an imported type only inside its enclosing scope.typeofretains value lookup. - A tag imported from the project is what its module exports under the
name, and a tag in a comment or a string is no tag.
jsxChildasksexportedComponent(the lookup a lazy route's module goes through) for a name the file imports from a module of the project: through anindexor barrel that hands the component on (import Sidebar from './App'; export default Sidebar), past a constant that holds another binding (const Avatar = AvatarWithHoverCard; export { Avatar }) and past a wrapper call (observer(AppSidebar),memo(Blurhash)). A module that exports no component under the name leaves the tag with no edge rather than a namesake elsewhere. The lookup reads the binding rows for what a file exports, as in trap 19. The JSX pass also reads each file once throughjsxCode(resolution/jsx-code.ts), which blanks comments, strings and templates, so react-router's "cannot render a inside another " is no render; and it skips.tsfiles, which hold no JSX. Upstream's sync input list for these modules (importedTagModules) is not taken: the fork re-runs synthesis in full on a sync. - An import that looks like a package can be a path. Create React App
and Vite admin templates (berry, mantis, matx) set
"baseUrl": "src"or"paths": { "*": ["./src/*"] }in a jsconfig and write every cross-file import aslayout/MainLayoutorpages/auth/Register. Taken for npm packages, those imports linked nothing: a route table imported that way was never read, and alazy(() => import('views/x'))route bound to the localLoadable(…)value instead of its page. The import resolver now reads such a specifier through the importing app's own config and binds it only to an indexed file (fileExistsalso answers for folders andnode_modules). A package that app'spackage.jsondeclares, or a Node built-in, never reaches a project module that way, only a.d.tsthe project keeps for it ("*": ["./typings/*"], as tsc reads it). A route resolves its lazy module as TypeScript whatever the file is written in, which is why the TypeScript extension list probes/index.jsxlast. - A navigation call named like a function must not fall back to one.
Nuxt auto-imports
navigateTo, so a call to it is written bare, with no import to say where it comes from. When Vue Router can't read where it goes — a computed destination, a path no page serves, an external URL — the reference would reach the name matcher, which links any project function of that name: elk declares anavigateToof its own inside a plugin. The Vue resolver used to sink everynavigateTointo an edge to its caller, which hid the navigation too, and which sync never revisits (trap 16).FrameworkResolver.ownsReferencenames the references a framework answers alone:nuxtResolverowns a barenavigateTothat no declaration or import in the file binds, and one no router links stays unresolved, so a sync binds it when its page appears. An absolute URL is no path of the app: Vue Router's call reader keeps only destinations that start with/, as its markup reader does. - What runs before a route has no import to it. Next wires its proxy
(
proxy.ts,middleware.tsbefore 16) by file name andconfig.matcher, sonextProxyEdgesdraws acallsedge from each route the matcher accepts to what runs there.callsbecause that is whatcallersand Flow ride: a route stands for a request arriving, and the proxy is the first thing that request runs.decoratesreads as well, and leaves the proxy with no callers and every flow stopped at the route.routeRootsskips these edges, since they don't say where the route's own code starts, and so does the Screens walk (arrivesFromAnotherContext): walked backwards from a redirect in the proxy, 30-odd routes are more callers than it takes, and the navigation was dropped. Screens stays byte-identical on all four apps. Two things decide whether this reaches real apps at all. About two thirds of real matchers are a negative lookahead (/((?!api|_next/static).*)), which a reader of literal paths links nothing from, and about a third of real proxies are a wrapped default export (export default clerkMiddleware(…)), which has no node, so the edge goes to the file, where the wrapper's calls are recorded.compileMatcherbuilds its regular expression from the parts it recognizes and never from the entry's text:new RegExp()on a pattern in an indexed repository could hang the index. Checked against path-to-regexp on the 344 entries of 157 real proxies: 339 read, all agreeing on 27 addresses, and 5 left unread, which link nothing.
Per CLAUDE.md's validation methodology, and what was actually done for the
four routers added on 2026-08-29:
- 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.
- Recall against ground truth.
grepevery navigation site in the source and account for each one: resolved, or correctly unresolved because it is computed. - Precision, site by site. For every synthesized edge, read the line its
registeredAtnames and confirm the tag or call there names that destination. Target: zero false positives. - Controls re-indexed. Node, edge, route and
navigatescounts 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. - Full suite green, and a CHANGELOG entry in the user-facing voice.
# 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-- 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 screenFramework name heuristics share the native resolver's lexical and cross-file visibility checks. They prefer the calling file and applicable package/directory, and refuse unrelated function-local declarations, nested types and test-suite symbols from production code. Rust heuristics also honor module/import reachability. React and Express naming conventions require imports to reach another file. NestJS provider lookup also supports convention siblings in the same directory. Vue/React conventions accept only script references; Astro component-name conventions accept only .astro references. Inline HTTP handler references retain simple dotted receivers, including optional access; calls on arbitrary returned expressions remain unresolved.
Interface dispatch synthesis links supported base methods to concrete overrides. JavaScript and TypeScript constructors are excluded because construction does not dispatch to descendant constructors.
Express uses the last registration argument as the handler, excluding a trailing comma. Inline arrows and function expressions contribute their body calls, including handlers inside wrapper calls and chained router.route(path) registrations. An earlier inline middleware does not replace a named final handler. Regression coverage is in __tests__/express-inline-function-handler.test.ts.
extraction/sfc-script.ts folds Vue, Svelte and Astro script results into one
whole-file node containing the component. Top-level script members have the
component as their only parent; nested symbols retain their script parent. Vue
<script setup>, Svelte instance scripts and Astro frontmatter
assign top-level calls and references, including constant initializers, to the
component. Imports, module-level execution and Astro browser scripts remain with the file. Svelte
recognizes context="module" and <script module>. The native embedded-block
seam preserves literal metadata and rebases binding scopes and lines to full-file
positions. sfc-component-owns-script.test.ts covers hierarchy and execution
ownership.
Vue interpolation, directive and event expressions link to visible script bindings. Template-local names and v-pre regions stay excluded; script and component ownership remain distinct. Named object literals in component scripts retain their own function members.
Axum and Actix route handlers may appear below the route registration line or inside nested call arguments. Handler references keep the actual argument position. Actix scoped paths and per-resource registrations retain their existing prefix rules.
frameworks/ruby.ts reads literal only: and except: action filters for both
resources and resource. Supported forms are symbol/string arrays, a single
symbol or string, %i/%w lists with brackets or parentheses, and hash-rocket
options such as :only => [:index, :show]. These filters restrict which REST
actions receive routes; namespaces, scopes, nested resources and member/collection
blocks keep their existing path and controller rules.
Laravel reads string Class@action handlers and string resource controllers,
including namespace paths under the controllers root. Resource options follow
the controller argument. Exact written paths distinguish same-named controller
classes; ambiguous or missing namespace targets remain unresolved.
laravel-controller-strings.test.ts covers these through full indexing.
Vapor route closures contribute direct body-call references from the route,
retaining each written call site and its argument labels. Route-call receiver
inference respects local and conditional bindings; unsupported loop, guard,
pattern and capture bindings stay unresolved instead of reusing an outer type.
Named use: handlers keep their existing typed
resolution. vapor-closure-route-body.test.ts checks closure ownership and
false-positive controls through full indexing.
React Router lazy modules follow default or Component exports through local named and wildcard re-exports. Local exports take precedence; ambiguous wildcard exports and re-export cycles stay unresolved. JSX component lookup follows its import and excludes names bound as local values or parameters.
Regression coverage: __tests__/react-router-lazy-barrel.test.ts, __tests__/jsx-child-disambiguation.test.ts and __tests__/sync-jsx-import-forwarding.test.ts. These deterministic cases do not establish a new agent-evaluation or performance result.
JSX child rendering preserves tags after plain text and line breaks. The lexer blanks child text without shifting source positions; type arguments and quoted examples remain excluded. Regression coverage is in __tests__/jsx-child-disambiguation.test.ts.
React component declarations retain the function or value that owns their
initializer. JSX outside a function still links from its enclosing value or
file. Wrapper calls such as memo, forwardRef and observer retain the
wrapped declaration. A module import must resolve to an exported component;
a same-named declaration in another module does not supply a missing export.
Express route extraction reads balanced arguments, including arrays, inline
callbacks and trailing commas. Only handler arguments supply route targets.
Strings, comments and route-path arguments do not become handlers. These
contracts are covered by react-wrapper-initializers.test.ts,
jsx-render-outside-function.test.ts, react-own-declaration.test.ts and
express-handler-arguments.test.ts; they do not establish whole-framework recall.