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.None yet.