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:
- Entry points: routing, pages, middleware, config. They decide what the outside world sees.
- 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 count | Files picked (with entry points) | Bytes picked | Share of total |
|---|---|---|---|
| 8 | 16 | 57,429 | 11.6% |
| 12 | 24 | 89,730 | 18.1% |
| 20 | 40 | 243,842 | 49.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-in | Bytes | File |
|---|---|---|
| 19 | 7,491 | src/lib/content.ts |
| 9 | 6,983 | src/config/pricing.ts |
| 7 | 1,051 | src/components/ui/LevelBadge.tsx |
| 2 | 15,129 | src/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.