Просмотр исходного кода

docs(changelog): highlights for the unreleased section, and a shape a reader can skim (#1665)

The section had grown to 67 entries with no summary, and the release notes
publish it verbatim — so the biggest thing in it was unfindable.

Three changes, none of which rewrites an entry:

**A Highlights block**, eight bullets, in plain language: the viewer, Screens,
Steps, the server and cross-tier coverage, the in-order reading, the new
screens (dead code, entry points, type hierarchy, trails), the six languages
that gained conditions and arguments, and the note that this release wants a
re-index.

**Fifteen entries moved from Fixes to New Features.** They were whole new
capabilities filed as fixes — `codegraph ui` itself was the thirty-ninth
bullet under Fixes, below a list of one-line corrections.

**The remaining thirty-seven fixes grouped** under four sub-headings, as the
house rules ask past fifteen, so a reader can find their area. A stray empty
`### Fixes` heading is folded into the one above it.

Every entry is moved verbatim; none was reworded, added or dropped. Verified
bullet-for-bullet, and both release scripts still promote and render the
section.


Claude-Session: https://claude.ai/code/session_012M9UE2Txyh7w8wyothDPTe

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Colby Mchenry 1 неделя назад
Родитель
Сommit
b9ca4b7981
1 измененных файлов с 91 добавлено и 74 удалено
  1. 91 74
      CHANGELOG.md

+ 91 - 74
CHANGELOG.md

@@ -12,6 +12,17 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 
 ## [Unreleased]
 ## [Unreleased]
 
 
+### Highlights
+
+- **`codegraph ui` — your graph in a browser.** A local, read-only viewer for the project you already indexed: your code with its callers and callees in the margin, a map of the whole repository, and a strip that shows how one symbol reaches another.
+- **See your app the way its users meet it.** A Screens tab draws every screen and the navigation between them, for Expo Router, React Router, Next.js, TanStack Router, Vue Router / Nuxt and SvelteKit apps.
+- **See what happens from a screen or an endpoint.** A Steps tab draws what one action sets in motion — the handlers it fires, the state it writes, the calls that leave your code, and every way it can answer — with the condition on each arrow.
+- **APIs too, and across tiers.** Endpoints in Express, NestJS, Fastify, Koa, Hono, FastAPI, Flask, Django, Spring, ASP.NET, Vapor and Gin, with a page's `fetch` following through to the route that serves it, a queued job to its consumer, an event to its handler.
+- **Read a handler in the order its code runs.** The same picture laid out by when things happen rather than by distance, so a reply sits below the token it carries. Where the code chooses, the condition is said once and each arrow answers it.
+- **Find what nothing reaches, and where everything starts.** Dead code and entry points as screens of their own, a type hierarchy showing what is built on what, and trails you can name, save and come back to after the code has changed.
+- **Conditions and arguments for more languages.** What a call passes, and the `if` / `switch` / `try` it runs under, are now read for Python, Java, Kotlin, C#, Go and C as well as JavaScript, TypeScript and Swift.
+- **Upgrading:** re-index your projects after this release — several of the new readings rest on edges that are written while indexing.
+
 ### New Features
 ### New Features
 
 
 - **A busy screen's picture is laid out by the parts of the screen.** A screen is a set of handlers with no order between them, so on a hub screen the old rows-by-distance collapsed into one enormous row — the main screen of one app put 89 boxes side by side on a canvas over 28,000px wide, every line a near-horizontal sweep across all of it. The Steps tab now groups a screen's picture by region — the component that owns each handler, named in a small caption over its boxes — with each region a column where a step sits above what it sets in motion, tiled in the screen's own source order. At rest the picture hides only two things: the screen's own fan-out — one line into each region stands in for it — and lines that point back up; every other line draws where it leads, between two regions included, and selecting a step brings out its whole story in the side panel, link by link. A box nothing points at is the screen's own doing — run on render or mount, or from a binding written inline — the key says so, and selecting it lights its line from the screen with what fires it. The same app's widest screen now lays out under 3,500px with every line local, and the whole picture fits on screen when it opens. Endpoints, handlers and the in-order reading are untouched, and nothing needs a re-index: the regions come from the same walk that draws the steps.
 - **A busy screen's picture is laid out by the parts of the screen.** A screen is a set of handlers with no order between them, so on a hub screen the old rows-by-distance collapsed into one enormous row — the main screen of one app put 89 boxes side by side on a canvas over 28,000px wide, every line a near-horizontal sweep across all of it. The Steps tab now groups a screen's picture by region — the component that owns each handler, named in a small caption over its boxes — with each region a column where a step sits above what it sets in motion, tiled in the screen's own source order. At rest the picture hides only two things: the screen's own fan-out — one line into each region stands in for it — and lines that point back up; every other line draws where it leads, between two regions included, and selecting a step brings out its whole story in the side panel, link by link. A box nothing points at is the screen's own doing — run on render or mount, or from a binding written inline — the key says so, and selecting it lights its line from the screen with what fires it. The same app's widest screen now lays out under 3,500px with every line local, and the whole picture fits on screen when it opens. Endpoints, handlers and the in-order reading are untouched, and nothing needs a re-index: the regions come from the same walk that draws the steps.
@@ -44,82 +55,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 
 - **React Native apps: Swift native modules and their events connect end to end.** A JS call like `captureView.finalizeCaptureSession()` — where `captureView` is bound to `NativeModules.CaptureView` and the module is a Swift class exposed through an `RCT_EXTERN_MODULE` shim — now resolves to the Swift method itself instead of stopping at the constant, so `codegraph_explore`, the Flow strip and the Steps view follow the code into native. Native → JS events now also land on listeners written inline (`addListener('onZipComplete', (data) => { … })`), attributed to the component that registers them. Re-index after upgrading to pick the new edges up.
 - **React Native apps: Swift native modules and their events connect end to end.** A JS call like `captureView.finalizeCaptureSession()` — where `captureView` is bound to `NativeModules.CaptureView` and the module is a Swift class exposed through an `RCT_EXTERN_MODULE` shim — now resolves to the Swift method itself instead of stopping at the constant, so `codegraph_explore`, the Flow strip and the Steps view follow the code into native. Native → JS events now also land on listeners written inline (`addListener('onZipComplete', (data) => { … })`), attributed to the component that registers them. Re-index after upgrading to pick the new edges up.
 
 
-### Fixes
-
-- **Where the app goes after login is a fork, not two always-es.** A navigation whose destination comes back from a helper — `router.replace(await resolvePostLoginRoute())` over `return (await hasSeenWelcome(…)) ? '/home/' : '/welcome/'` — drew both screens with no condition, reading as if the welcome screen always shows. The two arms share a line, and only a column can tell them apart; each synthesized edge now carries its literal's own position, so the guard reader says which arm it is: `WHEN await hasSeenWelcome(…)` → home, and its negation → welcome. And the scan starts at the helper's body, so a literal-union return type — `Promise<'/welcome/' | '/home/'>`, whose routes are string literals too, written first — no longer stands in for the navigation itself. Re-index after upgrading to pick the positions up.
-
-- **A handler called from under a binding says what it passes.** A press that runs `tryCatchSync(onClosePress)` drew a box for the wrapper and stopped — leaving the one thing a reader asks ("what is being wrapped?") unsaid, even though every other call-shaped site already prints its arguments. The panel and tooltip now say `tryCatchSync(onClosePress)` — the argument is the answer.
-
-- **A step the walk stopped at keeps its whole name.** A boundary — another screen, or a cap the walk hit — ends its name with an ellipsis by design, but the box was not sized for it, so a longer name lost its last letters instead (`/scan-to-verif…` for `/scan-to-verify …`). The anchor's start mark clipped a long path the same way (`/sheets/forgot-passw…`). The box now makes room for both.
-
-- **A screen that talks to native code keeps its own navigations.** In a React Native or Expo app, a `router.push` written inside a listener for a native event was credited to whichever screen had *started* that round trip, not to the screen the push is written on. In one app that moved seven transitions off the capture screen and onto the review screen it opens — leaving the review screen looking as though nothing in the app could reach it, stranded in the "no transition reaches this" band at the bottom of the Screens tab, and printing Swift conditions like `Thread.isMainThread` on a JavaScript navigation. A navigation now belongs to the screen whose file it is written in; an event arriving from native code, from an HTTP call or off a queue is no longer read backwards as if it were a caller.
-
-- **A link written under a condition says so on the Screens tab.** A checkout stepper whose tabs are each enabled by their own prop, and a navbar whose admin links only render for an admin, both read as **always** — every transition written in markup was drawn with no condition at all, while the ones written as calls carried theirs. They are read the same way now: a store's checkout tabs say `step1` … `step4`, its navbar says `userInfo && userInfo.isAdmin` for the admin links and `!userInfo` for sign-in, and 59 of that store's 74 transitions carry the condition they actually run under, up from 20. A template language with no condition rules of its own still says nothing rather than guessing.
-
-- **A link in markup no longer reads as a helper's return value.** `<Link to='/shipping'>` was labelled `return /shipping`, which in this picture means the destination came back from somewhere else and was inferred. It is written right there, so it now reads `link /shipping` — and an internal `<a href>` reads `a`. Only a destination that genuinely arrives from elsewhere still says `return`.
-
-- **A link that goes to one of several places now draws all of them.** A destination written as a choice — `!isAdmin ? keyword ? \`/search/${keyword}/page/${x}\` : \`/page/${x}\` : \`/admin/productlist/${x}\`, which is how a paginator shared between a storefront and an admin list is written — used to draw nothing at all, because one edge carried one destination and picking an arm would have been a guess. Every arm is now its own transition, labelled with the path THAT arm takes, so a store's paginated addresses are on the map instead of sitting there unreachable. The same goes for `redirect(307, user ? \`/profile/@${user.username}\` : '/login')` in a loader, and for `router.push(cond ? '/a' : '/b')`. Arms that name the same route still make one transition, and an arm nothing can read still contributes nothing.
-
-- **API endpoints are no longer drawn on the Screens tab.** A store's thirty Express endpoints sat beside its nineteen pages as boxes nothing navigates to and nothing leaves — in a picture that is only about navigation — and they stretched the row of unreachable pages hundreds of boxes wide. A screen is now a route named by its path alone; a route named with the method that reaches it (`GET /api/orders`, `POST /api/users/login`, `ANY /api/users`) is a request, not somewhere a user can be. Every route still appears on Entry points, which is the list of everything a request or a user can arrive at.
-
-- **A screen you could reach but never leave.** Three separate things left a page's own navigation off the map, and a store's home and sign-in pages showed nothing leaving them. **One component, several addresses:** a screen rendered at more than one route — a listing page that is also the search and the paginated results — handed all of its navigation to whichever route happened to be written first, and the rest were drawn as dead ends; every address it serves now gets it. **A link written as a choice:** `<Link to={redirect ? \`/register?redirect=${redirect}\` : '/register'}>` is how a link that carries state is written, and markup was read by a weaker reader than calls were, so it saw nothing; both now use the same one. **A destination whose other half is unknowable:** `const redirect = location.search ? location.search.split('=')[1] : '/'` followed by `history.push(redirect)` is how every app sends a user on after signing in — the `/` is where it lands by default, and reading neither half lost the whole transition. Where both halves ARE readable and disagree, it is still a fork and still nothing.
-
-- **A conditional inside a conditional no longer reads the wrong arm.** A paginator written `!isAdmin ? keyword ? '/search/…' : '/page/…' : '/admin/…'` was split at the first `:` rather than the matching one, so an admin's page links pointed at the storefront's pagination. The arms are paired properly now, and a three-way choice — which is more destinations than one link can name — is left alone.
-
-- **In a repository with several apps, a link no longer points into a different one.** Every app has a `/` and most have a `/login`, and the route table was built for the whole repository at once, so whichever app was indexed first claimed each address — a `<Link to="/posts">` in one app resolved to another app's `/posts`. Measured on a monorepo of 477 apps: **82% of navigations pointed at a route belonging to a different app**, and all of them now point within their own. Screens transitions are also no longer attributed to an unrelated page when several routes are declared in one file, as a code-based route tree or an Express router file is.
-
-- **A SvelteKit layout is no longer a second screen at a page's address.** `+layout.svelte` and `+error.svelte` sit at the same path as the `+page.svelte` beside them and were each indexed as a route, so one address appeared in the index two and three times over. Only a page is a route now.
-
-- **A framework whose package lives in a subfolder is detected again.** In a project that keeps its dependencies one level down — a `frontend/` and a `backend/`, or an `apps/web/` — the framework check ran once before any file had been indexed, found no folders to look in, and remembered that empty answer for the rest of the run. Every React, React Router and Next.js behaviour that depends on knowing the framework is there silently did nothing for those projects.
-
-- **A server action, or any handler written inside a wrapper, draws what it really does.** `const signIn = validatedAction(schema, async (data) => { … })` showed one call out of nine, because the constant held a reference to its schema and that counted as having a body of its own. Its picture is now whole — the lookup, the early returns, the `Promise.all`, the redirect.
-
-- **A call is no longer followed to a same-named method of its own class.** `crypto.createHash('sha256').update(…)` inside a service that happens to have an `update` method was followed into that method, so a login endpoint read as though it updated the user, extra replies and all. A method of your own class is written `this.update(…)`; a receiver that is not `this` now ends the walk instead of guessing.
-
-- **A step is ordered by the call it was actually reached at.** An endpoint whose handler is written inline at the registration (`router.post('/users/login', async (req, res) => { … })`) had every call in the handler read as part of the registration itself, which put its reply before the work that produces it.
-
-- **Express routes behind `app.use('/api', router)` are named by the path a request takes.** A router mounted at a prefix — through as many `router.use('/users', usersRouter)` levels as the app nests, by import or `require` — now names its routes `POST /api/users` instead of `POST /`, and the chained form `router.route('/:id').get(getProduct).put(protect, updateProduct)` (split across lines or not) registers one route per method. Entry points, the Steps tab and the client-to-route pairing all read the real paths.
-
-- **An Express handler written through a wrapper is the endpoint's handler.** `const authUser = asyncHandler(async (req, res) => { … })` — the `express-async-handler` idiom — now names the handler in Entry points and starts the Steps walk at it, with the database reads, the token check and the `401` row read from the arrow's body.
-
-- **A framework declared in a workspace's `package.json` is detected.** React, Next.js, Expo Router, Express and NestJS are found when their dependency lives in `frontend/`, `backend/`, `apps/web/` or `packages/api/` rather than at the repository root, so a monorepo's pages and endpoints exist in the index.
-
-- **FastAPI routes are named by the path a request takes.** `APIRouter(prefix="/items")` and a literal `app.include_router(router, prefix="/api/v1")` — nested through an aggregate router, through a module import or an alias — now compose onto the route names (`GET /api/v1/items/{id}` instead of `GET /{id}`); a prefix that is a setting rather than a string leaves that mount alone rather than guessing.
-
-- **A client call with a type argument binds too.** `useSWR<Team>('/api/team', fetcher)` and `ky.get<User>('/api/users/1')` reach their routes like the untyped forms.
-
-- **ASP.NET Minimal API endpoint groups are routes.** The handler-first form — `groupBuilder.MapPost(CreateTodoItem)`, `MapPut(UpdateTodoItem, "{id}")` inside an `IEndpointGroup` / `EndpointGroupBase` class (the Clean Architecture template and its descendants) — now registers `POST /api/TodoItems` and `PUT /api/TodoItems/{id}`, with the `/api/` head read from the app's own `MapGroup($"/api/{groupName}")` and a class's `RoutePrefix` honoured, each bound to its handler so the Steps tab starts there and lists its `TypedResults` replies by status code.
-
-- **A server action written through a wrapper starts its transition on the right page.** `export const signIn = validatedAction(schema, async (data) => { … redirect('/dashboard') })` — the arrow inside is no symbol of its own — now belongs to `signIn` on the Screens tab, and `signIn` is attributed to the page whose component hands it to `useActionState(signIn, …)`, read from the source when the graph holds no such edge.
-
-- **A row reads in the code's order.** The boxes one step away from a handler now sit left to right as the code runs them — the database read, the token signed while the reply is built, the reply, the error reply — instead of in an arbitrary order; a call written inside another call's arguments (`generateToken(…)` in `res.json({ token: generateToken(…) })`) comes before it, and its link says `inside res.json(…)` in the panel and the tooltip.
-
-- **Each way an endpoint answers is its own box.** A handler that replies `200` or `401` now draws two reply boxes instead of one `200 · 401`, so each line from the handler carries its own condition on the picture — `→ user && (await user.matchPassword(password))` into the 200, `→ NOT (…)` into the 401 — the way a screen's transitions do; replies whose status the code does not spell out share one box labelled by the call.
-
-- **A reply that sets no status is a 200.** `res.json(user)`, `res.send(…)`, `reply.send(…)`, `NextResponse.json(…)`, a `JSONResponse` or `jsonify(…)` with no status in the chain now count as `200`, so an endpoint's success reply has a box of its own beside the `401`'s and its row carries its code; a status set by the statement just before (`res.status(202); res.json(user)`) is that reply's. An Express handler written inline at the registration keeps its own replies too — they were filtered out with the framework noise.
-
-- **Files under an `e2e/` directory count as tests.** Their calls no longer appear as production callers in Steps, dead-code and test badges.
-
-- **Production code under a `samples` or `examples` package path is no longer treated as test code.** A Kotlin or Java project whose package path runs through `com/google/samples/…` (Now in Android, for one) had nearly every file counted as a fixture, so the Map opened on `build-logic`, the entry points hid the app, and dead-code and test badges were wrong. Only the project layout above a `src/` folder decides now; the package path below it never does.
-
-- **A FastAPI service that lives in one directory of a monorepo is detected.** `backend/pyproject.toml` and `backend/app/main.py` count, not only files at the repository root — the official full-stack template's routes now appear in Entry points and the Steps tab.
-
-- **The Steps walk stays out of test doubles.** A production handler that calls an interface method is no longer followed into `TestUserDataRepository` (or any implementation in a test folder); the counts in the summary no longer say "outside the indexs".
-
-- **React handlers written with `useCallback` are now symbols.** `const handleSubmit = useCallback(() => {…}, [])` — the way nearly every handler in a React or React Native component is written — is extracted as a function named by its binding (also `React.useCallback`, `useEffectEvent`), so `onPress={handleSubmit}` and `addListener('x', handleSubmit)` resolve to it, its calls are its own rather than the component's, and a tap's handler shows up in `codegraph_explore`, the Screens tab and the Steps tab. A JSX attribute value (`onPress={handleSubmit}`, `renderItem={renderRow}`) and a handler a hook hands back in an object (`return { handleSubmit, handleRetake }`) are now function-as-value references from the component or hook, so the graph knows which functions are wired as handlers.
-
-- **Stores exported on a later line are read like any other.** `const useStore = create((set, get) => ({ … }))` followed by `export default useStore` (or `export { useStore }`) now has its actions extracted as functions, the same as an `export const` store — previously the two-statement form, common in React Native apps, left every action invisible.
-
-- **API objects exported as a default namespace resolve through to their functions.** `import Api from './api'` + `Api.upload()` where the module ends in `const Api = { upload, createFolder }; export default Api` now links the call to `upload` itself (through the object's own imports), and a default import of any const named by an `export default NAME` statement finds that const rather than guessing the file's first exported function.
-
 - **A Screens tab in `codegraph ui` — the app the way its user meets it.** One box per screen, an arrow for every way of getting from one to another, and on each arrow the condition under which it happens. Click a screen and each of its transitions is labelled beside the screen at the other end of the line with the last condition checked before it happens — `→ isCollected` above `/object-detail` — laid out so that no two labels overlap and none sits under a line; hover a label, a line, or its row in the side panel for the whole condition and the chain the tap travels through (`HomeSearchResults → ItemCard → openObjectDetail`), with a link to each navigation call. A screen that returns to where it came from is drawn around the boxes rather than through them, shared chrome (a top bar rendered on ten screens) is one node a row above what it opens rather than the same arrows from every box, a screen that opens many others is wide enough to follow each line back to it and its lines take separate paths through the gap so they fan out instead of stacking, hovering picks the line nearest the pointer, and a helper that chooses the destination after login shows its fork. Projects whose graph holds screen navigation land on this tab. Expo Router apps today.
 - **A Screens tab in `codegraph ui` — the app the way its user meets it.** One box per screen, an arrow for every way of getting from one to another, and on each arrow the condition under which it happens. Click a screen and each of its transitions is labelled beside the screen at the other end of the line with the last condition checked before it happens — `→ isCollected` above `/object-detail` — laid out so that no two labels overlap and none sits under a line; hover a label, a line, or its row in the side panel for the whole condition and the chain the tap travels through (`HomeSearchResults → ItemCard → openObjectDetail`), with a link to each navigation call. A screen that returns to where it came from is drawn around the boxes rather than through them, shared chrome (a top bar rendered on ten screens) is one node a row above what it opens rather than the same arrows from every box, a screen that opens many others is wide enough to follow each line back to it and its lines take separate paths through the gap so they fan out instead of stacking, hovering picks the line nearest the pointer, and a helper that chooses the destination after login shows its fork. Projects whose graph holds screen navigation land on this tab. Expo Router apps today.
 
 
-- **The Map covers a multi-root project.** A React Native app's `ios/` beside its `src/` — or any second root holding a fifth of the code — is now on the picture, one level deeper, instead of the map silently drawing only the larger root.
-
-- **Conditions read as words, and each scenario gets its own row.** In the Screens and Steps tabs a condition is now written the way you would say it — `WHEN NOT (isUploadInProgress || elapsed < 5000) AND user?.organization_id`, the joining words set apart from the code — with the code inside each guard left as written, and a transition or step with several call sites (four early returns that each go home) is listed as four rows, the clauses they all share said once above them, instead of one string joined with `||`. A guard that is itself an either/or keeps its parentheses everywhere conditions are shown, including `codegraph_explore`'s Flow section.
-
-- **Every call now says when it happens.** In `codegraph ui`, a symbol's callee and caller rails and the Flow strip's connectors carry the branch conditions the call site sits under — `when !isUploading && isCollected` — and `codegraph_explore`'s Flow section prints the same on each hop (`↓ calls (when isCollected)`). The conditions come from the `if` / `else` / ternary / `switch` / `&&` branches around the call, the early returns before it (`if (busy) return` reads as `!busy`), and Swift's `guard`; an inline callback inherits the conditions of the place it is defined. Read from the source as it is now, never stored: nothing about your index changes. TypeScript, JavaScript and Swift today.
-
 - **Expo Router apps: screens and navigation are in the graph.** Every screen file under `app/` (or `src/app/`) is now a route node named by its path — `/object-detail`, `/item/[id]`, with `(group)` folders stripped — linked to the component it renders. Calls like `router.push('/object-detail?…')`, `router.navigate({ pathname: '/item/[id]', params })`, template-literal hrefs, an href held in a local `const`, and `router.push(await pickRoute())` where the helper returns screen paths (one edge per screen it can return) resolve to the screen they open as a new `navigates` edge that remembers the href, so "where does tapping this go" and "who opens this screen" are one hop in `codegraph_explore`, `callers`, and the viewer's Flow strip instead of a dead end at a string. Re-index after upgrading to pick the new edges up.
 - **Expo Router apps: screens and navigation are in the graph.** Every screen file under `app/` (or `src/app/`) is now a route node named by its path — `/object-detail`, `/item/[id]`, with `(group)` folders stripped — linked to the component it renders. Calls like `router.push('/object-detail?…')`, `router.navigate({ pathname: '/item/[id]', params })`, template-literal hrefs, an href held in a local `const`, and `router.push(await pickRoute())` where the helper returns screen paths (one edge per screen it can return) resolve to the screen they open as a new `navigates` edge that remembers the href, so "where does tapping this go" and "who opens this screen" are one hop in `codegraph_explore`, `callers`, and the viewer's Flow strip instead of a dead end at a string. Re-index after upgrading to pick the new edges up.
 
 
 - **Read your graph in a browser: `codegraph ui`.** Point it at a project you've already indexed and it opens a viewer for it on your own machine. Pick a symbol and you see who calls it on the left, its real source in the middle with a marker on every line that calls something, and what it calls on the right, each one drawn level with the line that calls it. Hover either end and both light up; click anything to step into it. Test callers fold into a single line so real callers stay in view, edges CodeGraph isn't confident about are folded away as "uncertain" rather than shown as fact, and a symbol no test reaches within three caller hops says so on a badge. A blast-radius strip counts what a change would reach. Search with `/` or Cmd-K across every symbol and file, start from suggested entry points (routes, hubs, files that run code when imported), and follow a trail of the path you walked that lives in the URL, so you can send someone the exact route you took. Click any file path for that file's outline in source order between everything it depends on and everything that depends on it.
 - **Read your graph in a browser: `codegraph ui`.** Point it at a project you've already indexed and it opens a viewer for it on your own machine. Pick a symbol and you see who calls it on the left, its real source in the middle with a marker on every line that calls something, and what it calls on the right, each one drawn level with the line that calls it. Hover either end and both light up; click anything to step into it. Test callers fold into a single line so real callers stay in view, edges CodeGraph isn't confident about are folded away as "uncertain" rather than shown as fact, and a symbol no test reaches within three caller hops says so on a badge. A blast-radius strip counts what a change would reach. Search with `/` or Cmd-K across every symbol and file, start from suggested entry points (routes, hubs, files that run code when imported), and follow a trail of the path you walked that lives in the URL, so you can send someone the exact route you took. Click any file path for that file's outline in source order between everything it depends on and everything that depends on it.
@@ -196,6 +133,86 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 
 ### Fixes
 ### Fixes
 
 
+#### Screens, links and navigation
+
+- **Where the app goes after login is a fork, not two always-es.** A navigation whose destination comes back from a helper — `router.replace(await resolvePostLoginRoute())` over `return (await hasSeenWelcome(…)) ? '/home/' : '/welcome/'` — drew both screens with no condition, reading as if the welcome screen always shows. The two arms share a line, and only a column can tell them apart; each synthesized edge now carries its literal's own position, so the guard reader says which arm it is: `WHEN await hasSeenWelcome(…)` → home, and its negation → welcome. And the scan starts at the helper's body, so a literal-union return type — `Promise<'/welcome/' | '/home/'>`, whose routes are string literals too, written first — no longer stands in for the navigation itself. Re-index after upgrading to pick the positions up.
+
+- **A screen that talks to native code keeps its own navigations.** In a React Native or Expo app, a `router.push` written inside a listener for a native event was credited to whichever screen had *started* that round trip, not to the screen the push is written on. In one app that moved seven transitions off the capture screen and onto the review screen it opens — leaving the review screen looking as though nothing in the app could reach it, stranded in the "no transition reaches this" band at the bottom of the Screens tab, and printing Swift conditions like `Thread.isMainThread` on a JavaScript navigation. A navigation now belongs to the screen whose file it is written in; an event arriving from native code, from an HTTP call or off a queue is no longer read backwards as if it were a caller.
+
+- **A link written under a condition says so on the Screens tab.** A checkout stepper whose tabs are each enabled by their own prop, and a navbar whose admin links only render for an admin, both read as **always** — every transition written in markup was drawn with no condition at all, while the ones written as calls carried theirs. They are read the same way now: a store's checkout tabs say `step1` … `step4`, its navbar says `userInfo && userInfo.isAdmin` for the admin links and `!userInfo` for sign-in, and 59 of that store's 74 transitions carry the condition they actually run under, up from 20. A template language with no condition rules of its own still says nothing rather than guessing.
+
+- **A link in markup no longer reads as a helper's return value.** `<Link to='/shipping'>` was labelled `return /shipping`, which in this picture means the destination came back from somewhere else and was inferred. It is written right there, so it now reads `link /shipping` — and an internal `<a href>` reads `a`. Only a destination that genuinely arrives from elsewhere still says `return`.
+
+- **A link that goes to one of several places now draws all of them.** A destination written as a choice — `!isAdmin ? keyword ? \`/search/${keyword}/page/${x}\` : \`/page/${x}\` : \`/admin/productlist/${x}\`, which is how a paginator shared between a storefront and an admin list is written — used to draw nothing at all, because one edge carried one destination and picking an arm would have been a guess. Every arm is now its own transition, labelled with the path THAT arm takes, so a store's paginated addresses are on the map instead of sitting there unreachable. The same goes for `redirect(307, user ? \`/profile/@${user.username}\` : '/login')` in a loader, and for `router.push(cond ? '/a' : '/b')`. Arms that name the same route still make one transition, and an arm nothing can read still contributes nothing.
+
+- **API endpoints are no longer drawn on the Screens tab.** A store's thirty Express endpoints sat beside its nineteen pages as boxes nothing navigates to and nothing leaves — in a picture that is only about navigation — and they stretched the row of unreachable pages hundreds of boxes wide. A screen is now a route named by its path alone; a route named with the method that reaches it (`GET /api/orders`, `POST /api/users/login`, `ANY /api/users`) is a request, not somewhere a user can be. Every route still appears on Entry points, which is the list of everything a request or a user can arrive at.
+
+- **A screen you could reach but never leave.** Three separate things left a page's own navigation off the map, and a store's home and sign-in pages showed nothing leaving them. **One component, several addresses:** a screen rendered at more than one route — a listing page that is also the search and the paginated results — handed all of its navigation to whichever route happened to be written first, and the rest were drawn as dead ends; every address it serves now gets it. **A link written as a choice:** `<Link to={redirect ? \`/register?redirect=${redirect}\` : '/register'}>` is how a link that carries state is written, and markup was read by a weaker reader than calls were, so it saw nothing; both now use the same one. **A destination whose other half is unknowable:** `const redirect = location.search ? location.search.split('=')[1] : '/'` followed by `history.push(redirect)` is how every app sends a user on after signing in — the `/` is where it lands by default, and reading neither half lost the whole transition. Where both halves ARE readable and disagree, it is still a fork and still nothing.
+
+- **A conditional inside a conditional no longer reads the wrong arm.** A paginator written `!isAdmin ? keyword ? '/search/…' : '/page/…' : '/admin/…'` was split at the first `:` rather than the matching one, so an admin's page links pointed at the storefront's pagination. The arms are paired properly now, and a three-way choice — which is more destinations than one link can name — is left alone.
+
+- **In a repository with several apps, a link no longer points into a different one.** Every app has a `/` and most have a `/login`, and the route table was built for the whole repository at once, so whichever app was indexed first claimed each address — a `<Link to="/posts">` in one app resolved to another app's `/posts`. Measured on a monorepo of 477 apps: **82% of navigations pointed at a route belonging to a different app**, and all of them now point within their own. Screens transitions are also no longer attributed to an unrelated page when several routes are declared in one file, as a code-based route tree or an Express router file is.
+
+- **A server action written through a wrapper starts its transition on the right page.** `export const signIn = validatedAction(schema, async (data) => { … redirect('/dashboard') })` — the arrow inside is no symbol of its own — now belongs to `signIn` on the Screens tab, and `signIn` is attributed to the page whose component hands it to `useActionState(signIn, …)`, read from the source when the graph holds no such edge.
+
+#### Steps — what a call does, and when
+
+- **A handler called from under a binding says what it passes.** A press that runs `tryCatchSync(onClosePress)` drew a box for the wrapper and stopped — leaving the one thing a reader asks ("what is being wrapped?") unsaid, even though every other call-shaped site already prints its arguments. The panel and tooltip now say `tryCatchSync(onClosePress)` — the argument is the answer.
+
+- **A step the walk stopped at keeps its whole name.** A boundary — another screen, or a cap the walk hit — ends its name with an ellipsis by design, but the box was not sized for it, so a longer name lost its last letters instead (`/scan-to-verif…` for `/scan-to-verify …`). The anchor's start mark clipped a long path the same way (`/sheets/forgot-passw…`). The box now makes room for both.
+
+- **A server action, or any handler written inside a wrapper, draws what it really does.** `const signIn = validatedAction(schema, async (data) => { … })` showed one call out of nine, because the constant held a reference to its schema and that counted as having a body of its own. Its picture is now whole — the lookup, the early returns, the `Promise.all`, the redirect.
+
+- **A call is no longer followed to a same-named method of its own class.** `crypto.createHash('sha256').update(…)` inside a service that happens to have an `update` method was followed into that method, so a login endpoint read as though it updated the user, extra replies and all. A method of your own class is written `this.update(…)`; a receiver that is not `this` now ends the walk instead of guessing.
+
+- **A step is ordered by the call it was actually reached at.** An endpoint whose handler is written inline at the registration (`router.post('/users/login', async (req, res) => { … })`) had every call in the handler read as part of the registration itself, which put its reply before the work that produces it.
+
+- **A client call with a type argument binds too.** `useSWR<Team>('/api/team', fetcher)` and `ky.get<User>('/api/users/1')` reach their routes like the untyped forms.
+
+- **A row reads in the code's order.** The boxes one step away from a handler now sit left to right as the code runs them — the database read, the token signed while the reply is built, the reply, the error reply — instead of in an arbitrary order; a call written inside another call's arguments (`generateToken(…)` in `res.json({ token: generateToken(…) })`) comes before it, and its link says `inside res.json(…)` in the panel and the tooltip.
+
+- **Each way an endpoint answers is its own box.** A handler that replies `200` or `401` now draws two reply boxes instead of one `200 · 401`, so each line from the handler carries its own condition on the picture — `→ user && (await user.matchPassword(password))` into the 200, `→ NOT (…)` into the 401 — the way a screen's transitions do; replies whose status the code does not spell out share one box labelled by the call.
+
+- **A reply that sets no status is a 200.** `res.json(user)`, `res.send(…)`, `reply.send(…)`, `NextResponse.json(…)`, a `JSONResponse` or `jsonify(…)` with no status in the chain now count as `200`, so an endpoint's success reply has a box of its own beside the `401`'s and its row carries its code; a status set by the statement just before (`res.status(202); res.json(user)`) is that reply's. An Express handler written inline at the registration keeps its own replies too — they were filtered out with the framework noise.
+
+- **The Steps walk stays out of test doubles.** A production handler that calls an interface method is no longer followed into `TestUserDataRepository` (or any implementation in a test folder); the counts in the summary no longer say "outside the indexs".
+
+- **Conditions read as words, and each scenario gets its own row.** In the Screens and Steps tabs a condition is now written the way you would say it — `WHEN NOT (isUploadInProgress || elapsed < 5000) AND user?.organization_id`, the joining words set apart from the code — with the code inside each guard left as written, and a transition or step with several call sites (four early returns that each go home) is listed as four rows, the clauses they all share said once above them, instead of one string joined with `||`. A guard that is itself an either/or keeps its parentheses everywhere conditions are shown, including `codegraph_explore`'s Flow section.
+
+- **Every call now says when it happens.** In `codegraph ui`, a symbol's callee and caller rails and the Flow strip's connectors carry the branch conditions the call site sits under — `when !isUploading && isCollected` — and `codegraph_explore`'s Flow section prints the same on each hop (`↓ calls (when isCollected)`). The conditions come from the `if` / `else` / ternary / `switch` / `&&` branches around the call, the early returns before it (`if (busy) return` reads as `!busy`), and Swift's `guard`; an inline callback inherits the conditions of the place it is defined. Read from the source as it is now, never stored: nothing about your index changes. TypeScript, JavaScript and Swift today.
+
+#### Routes and framework detection
+
+- **A SvelteKit layout is no longer a second screen at a page's address.** `+layout.svelte` and `+error.svelte` sit at the same path as the `+page.svelte` beside them and were each indexed as a route, so one address appeared in the index two and three times over. Only a page is a route now.
+
+- **A framework whose package lives in a subfolder is detected again.** In a project that keeps its dependencies one level down — a `frontend/` and a `backend/`, or an `apps/web/` — the framework check ran once before any file had been indexed, found no folders to look in, and remembered that empty answer for the rest of the run. Every React, React Router and Next.js behaviour that depends on knowing the framework is there silently did nothing for those projects.
+
+- **Express routes behind `app.use('/api', router)` are named by the path a request takes.** A router mounted at a prefix — through as many `router.use('/users', usersRouter)` levels as the app nests, by import or `require` — now names its routes `POST /api/users` instead of `POST /`, and the chained form `router.route('/:id').get(getProduct).put(protect, updateProduct)` (split across lines or not) registers one route per method. Entry points, the Steps tab and the client-to-route pairing all read the real paths.
+
+- **An Express handler written through a wrapper is the endpoint's handler.** `const authUser = asyncHandler(async (req, res) => { … })` — the `express-async-handler` idiom — now names the handler in Entry points and starts the Steps walk at it, with the database reads, the token check and the `401` row read from the arrow's body.
+
+- **A framework declared in a workspace's `package.json` is detected.** React, Next.js, Expo Router, Express and NestJS are found when their dependency lives in `frontend/`, `backend/`, `apps/web/` or `packages/api/` rather than at the repository root, so a monorepo's pages and endpoints exist in the index.
+
+- **FastAPI routes are named by the path a request takes.** `APIRouter(prefix="/items")` and a literal `app.include_router(router, prefix="/api/v1")` — nested through an aggregate router, through a module import or an alias — now compose onto the route names (`GET /api/v1/items/{id}` instead of `GET /{id}`); a prefix that is a setting rather than a string leaves that mount alone rather than guessing.
+
+- **ASP.NET Minimal API endpoint groups are routes.** The handler-first form — `groupBuilder.MapPost(CreateTodoItem)`, `MapPut(UpdateTodoItem, "{id}")` inside an `IEndpointGroup` / `EndpointGroupBase` class (the Clean Architecture template and its descendants) — now registers `POST /api/TodoItems` and `PUT /api/TodoItems/{id}`, with the `/api/` head read from the app's own `MapGroup($"/api/{groupName}")` and a class's `RoutePrefix` honoured, each bound to its handler so the Steps tab starts there and lists its `TypedResults` replies by status code.
+
+- **A FastAPI service that lives in one directory of a monorepo is detected.** `backend/pyproject.toml` and `backend/app/main.py` count, not only files at the repository root — the official full-stack template's routes now appear in Entry points and the Steps tab.
+
+#### Symbols, tests and the viewer
+
+- **Files under an `e2e/` directory count as tests.** Their calls no longer appear as production callers in Steps, dead-code and test badges.
+
+- **Production code under a `samples` or `examples` package path is no longer treated as test code.** A Kotlin or Java project whose package path runs through `com/google/samples/…` (Now in Android, for one) had nearly every file counted as a fixture, so the Map opened on `build-logic`, the entry points hid the app, and dead-code and test badges were wrong. Only the project layout above a `src/` folder decides now; the package path below it never does.
+
+- **React handlers written with `useCallback` are now symbols.** `const handleSubmit = useCallback(() => {…}, [])` — the way nearly every handler in a React or React Native component is written — is extracted as a function named by its binding (also `React.useCallback`, `useEffectEvent`), so `onPress={handleSubmit}` and `addListener('x', handleSubmit)` resolve to it, its calls are its own rather than the component's, and a tap's handler shows up in `codegraph_explore`, the Screens tab and the Steps tab. A JSX attribute value (`onPress={handleSubmit}`, `renderItem={renderRow}`) and a handler a hook hands back in an object (`return { handleSubmit, handleRetake }`) are now function-as-value references from the component or hook, so the graph knows which functions are wired as handlers.
+
+- **Stores exported on a later line are read like any other.** `const useStore = create((set, get) => ({ … }))` followed by `export default useStore` (or `export { useStore }`) now has its actions extracted as functions, the same as an `export const` store — previously the two-statement form, common in React Native apps, left every action invisible.
+
+- **API objects exported as a default namespace resolve through to their functions.** `import Api from './api'` + `Api.upload()` where the module ends in `const Api = { upload, createFolder }; export default Api` now links the call to `upload` itself (through the object's own imports), and a default import of any const named by an `export default NAME` statement finds that const rather than guessing the file's first exported function.
+
+- **The Map covers a multi-root project.** A React Native app's `ios/` beside its `src/` — or any second root holding a fifth of the code — is now on the picture, one level deeper, instead of the map silently drawing only the larger root.
+
 - Fixed a long-running `codegraph ui` session serving a symbol that a sync had already deleted. The viewer keeps one connection to your index open, and its in-memory lookup didn't notice when another process — your agent's sync, or `codegraph sync` — rewrote the file underneath it, so a symbol screen could keep showing a body with no callers while search correctly reported it had moved. Because a symbol's identity includes the line it starts on, this happened after almost any edit above it.
 - Fixed a long-running `codegraph ui` session serving a symbol that a sync had already deleted. The viewer keeps one connection to your index open, and its in-memory lookup didn't notice when another process — your agent's sync, or `codegraph sync` — rewrote the file underneath it, so a symbol screen could keep showing a body with no callers while search correctly reported it had moved. Because a symbol's identity includes the line it starts on, this happened after almost any edit above it.
 
 
 ## [1.6.0] - 2026-08-26
 ## [1.6.0] - 2026-08-26