**The map finds pages beyond Vite and Django.** A page used to be a Vite entry or a script
a Django template loads, and nothing else. On leanos-app, a Next.js 16 project, that meant
**0** pages and every one of its 6,268 symbols in "Not reached from any page" - while the
Next.js adapter had read all 27 of its routes. Pages now come from *entry sources*, a
second layer beside the backend adapters: each detects with a confidence, and every
confident one runs, so a monorepo gets all of them. Phase 1 of 4 in
`docs/plans/2026-09-15-entry-sources-design.md`.
- **Added** - Next.js pages. Every App Router `page.*`, rooted with the `layout.*` and
`template.*` files around it, and every Pages Router file except `_app`, `_document` and
`api/`. Addresses come from the Next.js adapter itself, so route groups and slots are
read in one place; a monorepo holding more than one app qualifies each page by its app.
- **Added** - server entries, from any backend. Every file holding a route handler or a
view is an entry in its own picker group below the pages, titled by the URLs it serves,
and draws its own routes and handlers. A project with no pages at all
(fastapi-realworld) now has something to open.
- **Added** - the `entry_sources` config key forces which sources run, the way
`server_adapter` does for backends. `seamcheck config` prints each source with its
confidence, and names a source that does not exist rather than raising.
- **Fixed** - an import through a `tsconfig`/`jsconfig` alias (`/components/X`) or a
workspace package was taken for `node_modules` and never followed, in every import
walk: a leanos-app page reached **1** of the 83 files it imports. One resolver now
handles relative paths, `paths`/`baseUrl` through `extends`, workspace packages through
their manifests, and non-script imports (a stylesheet, JSON) as assets that are reached
but not walked.
- **Changed** - a symbol is on a page when an entry reaches its *file*. Drawing still starts
only from seeds and stays bounded, and the seeds now include every kind of touch: store
uses, env reads, Stripe, URL references, JSX attributes. What a page's files hold but no
page draws - class applications, rules in an imported stylesheet - goes to a new bucket,
"On a page, nothing to draw", instead of "Not reached". This moves bucket counts on
projects whose page lists do not change.
- **Changed** - an entry with nothing to draw stays in the picker, and its canvas says how
many symbols its files hold; a page with a single node used to vanish. The map opens on
the first page that draws something.
- **Changed** - on a Next.js project, autoconfig's sweep of loose `.js` files no longer
becomes pages (cal.com and dub lose 22 and 5 of them: `jest.config`, `next.config`, ...).
Only when `js_entry_files` was detected, never when it was configured. Vite entries and
scripts a template loads are never dropped.
- **Fixed** - a not-reached bucket's blurb carried its count, and the picker appended it
again ("- 3 - 3 nodes"). Blurbs are now worded from the detected backends and the
languages actually in the bucket, not in Django's vocabulary.
- **Fixed** - a project whose workspace lists its own root (`packages: ["."]` in
`pnpm-workspace.yaml`, as saleor-dashboard's does) crashed `seamcheck share` and the
`seamcheck_services` MCP tool on Python 3.12, and its map silently lost its service
labels: `Path.glob(".")` raises `IndexError` there (3.13 and 3.14 raise a `ValueError`
that was already caught). The root is now read as that package - by the new import
resolver too, which walks the same globs and crashed every map of such a project before
this release shipped.
- **Fixed** - a Vite entry written the ordinary Node way was not read at all.
`path.resolve(__dirname, "src", "index.tsx")` failed three times over: the pattern
demanded a bare `resolve(`, so a dotted `path.resolve` never matched; only the first
string after `__dirname` was captured, so a path split across arguments read as `src`, a
directory; and anything not ending in `.js` was dropped, so a `.ts`/`.tsx` entry never
survived. A single-string `build.rollupOptions.input` and `vite-plugin-html`'s
`createHtmlPlugin({ entry })` are read too, and a `resolve.alias` pointing at a directory
or into `node_modules` is no longer mistaken for an entry.
- **Fixed** - every Vite entry vanished when the scan ran from anywhere but the repository
root. An entry is written relative to `__dirname`, the directory holding the config, and
was tested with `is_file()` on the literal string - which resolves against the PROCESS's
working directory. The two agree only when the command runs from the repository root, so
`--repo-root`, an editor plugin and the corpus harness all silently found no entries.
- **Changed** - the last-resort fallback, which lists every first-party script no other
first-party file imports, no longer offers tests, stories, `__tests__`/`e2e` files or a
tool's `*.config.*` as pages; it caps the list at 200, nearest the repository root first;
and each entry says so when a bundler config it could not read is what really declares
where the project starts. It was never meant to be a project's page list, and on
saleor-dashboard it had become one.
- **Changed** - a map too large for one file is no longer printed to stdout. `seamcheck
map` with neither `--out` nor `--serve` says which to use and exits 3, rather than
writing a 43 MB document into a terminal. The single-file threshold drops 50 MB → 10 MiB:
at 50 MB it had never once fired on real output, because the largest map the corpus had
ever produced still sat under it.
**On saleor-dashboard** (corpus; Vite + TypeScript, no framework page convention), measured
against fa8b438b4: its one real entry `src/index.tsx` is read, so the page list is **1 page**
where it was **1,240 `entry_file` entries** - 902 of them tests and stories, 6 tool configs,
leaving 332 the new filters would still have offered. The map falls from **43,516,011 bytes
over 415,844 nodes to 1,837,956 over 8,125**, and both render sound. Symbols on a page fall
with it, **4,144/5,637 → 1,452/5,844**: the 4,144 came from 1,240 fabricated pages each
dragging its own reach set, and what leaves them lands in the not-reached buckets, where it
is visible as unreached instead of hidden inside a page that was never real.
Across the 20 repositories `tools/corpus.py entries` measures, every page list is identical
before and after these four fixes, and the share of symbols on a page is unmoved at 49.0%:
saleor-dashboard is not one of the 20, and no repository there gained a page or lost one.
**Measured, 0.15.1 against this release.** Symbols on a page, across the 20 corpus
repositories `tools/corpus.py entries` measures: **1.2% → 49.0%**. The six with Next.js
gain their pages - dub +194, cal.com +83, commerce +5, nextjs-subscription-payments +4,
platforms +3, documenso +2 - and the page lists of the 14 without it are identical.
**On leanos-app** (private, not in the corpus - `tools/labels/leanos-app.json`): 0 pages →
20 Next.js pages and 10 route handlers, every page and route file on disk; on a page 0 →
1,215 of 6,316 symbols; "Everything else" 898 → 50. Precision on its hand-labelled sample
unchanged, 5 true / 2 false.
**Corpus (34/34 cloned repos):** `tools/corpus.py scan` identical before and after - no
CRASH, no repo lost its routes, uncertain share **4.982%** both times. That gate runs the
adapters and never builds a map, so it is flat by construction. Recall **6/6**, unchanged.
Render **46/46**: every cloned repository builds a map whose emitted scripts parse and
whose payload matches its graph - the gate that caught the workspace crash above.
**Measured:** coverage **91%** across 48 projects and 368,052 symbols (Django 93%, Flask
93%, Next.js 72%, Express 69%, FastAPI 58%, NestJS 49%) · recall **6/6** · render **46/46**
· corpus 34/34 scanned with no crash and no lost routes.
Coverage was last published at 0.11.0 - 89% across 47 projects and 358,845 symbols - so
89% → 91% spans 0.12.0 through this release, not this one alone. **Next.js 46% → 72%** is
this line of work: an import through a `tsconfig`/`jsconfig` alias is now followed, so a
page reaches the files it imports rather than one of the eighty-three. The four backends
with no entry-source work in this release - Flask, Express, FastAPI, NestJS - are
unchanged to the point, which is what a targeted change should look like.
Precision is deliberately not quoted. At 78 true of 140 hand-labelled claims it is where