idlejet

your app as a subway map

A structural diagnostic for an app's navigation graph — it finds the screens a user can reach but never leave, draws the whole thing as a transit map, and hands the fixes to your own coding agent without your source ever leaving your machine.

role
design + engineering
timeline
2026 — shipping
status
live
stack
Next.js · TypeScript · React Flow · Zod · Groq · Gemini
01

The bug that isn't in the code

Most apps break before anyone shows up, and not because a function is wrong. They break because nobody ever drew the workflow. A settings screen you can get to from three places and out of from none. A modal route that survived a refactor and is now reachable only by typing the URL. A success page that dead-ends because the button that used to leave it moved into a layout six months ago.

None of that is a failing test. Every individual file is fine. The defect only exists in the shape of the whole thing, which is exactly the view nobody has, because no tool builds it and no person holds it in their head past about thirty routes.

Fig. 01Stations are routes, lines are navigation, and the band below the rule is everything the network cannot reach.
02

Two prompts, and a line your code never crosses

You run an extraction prompt inside your own agent — Claude Code, Cursor, whatever you already have open — and it writes one file, dump.md. That file is node ids, labels, types, relative paths, and the edges between them. It is the only artifact that leaves your machine.

idlejet reads it, and hands back two things: a transit map, and idlejet.md. That second file is written for a machine, not for you — it opens with a section addressed to the agent that will read it, and its actions are sorted by severity rather than by file. You drop it back into the same agent and it does the work.

So the loop is agent → service → agent, and the round trip never carries a line of source in either direction.

03

Telling the agent not to is not a guarantee

The extraction prompt is emphatic about the boundary: no source, no config, no business logic, no env var names — not even the names, with the value stripped. When in doubt, it says, it is implementation, so leave it out.

That is a policy. A policy is a thing a model complies with most of the time, and the whole product is standing on it, so there is a second pass that assumes the first one failed. Before anything is sent anywhere, the dump is scanned independently: twelve credential patterns and nine code-shape patterns, run over every line.

The trick is that the contract already defines what a legal line looks like — a heading, a key: value, or a table row. Contract-shaped lines get checked cell by cell for credentials. Anything off-contract gets the code detector too. That split is what lets router.push and app/page.tsx through while catching a pasted file body, which a single naive regex pass cannot do.

A blocking finding is a 422 with the findings attached, and nothing is forwarded. There is also a scan-only endpoint that makes no outbound call at all, so you can check a dump before you ever agree to send it.

The extraction prompt forbids source and secrets. But “we told the agent not to” is a policy, not a guarantee.

04

Hubs, not cycles

The first version treated a cycle as a defect. That turned out to be wrong: in a well-connected app almost everything is in a cycle, and the flag fired constantly while missing the thing that actually hurts.

What hurts is being trapped — on a screen you can reach but cannot leave. So the model got rebuilt around hubs. A hub is an entry point, or anywhere the persistent navigation can take you: the sidebar, the top bar, the tab bar. The load-bearing question for every node became whether it can reach one.

Getting there needed a second change. Navigation offered by a layout used to be an ordinary edge, which meant reachability was dominated by layout wrapping — every page looked connected because its shell was. Now shared navigation is its own edge kind, and the normalizer fans it out onto every page the layout wraps, so the graph reflects what a user can actually do from each screen rather than what the component tree happens to contain.

05

The model writes the prose. The rules decide the verdict.

Every graph fact — in and out degree, depth from the nearest entry point, strongly connected components, reachability, hub membership — is computed locally before a model is involved. The classifier is handed those as facts and told not to recompute or dispute them, so it is doing structural judgement rather than graph theory.

Then the part that matters: its verdict is not the verdict. A reconciliation pass re-derives each classification from the graph metrics and overwrites the model's, so what ships is a rule-based answer wearing model-written prose. The schema does not even ask the model for the classification enum — it would only be thrown away, and the free-tier token budget is better spent on the reasoning.

This is also why the fallback is not a downgrade. If the primary model is unavailable, a local rule-based classifier runs instead and the report says so. It produces the same verdicts, because the verdicts were never the model's to produce. All you lose is bespoke wording.

What the machine guarantees

verdictsrule-derived from computed graph facts, never model-asserted
layoutdeterministic — same graph in, same coordinates out
node idsstable across runs, so a fixed issue disappears under the same id
privacysanitized before the first network call, or refused
ceiling512KB per dump · 10 analyses/min/IP
degradationlocal classifier on model failure, surfaced in the report
06

Why this cannot be a prompt file

It is a fair challenge, and the answer has to be better than “because it is a website”. Stage one genuinely is a prompt. Anyone can copy it.

What a prompt cannot do is refuse to take the model's word for it. Ask an agent to audit the navigation graph it just described and it is marking its own homework — the same context that produced the description produces the verdict, and a hallucinated edge becomes a hallucinated all-clear. The value is in the part that re-derives the answer deterministically and overrules it.

A prompt also has no memory. Stable node ids across runs are the beginning of a history, and a history is what turns a report into a regression test: this branch trapped two screens that were fine last week. And a prompt cannot fail a pull request, which is where a structural check actually belongs — nobody remembers to run the audit on the day they break the graph.

The honest framing is that the prompt is the adapter and the engine is the product. The adapter is free to copy. The engine is determinism, persistence, and enforcement, and those are not things you can write in markdown.

Where it goes next

  • Runs, keptStore each analysis against a commit. Because node ids are already stable, the diff between two runs is free — “this PR made three screens unreachable” is a query, not a feature.
  • A check that fails the buildA status check with a budget, the way bundle size has one. New Invalid nodes block the merge; the annotation lands on the file that introduced them. This is the step that turns idlejet from a thing you remember to run into a thing that runs on you.
  • The map as the inputRight now the diagram is an output. Let someone draw the edge that ought to exist, or pull a detached station onto the line, and emit that as the directive. Editing the map becomes the way you author the fix, and the agent implements what you drew.
  • More than one frameworkThe extraction prompt recognises routes by how a codebase registers them, not by what they are named — the graph model does not know what Next.js is. React Router, Expo and SvelteKit are prompt work, not engine work.