Skip to content

Latest commit

 

History

History
740 lines (639 loc) · 66.8 KB

framework-coverage.md

File metadata and controls

740 lines (639 loc) · 66.8 KB

Framework & language coverage — what is done, what is left

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.


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)

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.


Static JavaScript HTTP declarations

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.

What is left

Ordered by cost-to-value. Each row says what is missing, not merely that something is.

1. Astro — custom configuration and additional page formats

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.

2. Server-rendered frameworks — a redirect is a transition, not just a response

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.

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. The kernel's language gate (apply_language_gate in codegraph-kernel/src/resolve/names.rs) 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. A variable that holds a call's result stands for the table the function returns: Medusa's const routes = getRouteMap({ … }) inside a class's render(), 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's const 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 every return of its own, an early if (!user) return []; and each branch alike, and never one of a function written inside it: the first return [ anywhere in the body was a .forEach callback's [] in devanshthakker15/BANKFORM1's generateRoutes, and could be a helper's menu. A function that returns another's table (return buildRoutes()) is followed, and so is useMemo (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.
  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, in a route object too (element: <Outlet />, Component: Outlet, a lazy loader handing back { Component: Outlet }), as a route with no element renders one: taking it for the page made every route inside a layout:Outlet that links nothing, and a loader returning it fell back to the module it loads for its loader, 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.
  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 in a .tsx or .jsx file, 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); 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. In type 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. typeof retains value lookup.
  20. 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. jsxChild asks exportedComponent (the lookup a lazy route's module goes through) for a name the file imports from a module of the project: through an index or 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 through jsxCode (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 .ts files, 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.
  21. 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 as layout/MainLayout or pages/auth/Register. Taken for npm packages, those imports linked nothing: a route table imported that way was never read, and a lazy(() => import('views/x')) route bound to the local Loadable(…) 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 (fileExists also answers for folders and node_modules). A package that app's package.json declares, or a Node built-in, never reaches a project module that way, only a .d.ts the 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.jsx last.
  22. 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 a navigateTo of its own inside a plugin. The Vue resolver used to sink every navigateTo into an edge to its caller, which hid the navigation too, and which sync never revisits (trap 16). FrameworkResolver.ownsReference names the references a framework answers alone: nuxtResolver owns a bare navigateTo that 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.
  23. What runs before a route has no import to it. Next wires its proxy (proxy.ts, middleware.ts before 16) by file name and config.matcher, so nextProxyEdges draws a calls edge from each route the matcher accepts to what runs there. calls because that is what callers and Flow ride: a route stands for a request arriving, and the proxy is the first thing that request runs. decorates reads as well, and leaves the proxy with no callers and every flow stopped at the route. routeRoots skips 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. compileMatcher builds 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.

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

# 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 screen

Name heuristics and inline handlers

Framework 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 inline handler bodies

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.

Single-file component ownership

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 template calls and Rust handler positions

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.

Rails resource action filters

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 string controller handlers

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 trailing closure handlers

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 wrappers and Express handler arguments

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.