Back to blog
Guides8 min read

AI code generation build order: why topological sort matters

Why multi-file AI code generation should follow a topological build order, what each step should be shown, and why the walk is sequential, not parallel.

Multi-file AI code generation fails in a specific, repeatable way: the model writes a page that imports a hook that does not exist, with a signature it invented, from a store it has not seen. Each file looks fine on its own. The project does not build.

The fix is not a better prompt. It is an ordering rule: generate the files in a topological build order, dependencies before dependents, and show every step the exports of the files already written. This is how the Export tab in Nodlume generates a project, and this post explains the mechanism and the trade-offs behind it.

What a build order is, and why an AI needs one

A build order is a sequence of files in which no file is written before something it imports. Compilers and build tools have used it for decades. Maven resolves module order this way, TypeScript project references do, and every bundler walks the import graph before it emits anything.

The reason an AI code generator needs the same thing is that a language model can only be faithful to what is in its context. Ask it for a checkout page and it will import useCart from @/lib/stores/cart, because that is the most probable spelling of a cart store. If the store was actually generated as useCartStore with a lines array instead of items, the page is wrong in a way no single-file review catches.

Ordering removes the guess. When the store is written first and its export signature is in the prompt for the page, the model composes what exists rather than what it imagined. That is the entire value of the order, and it is worth keeping in mind for every design decision that follows.

Topological sort in one paragraph

A topological sort orders the nodes of a directed acyclic graph so that every edge points forward. For code, the nodes are files and the edges are imports. If page.tsx imports cart.ts, the edge is cart.ts → page.tsx and the sort places cart.ts first.

Kahn's algorithm is the usual implementation: count each node's incoming edges, start with the nodes that have none, and each time you emit a node, decrement the count of everything it points to. Anything that drops to zero joins the queue.

type Edge = { from: string; to: string }

export function buildOrder(files: string[], edges: Edge[]): string[] {
  const inDegree = new Map(files.map((f) => [f, 0]))
  const dependents = new Map<string, string[]>()
  for (const { from, to } of edges) {
    inDegree.set(to, (inDegree.get(to) ?? 0) + 1)
    dependents.set(from, [...(dependents.get(from) ?? []), to])
  }
  // Sort the queue so ties break deterministically — the same board
  // must produce the same order on every render.
  const queue = files.filter((f) => inDegree.get(f) === 0).sort()
  const order: string[] = []
  while (queue.length > 0) {
    const file = queue.shift()!
    order.push(file)
    for (const next of dependents.get(file) ?? []) {
      const remaining = inDegree.get(next)! - 1
      inDegree.set(next, remaining)
      if (remaining === 0) queue.push(next)
    }
    queue.sort()
  }
  return order
}

Two details matter more than the algorithm. Ties must break deterministically, or the order changes between runs and a resumable generation cannot know where it left off. And cycles must be broken by a rule, not by crashing: two components that import each other are rare in a planned project, but a generator that throws on them is a generator that stops.

What the order looks like for a Next.js app

You do not need a general graph solver if the project already has a shape. A Next.js App Router project has a natural rank order, and the scaffolding Nodlume derives from a canvas uses ranks rather than raw edges:

  1. Stores first. A Zustand store imports nothing from the rest of the tree, and everything below may read it. Within the rank, the most-used store goes first, because the module more of the tree leans on is the one whose shape should settle earliest.
  2. The project's own components. Pages compose them. Catalogue parts such as shadcn components take no step, since they ship as finished registry source and are not work to do.
  3. The guard contract. Every protected page imports the same requireRole module, so it must exist before any of them.
  4. Layouts, outermost first. The root layout wraps everything, then each route group's layout, then nested ones.
  5. Pages, parent before child. The same breadth-first walk that names the routes also orders them.

For a terminal target built with Ink the list closes rather than opens with the router and the bin entry, because both import every screen above them. For a browser extension the service worker and content scripts sit last, after the message contract both sides are written against. The rule is the same in each case, and only the file names change.

Ranked ordering is a topological sort where the edges are implied by kind. It is faster to compute, it is stable by construction, and it is easier to explain to the person reading the generated AGENTS.md than a list produced by a generic graph walk.

What each step should be shown

The order only pays off if each generation step is fed the right context. Three things belong in every per-file prompt:

  • The skeleton. The imports, the exported signature, and the doc block for the file being written. This is the contract. It is derived deterministically from the design, costs nothing, and is the last thing to cut when the prompt gets long.
  • The exports of everything written above. One line per file, in the form path — exports. Not the generated code. A forty-file project would blow the context window if every step carried every earlier body, and the model does not need the bodies. It needs to know that the cart store exports useCartStore and that useCartStore exposes lines, add, and remove.
  • The paths of what is still to come. A page that knows a CheckoutSummary component is planned will import it rather than inline it. Without the list, the model helpfully writes the component into the page, and the later step that generates the real one produces a duplicate.

The exports line is the interesting piece. Each step returns it alongside the file body, and it is the only thing later steps see. That keeps the prompt bounded: when the list outgrows its cap, the oldest lines drop first, because the build order has already placed the modules a file is most likely to import directly above it.

Project-wide instructions ride along too. If the author wrote "use React Query for server state" in the domain document, every file should see that line, or the first file will use fetch in an effect and the rest will follow it.

Why the walk is sequential, not parallel

The obvious optimisation is to generate each depth layer in parallel. Files at the same depth do not depend on each other, so nothing stops you from firing ten requests at once. The original design for Nodlume's generation pass proposed exactly that.

It was dropped, deliberately, for one reason: parallelism inside a layer trades away the context that made the order worth following.

Consider two pages at the same depth, a product listing and a product detail. Neither imports the other, so the sort places them side by side. But if the listing is generated first, the detail page can see that the listing extracted a ProductCard helper and reused a formatPrice utility. Generated in parallel, each invents its own. The project still builds, but it is a worse project, and the next human to open it inherits two spellings of the same thing.

There is a second, more practical reason. A generation pass is one model call per file, and any responsible API has a velocity limit per user. Ten parallel calls hit that limit immediately; the run then spends its time waiting out rate-limit responses instead of writing code. A sequential walk meets the same limit later and less often.

The cost is wall-clock. A forty-file project takes forty round-trips instead of five. For code that a person is going to download, open, and build on, a few extra minutes is a good price for files that agree with each other.

Make the run resumable

A sequential walk over a long list will be interrupted. The tab closes, a request fails, the user runs out of credits, or a rate limit forces a wait. None of those should throw away the files already written.

The mechanism is simple once the order is stable: skip every step whose file already has generated code. A run then continues from the first unwritten file, and the same rule handles a stop, a failure, and a batch cap identically. Nodlume caps a run at twenty files for a related reason: generation is metered per file, and a single button that could quietly spend a month's allowance is not a button anyone presses twice.

Resumability is also why the ordering must be deterministic. If the sort could return a different sequence on the next render, "skip what is written" would skip the wrong things.

Where the deterministic layer ends and the model begins

There are two layers, and it is worth being precise about which is which.

The deterministic layer derives the file tree, every file's skeleton, and the build order from the design. It runs in the browser, it is free, and it produces the same output for the same input every time. This is the scaffolding you get from the Export tab without spending a credit, and it is what the AGENTS.md appendix prints when the build order is enabled.

The model layer fills each skeleton with a body, one file at a time, in that order. This is what costs credits, because it is a real model call per file. The two are separated so the free half is always available and the paid half never has to guess at structure.

If you are building your own generator, keep the same boundary. Derive as much as you can without a model: paths, exports, imports, guards, route groups. Then give the model only the job it is good at, which is writing a body against a signature it can see.

Checklist for a multi-file generation pass

  • Order files so no file is written before something it imports. Use ranks when the project has a shape, and a general topological sort when it does not.
  • Break ties by name so the order is stable across runs.
  • Show each step the skeleton of its own file, the exports line of every file above it, and the paths of the files still to come.
  • Return an exports summary from every step, and feed that rather than the generated code forward.
  • Run the walk sequentially. Parallel layers are faster and produce worse projects.
  • Skip files that already have code, so any interruption is a pause rather than a restart.
  • Keep the deterministic skeleton free and meter only the model calls.

The order is not decoration on top of generation. It is the reason the generated project fits together, and every other decision in the pipeline follows from protecting it.

Application architecture

Continue this learning path

Route trees, layouts, navigation, guards, and the structural decisions that become expensive after implementation begins.

Open the Structure Painter

Design your application

Carry the route tree into a complete project with data, APIs, access rules, tests, and generated scaffolding.

Open projects

We'd like to use Google cookies to understand how Nodlume is used and to measure our advertising. Nothing loads until you choose, and declining does not affect anything in the app.