graf blame:<path> costs O(total history), not O(file history). GRAFFileWeave (graf/BLAME.c) topo-walks the tip's full ancestor closure (nord) and calls GRAFBlobAtCommit (graf/BLOB.c) for every commit — inflating the commit object, each path tree, and the (large) file blob from keeper — then byte-dedups AFTER the fetch. Measured (release): blame:graf/GRAF.c 0.34s (1.16s debug+ASAN), nord=1545 for EVERY file regardless of its 11–116-commit history, ~16 inflates/commit, only ~12% of fetches yield a fold. The fix is keeper-side, no index format change: compare object hashes top-down (each child OID is free in its parent tree), stop at the highest unchanged subtree, and inflate the blob + fold only when the leaf OID actually changed. The deeper index-side variant is BLAME-002; parallelism is BLAME-003. See Plan.
Cost scales with total repo history, not the file's; the big blob is re-inflated at every ancestor, then discarded.
nord=1545 ancestors walked per file; blob fetches ≈ nord; only ~184/1544 (12%) become real folds (graf/BLAME.c:344).GRAFBlobAtCommit/GRAFTreeStep).blame_fetch_author <0.4%, render <1% — CPU-bound in zlib + WEAVE, not I/O.
None. Keeper-side only, no on-disk .graf.idx format change; each step lands green and ships independently.
Compare object hashes top-down, stop at the highest unchanged subtree, and inflate a blob only when its OID actually changed.
graf/test/BLAME01.c asserting inflate count is O(changes), not O(nord).tree_hs[i] (= DAGCommitTree, no inflate) equal to the prev folded root ⇒ nothing changed anywhere ⇒ skip (zero inflate); no commit inflate ever (use tree_hs, not the commit body).WEAVEFromBlob + fold ONLY when the leaf OID changed.(tree_sha,name)→child step cache in the GRAF singleton (consulted by GRAFTreeStep) so the trees we do inflate aren't re-inflated across commits; also speeds GET.c replay.(tree,name)→child records remove even that. Expected: ~1.16s → low tens of ms.GRAF_BLAME_STATS (graf/BLAME.c:579 — folds vs blobfetch vs treeinfl) + graf/test/blame-perf.sh — landed with 1a5acce8.tree_hs[i] == prev_root_h ⇒ continue, zero inflate, no commit-body inflate (graf/BLAME.c:457-463) — landed 1a5acce8.blame_descend_leaf (graf/BLAME.c:202,486) stops at the first matching level (BLAME_DESC_SAME ⇒ continue), blob fetched only on a changed leaf OID — landed 1a5acce8 (index-served, i.e. the BLAME-002 variant subsumed this keeper-side plan).(tree_sha,name)→child step cache — moot: DAGChildStep (graf/DAG.h:170) serves the descent from the index with ZERO keeper inflate, beyond what the cache would save.tree_hs[i]==0, graf/BLAME.c:510) still GRAFBlobAtCommit-fetches then byte-dedups post-inflate ("correct, just not faster") — decide whether that path matters in practice.1a5acce8, or keep it open for the unindexed fallback — blocked on gritzko.graf/BLAME.c since 1a5acce8 (2026-06-12); left open pending the close-as-subsumed ruling above.1a5acce8 (BLAME-002 "index-served tree descent, zero re-inflate") delivers this ticket's Steps 0-2 and moots Step 3; only the unindexed-commit fallback remains on the old fetch+byte-dedup path.