Orchestrata Ultimate › Plans › briefs › 2026-10-22-maps-delta-builder

Maps step B1 — per-file cache for gen_codebase_map.py (builder brief, 2026-10-22)

Source: plans/briefs/2026-10-22-maps-delta-builder.md at 8a2cb42ad · last changed 2026-10-22 · first committed 2026-10-22 · 161 lines.

Prose pending.

Plan text

Maps step B1 — per-file cache for gen_codebase_map.py (builder brief, 2026-10-22)

GOAL: make scripts/gen_codebase_map.py --with-full re-parse only the files whose content changed since its last run, by keeping a per-file cache of symbol rows and module docstring lines, so a run with no source changes takes about 3 s or less instead of 11.7–13.9 s, and both maps stay byte-identical. This is step B of Mohamed's ruling "maps should use delta" (step A, commit e1f3a5047, put both maps in one process).

FILES: scripts/gen_codebase_map.py only. Nothing else changes in this increment (no tests yet: they are the next increment). Worktree /home/mohamed/workspace/wt-MAP-PRECOMMIT, branch feat/maps-delta (already created from e1f3a5047). Do not stage or commit.

Measured state (do not re-measure)

cProfile of one --with-full run on this tree (21.8 s under the profiler; 11.7–13.9 s unprofiled):

where cumulative what
toolkit.list_symbols, 11 calls 16.4 s 1 from _class_map over src/ (kind="class") + 10 from build_full_map
_extract_symbols (2283 calls) 12.8 s the per-file AST walk; ast.iter_child_nodes alone 7.1 s
builtins.compile (3589 calls) 6.1 s ast.parse: once per file in _parse_cached, again in first_doc_line
_section → first_doc_line (2203 calls) 4.0 s parses every scripts/, entrypoint, package __init__, tests/test_*.py file
imports incl. loading the toolkit 0.6 s

So nearly all the time is parsing and walking unchanged files. Nothing is cached across runs today: the generator loads the toolkit in-process, where _CACHE_STATE["enabled"] is False, so _cached_extract just calls compute() (deliberately: the toolkit's own warm cache is a 200 MB pickle that costs ~9 s to load, which was step A's problem).

Code facts (read with the AST toolkit; line numbers are in the current tree)

scripts/gen_codebase_map.py:

scripts/serialize_codebase_ast.py (do not edit it):

The design (decided — implement exactly this)

  1. A cache class in gen_codebase_map.py, e.g. _MapCache, persisted as JSON (stdlib json, hashlib): {"version": <str>, "files": {<absolute path str>: {"sha1": <hex>, "symbols": [rows] | null, "doc": <str>}}}.
    • version = sha1 over Path(__file__).read_bytes() + the toolkit file's bytes + sys.version. A mismatch discards every entry (a toolkit or generator edit means one cold run).
    • Load: a missing file, unreadable file, bad JSON or wrong shape → start empty. A damaged cache degrades to a cold build; it never breaks the map.
    • Per file, read path.read_text(encoding="utf-8") and sha1 its UTF-8 bytes. An entry is a hit when its sha1 matches. A missing field on a hit entry is computed and stored. A file is parsed at most once per run: one ast.parse serves both the symbols and the doc fields.
    • symbols: the rows of toolkit._extract_symbols(tree, rel), where rel = path.relative_to(ROOT), so the stored file is root-relative. Use null when the toolkit would skip the file: reading fails with OSError/UnicodeDecodeError, or the parse fails with SyntaxError/UnicodeDecodeError.
    • doc: exactly first_doc_line's result, including its "" cases. Note its broader catch, OSError, SyntaxError, ValueError.
    • Save only when something changed: a parse, a new field, a version reset, or an entry pruned. Keep only the entries used this run, so deleted files drop out. Write with dump_text_atomic(cache_path, json.dumps(...)) after cache_path.parent.mkdir(parents=True, exist_ok=True).
    • Counters: hits and parsed.
  2. A cached list_symbols twin in the generator, used only when a cache is given. It enumerates exactly as _iter_py_files does for one source:
    • src = ROOT / tree for the full map, or ROOT / "src" for the class map;
    • a file gives [src], a dir gives sorted(src.rglob("*.py"));
    • skip on toolkit._DEP_SKIP_DIRS and de-dupe on resolve();
    • the rows are cache.symbols(...) for each file, skipping None;
    • filter by kind, then apply the same sorted(key=(file, line)). For the class map, the stored root-relative file values sort in the same order as the toolkit's absolute ones, because they all share the ROOT prefix, and _class_map already normalises them.
  3. Wiring.
    • build_map(cache=None), _class_map(add, cache=None), _section(..., cache=None) and build_full_map(toolkit, cache=None) take an optional cache.
    • None must run today's code unchanged: toolkit.list_symbols and first_doc_line. Existing tests call these with no cache and must still pass, and --no-cache is the reference the proof compares against.
    • main() builds the cache from flags:
      • --cache PATH overrides the default ROOT / ".cache" / "codebase-map.json" (.cache is gitignored, .gitignore:75);
      • --no-cache passes None;
      • --cache-stats prints one stderr line in exactly this format: gen_codebase_map: cache: {hits} hit, {parsed} parsed, {n} files, {path}.
    • One cache object serves the whole run: header map, class map and full map. Save it once at the end of main().
    • When the toolkit fails to load, keep today's behaviour. The class map shows UNAVAILABLE, and the full map goes through the guard with "".
    • Without --cache-stats, print nothing new. The SessionStart hook now shows the generator's stderr.

Traps

Run budget

Run the proof script at most 3 times. If it is still not green after the third run, STOP and report the failing lines and your diagnosis from /tmp/claude-delta-proof/*.err. If the warm run is green but slower than 3.0 s, STOP and report the timings rather than optimising further.

CONSTRAINTS:

DONE WHEN: one run of bash /tmp/claude-1001/-home-mohamed/829ea13c-8ceb-4f51-9b28-672fef74daca/scratchpad/prove-delta.sh /home/mohamed/workspace/wt-MAP-PRECOMMIT prints all of the following:

Separately, ruff check scripts/gen_codebase_map.py and ruff format --check scripts/gen_codebase_map.py must be clean, using the worktree's .venv/bin/ruff or uv run ruff.

VERIFY: bash /tmp/claude-1001/-home-mohamed/829ea13c-8ceb-4f51-9b28-672fef74daca/scratchpad/prove-delta.sh /home/mohamed/workspace/wt-MAP-PRECOMMIT > /tmp/claude-delta-proof.log 2>&1; echo "PROOF-EXIT $?" >> /tmp/claude-delta-proof.log

DESIGN_WAIVER: none — the design above is decided by the orchestrator under Mohamed's ruling "maps should use delta".