Last verified: 2026-08-29 (Angular row: 2026-10-06; React Router row: 2026-10-07) 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.
Seven. Each reads a literal destination and leaves a computed one, a path no route serves, and a conditional whose arms disagree unresolved rather than guessed.
| Router | Resolver | Markup synthesizer | Tests | Validated on |
|---|---|---|---|---|
| Expo Router | frameworks/expo-router.ts |
expo-router-synthesizer.ts |
expo-router.test.ts |
— |
| Next.js | frameworks/nextjs.ts |
next-router-synthesizer.ts |
nextjs.test.ts |
next-saas-starter |
| React Router | frameworks/react-router.ts |
react-router-synthesizer.ts |
react-router.test.ts |
proshop (44 edges), proshop-v2 (28), react-redux-realworld (22), react-boilerplate (styled(Link)), takenote (v5 <Redirect>), bulletproof-react (nested children, lazy routes, paths.x.path constants through an app's own tsconfig alias: 0 → 9 named, linked routes), and its paths.x.getHref(id) links and navigate(paths.x.getHref()) calls (0 → 11 navigates). Route tables another file hands the router (tableRoutes in frameworks/react.ts, trap 13): the ASP.NET Core React template's AppRoutes.map(… <Route {...rest}>) on jasontaylordev/CleanArchitecture's ClientApp-React (0 → 6 routes, 11 navigates), replaysMike/Binner (0 → 46, 87), GavinLonDigital/RankingApp, GarrettHays/PaydirtPickem, charlessolar/eShopOnContainersDDD; useRoutes(routes) from another file and [...sessionRoutes] on uilibrary/matx-react (0 → 7, 16); codedthemes' createBrowserRouter([MainRoutes, LoginRoutes]) on berry and mantis (9 and 8 routes, no navigation they can resolve); a table .mapped inside <Route path="/dashboard"> on RADeveloping/chickadeeinvest; useRoutes([...]) in place on minimal-ui-kit/material-kit-react (0 → 7, bound through lazy(() => import(…)) to each page). JSX index routes and layouts (trap 14): proshop-v2's createRoutesFromElements (/ is HomeScreen inside App, not App; 30 layout edges, the path='' guards included; Screens 30 → 74 transitions, the guards' redirects drawn from the screens they guard), chickadeeinvest's table inside <Route element={<DashboardLayout/>}> (its 4 routes get the layout), React Router 6.29's own examples and tests, refine's 281 examples (one / per app, 191 layout edges, 21 new navigations), crwn-clothing, the vanlife course app, volun-mern (105 layout edges, six levels deep), spotify/vispana, react_persist_login, chushi (path={"agents"}: 2 → 6 routes), hathor-explorer (path="" at the root), cboard, react-router-auth-v6: 578 of 581 new layout edges name a component of an enclosing <Route> (vanlife's 3 inherit a default-import resolution bug that already misbinds its pages), 27 of 27 new navigations precise, no navigation lost, 16 control repos byte-identical |
| TanStack Router | frameworks/tanstack-router.ts |
tanstack-router-synthesizer.ts |
tanstack-router.test.ts |
TanStack examples, fastapi-template frontend |
| Vue Router / Nuxt | frameworks/vue-router.ts (Nuxt file routes: nuxtResolver in frameworks/vue.ts) |
vue-router-synthesizer.ts |
vue-router.test.ts |
vue-realworld (23 edges); vue-element-admin (62 routes), vue-admin-template (14), vben (192), halo console (34) — named tables, module files, children + layouts; Nuxt: mealie, elk, nuxt/movies |
| SvelteKit | frameworks/sveltekit-router.ts |
sveltekit-synthesizer.ts |
sveltekit-router.test.ts, sveltekit-route-names.test.ts |
sveltekit-realworld (31 edges); shadcn-svelte and skeleton ((group) layouts: 13 and 23 edges), svelte.dev (74), kit's test apps (47) |
| Angular | frameworks/angular-router.ts |
angular-template-synthesizer.ts |
angular-router.test.ts |
angular-realworld (31 edges, 18 renders, 32 bindings), Ghostfolio (189 edges, 170 renders, 215 bindings), ngx-admin (routes and renders; its menus are config; 13 bindings), CleanArchitecture's src/Web/ClientApp (signals; 12 bindings), angular-spotify (Nx libs behind barrels: 14 routes), jira-clone (class-constant paths, mount-only redirects), jhipster (60), ionic-conference (18), Angular-JumpStart (18) |
Shared machinery all seven use, in frameworks/expo-router.ts: RouteTable /
RootedRouteTable, routesForFile, addRouteTo, matchRoute, appRootFor,
parseHrefExpression, readHrefViaLocal, nthArgumentText, readStringAt,
toHref. Plus pageForHref in frameworks/nextjs.ts (framework-agnostic
despite where it lives) and the object-literal walker in
frameworks/object-literal.ts.
Angular is the one whose markup is not indexed: a component's template is a
templateUrl file (or an inline template: string) read at synthesis time,
which also yields the component tree (<app-foo> by element selector) — the
edge a navigation in a child component rides to its screen. A routerLink:
field written in a component's class (a tab bar's or a menu's config, bound
in a loop elsewhere) counts as a link from that component. An event binding
((click)="save()") is a calls edge from the component to its own method
carrying metadata.trigger, which Steps uses in place of reading a trigger
at the edge's line (the binding is in the template, not the source there).
A property binding, an interpolation, a structural directive or a
control-flow block that calls one of the component's own members
([name]="icon()", {{ label() }}, *matRowDef="let row; columns: displayedColumns()", @if (loading()), @let total = price();) is a
calls edge too (angular-binding, the binding as via), with no trigger:
it runs when the template renders, so Steps folds it into the screen rather
than drawing a handler the user fires — a trigger there drew computed
signals as user actions. A member read without a call counts only when it is
method-kind: a getter is called, a method handed to a child
([displayWith]="displayFn", trackBy: trackById) is a references edge
with fnRef, and a field a call filled (days = Array.from(…)) a plain
references. A property read ([value]="title") links nothing, as in
TypeScript, which records no property reads either; on angular-realworld,
ngx-admin, Ghostfolio and CleanArchitecture it would have added four times
the edges and taken nothing off the Dead code list, which never asks about
properties. A pipe, an object key, a
microsyntax key (of, trackBy:, index), a template's own names (let item, as user, #ref, @let x, let-row) and a commented-out binding
link nothing. A route with
children is a layout: its component carries a references edge marked
layout: true from each screen nested in it, and routeLayouts in
route-roots.ts gives Screens every screen a layout serves. Known limits: a
route with a custom matcher has no static address, a relative navigation
(relativeTo) is left unresolved, an edit to a template file alone is
picked up at the next sync of any source file (templates are not watched), a
member a component inherits from a base class is not linked from its
template, and a template's local hides a same-named member for the rest of
the template rather than only inside its block.
Registered Tauri string commands and Rust/JS/TS bridge dispatch are covered by the shared string-contract pass. Generic calls and local wrappers retain registration evidence; generated fragment groups are matched only after content verification. Fixture and Codex3N validation are recorded in personal verification; broad framework recall and agent A/B are not claimed. The current contract is string contracts.
The current worktree extends existing Vue/React support with imported useRouter
aliases, explicit returned composable members, Vue literal emits and callback
props, and React named callback props. These edges retain heuristic provenance
and registration sites. Static identifier props also expose value dependencies.
Computed destinations, dynamic components, spread props, inline callback bindings
and runtime event names remain coverage gaps and are labelled in queries.
Fixture validation is recorded in component-bindings.test.ts; real-repository
recall/precision and agent A/B have not been measured for this change.
Ordered by cost-to-value. Each row says what is missing, not merely that something is.
Has: src/pages/ file routes (.astro pages + .ts endpoints,
[param]/[...rest]), in frameworks/astro.ts.
Missing: navigates edges. Astro is an MPA — navigation is a plain
<a href="/about">, plus Astro.redirect('/x') in frontmatter and
redirect entries in astro.config.
Size: smallest job on this list. sveltekit-synthesizer.ts's
svelteKitLinkEdges is the same pass over the same tag against a different
table; the resolver half is one Astro.redirect reader.
Validate on: any withastro/astro example, or the Astro docs site.
Fourteen frameworks have route nodes and no navigation: Django, Flask, FastAPI, Express, NestJS, Laravel, Drupal, Rails, Spring, Play, Gin/chi/gorilla, Axum/actix/Rocket, ASP.NET, Vapor.
Be precise about what is missing. redirect_to, HttpResponseRedirect,
res.redirect, PHP's redirect() are already recognised as response
effects (ui-server/api/effects.ts), so they draw as a box in the Steps
picture. What is missing is the edge to the page they name — so two pages never
connect on the Screens tab.
For a pure API this is correct and nothing should change: an endpoint is not a screen. It matters for the server-rendered half, where a classic MVC app gets no Screens picture at all today:
| Framework | The destination to read | Why it is harder than a client router |
|---|---|---|
| Rails | redirect_to :dashboard, redirect_to users_path |
destinations are named helpers (*_path/*_url) generated from routes.rb, not literals |
| Django | redirect('profile'), reverse('profile') |
same — a route name, like Vue's { name }, which vue-router.ts already shows how to index |
| Laravel | redirect()->route('home'), ->view() |
route names again |
| Spring | "redirect:/x", RedirectView |
a literal inside a string return value |
| ASP.NET | RedirectToAction("Index", "Home") |
controller + action pair, not a path — needs the route table's reverse mapping |
| Flask | redirect(url_for('profile')) |
nested call; the name is url_for's argument |
The Vue name-index (VueAppRoutes.byName) is the closest existing precedent for
all of these.
| 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.applyLanguageGateinname-matcher.tsfiltersreferencescandidates 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. A receiver other than the conventionalrouter/$routeris not enough on its own either:vue-router.tsproves it by askingstore-bindingwhatconst nav = …was initialized from, in the innermost scope at the call site, so a parameter of the same name shadows the handle and claims nothing. - 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. - Registration order is priority, and the first claim at 0.9 ends the
search.
FRAMEWORK_RESOLVERSis an array andresolution/index.tstakes the first result at confidence ≥ 0.9, so two resolvers that both claim a name resolve it by array position rather than by specificity. Nuxt'snavigateTois an auto-import inframeworks/vue.tsand a navigation verb inframeworks/vue-router.ts; withvueResolverfirst it answered at 1.0 and broke the loop, and thenavigatesedge was never drawn — the verb read as supported and was silently dead.vueRouterResolvernow runs first, which is safe only because it declines every non-callsreference. - A built-in has nothing to point at — consume the reference, draw
nothing.
definePropsand Nuxt'suseRouterare provided by the toolchain, so resolving them to the node that mentions them keeps the reference out of the failed-lookup queue, but the edge it produces points the symbol at itself and reads as "this calls itself" in callers/callees.selfResolvedBuiltin(resolution/types.ts) marks these andcreateEdgesdrops them — not a blanketsource === targetskip, which would eat genuine recursion. Resolve below 0.9 when the repo may define the same name itself, so import and name resolution outrank the built-in instead of being shadowed. Svelte (frameworks/svelte.ts) and Astro still draw the self-edge; the same marker applies there. - 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. - 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. 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 still asked of every detected framework.- 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.
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 screen