◉ANTIGRAVITY LABJP
Articles/App Development
▣ App Development/2026-10-14Intermediate

Before Turning Undocumented Code Into a Spec, Decide the Reading Order by Fan-In

When you ask an agent to turn undocumented code into a spec, which files do you hand over first? A 36-line Python script ranks files by import count (fan-in) plus route entry points, with measurements from a 77-file Next.js project.

antigravity464legacy-codespecificationfan-inpython27

I recently read a Zenn write-up about pointing an agent at a Rails app with no documentation at all and ending up with a usable spec. My first reaction wasn't admiration. It was a question: what would I hand over first?

Even for an app I wrote myself a few years ago, half a year of not touching it is enough to forget where the front door is. The tempting move is to give the agent the whole folder. I did that once and got a blurry summary, because I had asked it to read too much.

Decide how much to hand over by how deep you want it to read. That's the line I draw now.

Here is a small script I wrote to get the reading order from numbers instead of instinct, along with what it showed on a real project.

Start With the Front Doors and the Most-Imported Files

You don't need every file to write a spec. Two kinds of files carry most of the weight:

  1. Entry points: routing, pages, middleware, config. They decide what the outside world sees.
  2. Files that many others import: shared modules. If you misread one of these, the whole spec drifts.

The second group can be found mechanically by counting imports. I call the number of files importing a given file its fan-in.

The Script (Standard Library Only, 36 Lines)

This version targets TypeScript / JavaScript. It resolves relative imports and the @/ alias, and ignores external packages.

#!/usr/bin/env python3
"""reading_order.py — 既存コードの「読ませる順番」を fan-in とエントリポイントで出す。標準ライブラリのみ。"""
import os, re, sys, json, collections
root = os.path.abspath(sys.argv[1]); top_n = int(sys.argv[2]) if len(sys.argv) > 2 else 12
SKIP = {"node_modules", ".git", ".next", "dist", "build", "public", "content", "__pycache__"}
EXT = (".ts", ".tsx", ".js", ".mjs", ".jsx")
ENTRY = re.compile(r"(^|/)(page|route|layout|middleware|proxy)\.(t|j)sx?$")
IMP = re.compile(r"""(?:from|import)\s*\(?\s*['"]([^'"]+)['"]""")
files, size = [], {}
for d, ds, fs in os.walk(root):
    ds[:] = [x for x in ds if x not in SKIP]
    for f in fs:
        if f.endswith(EXT):
            p = os.path.relpath(os.path.join(d, f), root); files.append(p); size[p] = os.path.getsize(os.path.join(root, p))
fset = set(files)
def resolve(src, spec):
    if spec.startswith("@/"): base = os.path.join("src", spec[2:])
    elif spec.startswith("."): base = os.path.normpath(os.path.join(os.path.dirname(src), spec))
    else: return None
    for c in [base] + [base + e for e in EXT] + [os.path.join(base, "index" + e) for e in EXT]:
        if c in fset: return c
fan = collections.Counter()
for p in files:
    try: t = open(os.path.join(root, p), encoding="utf-8").read()
    except Exception: continue
    for m in set(IMP.findall(t)):
        r = resolve(p, m)
        if r and r != p: fan[r] += 1
entries = [p for p in files if ENTRY.search(p)]
total = sum(size.values())
ranked = [p for p, _ in fan.most_common(top_n)]
pick = list(dict.fromkeys(ranked + entries[:top_n]))
pb = sum(size[p] for p in pick)
print(json.dumps({"files": len(files), "total_bytes": total, "entries": len(entries),
  "picked": len(pick), "picked_bytes": pb, "ratio_pct": round(pb * 100 / total, 1)}, ensure_ascii=False))
for p in ranked: print(f"fan-in {fan[p]:>3}  {size[p]:>6}B  {p}")

Run it as python3 reading_order.py <project-root> 8. The second argument is how many top fan-in files to take. The first output line is JSON: file count, total bytes, picked files, picked bytes, and the ratio.

Results on a 77-File Project

I ran it on a Next.js project of mine: 77 TypeScript / JavaScript files, 496,852 bytes in total (article data and build output excluded). Each run took 0.03 seconds, and three runs gave identical output.

Top fan-in countFiles picked (with entry points)Bytes pickedShare of total
81657,42911.6%
122489,73018.1%
2040243,84249.1%

Eight files plus entry points came to 16 files, a bit over a tenth of the code. Going to 20 jumped the share to nearly half, because some large files joined between ranks 12 and 20.

Here are four from the top 8 (ranks 1, 2, 3, and 8):

Fan-inBytesFile
197,491src/lib/content.ts
96,983src/config/pricing.ts
71,051src/components/ui/LevelBadge.tsx
215,129src/config/gone-slugs.ts

The last row shows the script's limit. Its fan-in is only 2, yet it is 15,129 bytes, and it's a list of URLs that tells you nothing about behavior. Counting alone lets files like that slip in: big, but not worth reading.

Treat the Numbers as a Draft, and Prune by Eye

My routine now:

  • Start with the top 8 plus entry points, about a tenth of the code
  • Scan the list and remove data tables, generated files, and constants by hand
  • Widen to 12, then 20, only when the first spec draft leaves a "where does this function come from?" gap

Tying the widening to a concrete gap in the draft keeps me from over-reading.

One caveat: what I verified here is the script's output only. I haven't measured how the spec quality changes when an agent actually reads these lists, and I'd like to compare that on the same project another time.

Your Next Step

Pick an old project you've been wondering about and run the script once with 8. Count how many files on the list you'd never want an agent to read. That count is a fair estimate of how much manual pruning your project will need.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

▣ App Dev2026-04-23
Keeping the Antigravity Python API Stable in Production — Retries, Timeouts, and Circuit Breakers That Actually Work
A deeply practical guide to keeping Python services built on the Google Gen AI SDK alive under real traffic. We cover retry, timeout, circuit breaker, rate limit, and cost budgeting patterns with runnable code from an Antigravity workflow.
▣ App Dev2026-10-08
Three Safety Nets to Test with a Fake agy Before Running the Antigravity CLI in a Container
Before you call the Antigravity CLI (agy) from Docker or a Cloud Run job, reproduce three failure modes with a fake agy: an installer that returns gzip, a hang on tool approval, and an exit code of 0 with nothing produced. Everything here rests on official docs and on code I ran myself.
▣ App Dev2026-09-10
When the iOS 27 Date Landed, the First File I Grepped Was AGENTS.md
iOS 27 and iPadOS 27 ship on September 14. Old version numbers linger in your source, but also in the files only your agent reads. Here is a script that counts both zones, plus the one line keyword search will never find.