"""aider-repo-map-edit-formats-explained.py — a miniature "repo map" for Python code.
What it does: walks a directory of .py files, collects top-level function and class
signatures with the ast module, builds an import graph between the files (file A imports
module B => edge A -> B), ranks the files with a small PageRank (power iteration, no
third-party packages), and prints a signature-only map trimmed to a token budget.
The token budget uses the chars/4 heuristic (the rough "1 token ~ 4 characters" figure
from the Claude pricing FAQ); it is NOT a tokenizer, so treat budget numbers as estimates.
Inputs: a directory path, optional --map-tokens N (default 1000), optional --top K.
Run: python aider-repo-map-edit-formats-explained.py
[--map-tokens 1000] [--top 20]
Needs: Python 3.10+ (tested on 3.13.12 on Windows). Standard library only.
"""
from __future__ import annotations
import argparse
import ast
import os
import sys
def module_name(root: str, path: str) -> str:
rel = os.path.relpath(path, root).replace(os.sep, "/")
return rel[:-3].replace("/", ".")
def collect(root: str) -> dict[str, dict]:
"""Return {module: {"path":..., "sigs": [...], "imports": set()}} for every .py under root."""
files: dict[str, dict] = {}
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if not d.startswith((".", "__pycache__", "prod_v1", "node_modules"))]
for name in filenames:
if not name.endswith(".py"):
continue
path = os.path.join(dirpath, name)
try:
tree = ast.parse(open(path, encoding="utf-8").read(), filename=path)
except (SyntaxError, UnicodeDecodeError) as e:
print(f"skip {path}: {e}", file=sys.stderr)
continue
sigs: list[str] = []
imports: set[str] = set()
for node in tree.body:
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
sigs.append(f"def {node.name}({ast.unparse(node.args)})")
elif isinstance(node, ast.ClassDef):
bases = ", ".join(ast.unparse(b) for b in node.bases)
sigs.append(f"class {node.name}({bases})" if bases else f"class {node.name}")
for sub in node.body:
if isinstance(sub, (ast.FunctionDef, ast.AsyncFunctionDef)):
sigs.append(f" def {sub.name}({ast.unparse(sub.args)})")
for node in ast.walk(tree):
if isinstance(node, ast.Import):
imports.update(a.name.split(".")[0] for a in node.names)
elif isinstance(node, ast.ImportFrom) and node.module:
imports.add(node.module.split(".")[0])
files[module_name(root, path)] = {"path": path, "sigs": sigs, "imports": imports}
return files
def pagerank(files: dict[str, dict], damping: float = 0.85, iters: int = 50) -> dict[str, float]:
"""PageRank over the import graph: an edge A -> B when A imports module B (B in the set)."""
names = list(files)
short = {n.split(".")[-1]: n for n in names} # allow `import engine` to hit `engine`
edges = {n: set() for n in names}
for n, info in files.items():
for imp in info["imports"]:
target = files.get(imp) and imp or short.get(imp)
if target and target != n:
edges[n].add(target)
n_nodes = len(names)
rank = {n: 1.0 / n_nodes for n in names}
for _ in range(iters):
new = {n: (1.0 - damping) / n_nodes for n in names}
for src, targets in edges.items():
share = rank[src] / len(targets) if targets else rank[src] / n_nodes
for t in (targets or names):
new[t] += damping * share
rank = new
return rank
def render(files: dict[str, dict], rank: dict[str, float], map_tokens: int, top: int) -> str:
"""Emit files best-first, whole file blocks only while the chars/4 budget holds."""
out: list[str] = []
used = 0
for name in sorted(rank, key=rank.get, reverse=True)[:top]:
info = files[name]
block = [f"{os.path.relpath(info['path'])}: (rank {rank[name]:.3f})"] + [f" {s}" for s in info["sigs"]] + [""]
text = "\n".join(block)
cost = len(text) // 4 # heuristic tokens
if used + cost > map_tokens:
out.append(f"... {os.path.relpath(info['path'])} omitted ({cost} est. tokens over budget)")
continue
used += cost
out.append(text)
out.append(f"[map: ~{used} est. tokens of {map_tokens} budget, chars/4 heuristic]")
return "\n".join(out)
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("root")
ap.add_argument("--map-tokens", type=int, default=1000)
ap.add_argument("--top", type=int, default=20)
a = ap.parse_args()
files = collect(a.root)
if not files:
sys.exit("no .py files found")
rank = pagerank(files)
print(render(files, rank, a.map_tokens, a.top))
if __name__ == "__main__":
main()