First, find out what the app is.

Every per-page feature needs a list of pages. Discovery builds it three ways at once: a crawler follows links, a static pass reads the source for routes the crawler cannot reach, and an agent walks past the walls (sign-in, an onboarding wizard, a feature flag) that stop the other two. The result is the URL map every sweep and every check runs against.

In the dashboard
Explore
Feature specs
42, 72
The Explore canvas: a tree of the app's pages with screenshot thumbnails, visit counts and check status per node.

Three ways in, one map out.

Crawl

Starting from paths you give it, a crawler follows same-origin links to a depth and page budget you set. Cheap, fast, and it records which pages were reached and from where.

Read the source

When the project has a repository, the run reads its route definitions and templates for paths the crawler never saw: pages behind a form submission, admin routes, deep links only an email contains. Read-only, and optional.

Walk past the walls

An agent takes the blocked entries, signs in with the catalog's login test, completes the wizard, flips the flag, and records how it got there. That "how to reach" note is kept on the page node for every later run.

Grouping rules keep one screen one node.

Built-in normalisation folds numeric ids, UUIDs and long hashes into :id. Real apps also route on human slugs, so a project adds its own rules: a segment after a literal, a segment matching a pattern, a whole-path rewrite, query keys to drop, and for hash-routed apps a rule that makes the fragment the route. Rules preview before they apply, and a remap shows what would merge.

Every visit any test makes adds to the map: the screenshot's page URL is normalised and attached to its node with the test that reached it, so the map grows with ordinary regression runs, not only discovery.

Jade's url_rules (project settings)
[
  { "kind": "after_segment", "segment": "tenant", "placeholder": ":tenant" },
  { "kind": "after_segment", "segment": "client", "placeholder": ":client" },
  { "kind": "after_segment", "segment": "event",  "placeholder": ":event"  },
  { "kind": "query_drop",    "keys": ["utm_source", "utm_medium"] }
]

/organizer/tenant/acme/client/foo-2026/event/housing-summit/housing
/organizer/tenant/globex/client/bar/event/expo/housing
        → /organizer/tenant/:tenant/client/:client/event/:event/housing
A blocked page's node after discovery
/account/orders
  reached:  0 visits  ·  status: BLOCKED
  requires: a signed-in customer account
  how to reach: sign in at /login (catalog test "customer-login"),
                then open Account → Orders
  last discovery: passed · 9 visited (9 new) · 1 blocked · 1 rule added

Where it stops today

  • Discovery boots the app in an isolated environment and can take minutes on a large monorepo; the status line on the Explore screen is where to watch it.
  • Source reading needs a repository the project can clone and a framework whose routes are declared in files the pass understands; a fully dynamic router is found by crawling and walking only.
  • Grouping rules are per project and hand-written. A new route shape that mints tokens will fan out until a rule covers it; the token_segment rule catches most, not all.
  • Clearing the map to rebuild it deletes the accumulated per-page check history with it.