#!/usr/bin/env python3 """Generate 9 Excalidraw diagrams for code-review-graph Medium article. All statistics match repo benchmarks exactly. No invented features or numbers. """ import json import math import os import random random.seed(2024) OUT = os.path.dirname(os.path.abspath(__file__)) _n = 0 def _id(): global _n; _n += 1; return f"e{_n:04d}" def _s(): return random.randint(100000, 9999999) def _tw(t, fs): lines = t.split('\n') return max(len(l) for l in lines) * fs * 0.6 def _th(t, fs): return (t.count('\n') + 1) * fs * 1.25 # ── Color palette ── RED = "#e03131"; RED_BG = "#ffc9c9" GRN = "#2f9e44"; GRN_BG = "#b2f2bb" ORG = "#e8590c"; ORG_BG = "#ffd8a8" YLW = "#e67700"; YLW_BG = "#fff3bf" BLU = "#1971c2"; BLU_BG = "#a5d8ff" PRP = "#6741d9"; PRP_BG = "#d0bfff" GRY = "#868e96"; GRY_BG = "#dee2e6" DRK = "#1e1e1e" # ── Element factories ── def _base(typ, x, y, w, h, **k): return { "type": typ, "version": 1, "versionNonce": _s(), "isDeleted": False, "id": _id(), "fillStyle": k.get("fs", "hachure"), "strokeWidth": k.get("sw", 2), "strokeStyle": k.get("ss", "solid"), "roughness": k.get("rough", 1), "opacity": k.get("op", 100), "angle": 0, "x": x, "y": y, "strokeColor": k.get("sc", DRK), "backgroundColor": k.get("bg", "transparent"), "width": w, "height": h, "seed": _s(), "groupIds": k.get("gids", []), "frameId": None, "roundness": k.get("rnd", None), "boundElements": k.get("be", []), "updated": 1710000000000, "link": None, "locked": False, } def R(x, y, w, h, **k): k.setdefault("rnd", {"type": 3}) return _base("rectangle", x, y, w, h, **k) def E(x, y, w, h, **k): k.setdefault("rnd", {"type": 2}) return _base("ellipse", x, y, w, h, **k) def D(x, y, w, h, **k): k.setdefault("rnd", {"type": 2}) return _base("diamond", x, y, w, h, **k) def T(x, y, s, fs=20, **k): w = _tw(s, fs); h = _th(s, fs) e = _base("text", x, y, w, h, **k) e.update({"fontSize": fs, "fontFamily": k.get("ff", 1), "text": s, "textAlign": k.get("ta", "left"), "verticalAlign": k.get("va", "top"), "containerId": None, "originalText": s, "lineHeight": 1.25, "autoResize": True}) e.pop("roundness", None) return e def A(x, y, pts, **k): w = max(abs(p[0]) for p in pts) if pts else 0 h = max(abs(p[1]) for p in pts) if pts else 0 e = _base("arrow", x, y, w, h, **k) e.update({"points": pts, "lastCommittedPoint": None, "startBinding": None, "endBinding": None, "startArrowhead": None, "endArrowhead": k.get("head", "arrow")}) return e def LN(x, y, pts, **k): w = max(abs(p[0]) for p in pts) if pts else 0 h = max(abs(p[1]) for p in pts) if pts else 0 e = _base("line", x, y, w, h, **k) e.update({"points": pts, "lastCommittedPoint": None, "startBinding": None, "endBinding": None, "startArrowhead": None, "endArrowhead": None}) return e def TC(cx, y, s, fs=20, **k): """Text centered horizontally at cx.""" w = _tw(s, fs) return T(cx - w/2, y, s, fs, **k) def save(name, els): with open(name, 'w') as f: json.dump({"type": "excalidraw", "version": 2, "source": "https://excalidraw.com", "elements": els, "appState": {"viewBackgroundColor": "#ffffff", "gridSize": None}, "files": {}}, f, indent=2) print(f" {name}: {len(els)} elements") # ════════════════════════════════════════════ # DIAGRAM 1 — Before vs After # ════════════════════════════════════════════ def d1(): els = [] LC = 420 # left panel center-x RC = 1420 # right panel center-x # Title els.append(TC(920, 25, "The Token Problem", 40, sc=DRK)) # Dashed divider els.append(LN(920, 80, [[0,0],[0,680]], ss="dashed", sc=GRY, sw=1, op=40)) # ── LEFT: Without Graph ── els.append(TC(LC, 85, "Without Graph", 28, sc=RED)) # Claude Code box els.append(R(295, 140, 250, 48, bg=GRY_BG, fs="solid")) els.append(TC(LC, 150, "Claude Code", 20)) # Arrow + label els.append(A(LC, 195, [[0,0],[0,55]], sc=RED)) els.append(TC(LC, 215, "reads entire codebase", 14, sc=RED)) # File grid container els.append(R(195, 275, 450, 240, sc=GRY, bg="#f8f9fa", fs="solid", op=80)) # 20 small file rects (4×5 grid) for row in range(4): for col in range(5): fx = 218 + col * 85 fy = 292 + row * 52 shade = random.choice(["#dee2e6", "#e9ecef", "#ced4da"]) els.append(R(fx, fy, 62, 32, bg=shade, fs="solid", sc=GRY, sw=1, rough=0)) els.append(TC(LC, 528, "Entire Codebase", 16, sc=GRY)) # Red badge els.append(R(295, 565, 250, 48, bg=RED_BG, fs="solid", sc=RED)) els.append(TC(LC, 575, "125,022 tokens", 22, sc=RED)) # Impact detection els.append(TC(LC, 630, "Impact detection: unknown", 16, sc=GRY)) # ── RIGHT: With Graph ── els.append(TC(RC, 85, "With Graph", 28, sc=GRN)) # Claude Code box els.append(R(1295, 140, 250, 48, bg=GRY_BG, fs="solid")) els.append(TC(RC, 150, "Claude Code", 20)) # Arrow + label els.append(A(RC, 195, [[0,0],[0,40]], sc=GRN)) els.append(TC(RC, 210, "queries graph", 14, sc=GRN)) # Diamond: Graph els.append(D(1378, 255, 84, 58, bg=PRP_BG, fs="solid", sc=PRP)) els.append(TC(RC, 269, "Graph", 16, sc=PRP)) # Arrow + label els.append(A(RC, 320, [[0,0],[0,40]], sc=GRN)) els.append(TC(RC, 332, "blast radius", 14, sc=GRN)) # Ghost rect (faded full codebase) els.append(R(1195, 380, 450, 155, sc=GRY, bg="#f8f9fa", fs="solid", op=20, ss="dashed")) # Relevant files rect els.append(R(1270, 393, 300, 125, sc=GRN, bg="#ebfbee", fs="solid")) # 5 green file squares for i in range(5): fx = 1290 + i * 55 els.append(R(fx, 415, 40, 35, bg=GRN_BG, fs="solid", sc=GRN, sw=1, rough=0)) els.append(TC(RC, 465, "Minimal Review Set", 16, sc=GRN)) # Green badge els.append(R(1295, 565, 250, 48, bg=GRN_BG, fs="solid", sc=GRN)) els.append(TC(RC, 575, "1,986 tokens", 22, sc=GRN)) # Impact detection els.append(TC(RC, 630, "100% recall on impact detection", 16, sc=GRN)) # ── BOTTOM BANNER ── els.append(R(600, 700, 640, 52, bg=BLU_BG, fs="solid", sc=BLU)) els.append(TC(920, 710, "71.4\u00d7 fewer tokens \u00b7 100% impact recall (flask)", 22, sc=BLU)) return els # ════════════════════════════════════════════ # DIAGRAM 2 — Architecture Pipeline # ════════════════════════════════════════════ def d2(): els = [] els.append(TC(920, 25, "How It Works", 40)) boxes = [ ("Repository", "your code", BLU_BG, BLU, 60), ("Tree-sitter Parser","30+ languages + notebooks", ORG_BG, ORG, 380), ("SQLite Graph", "nodes + edges\nflows + communities", PRP_BG, PRP, 700), ("Blast Radius", "BFS traversal", YLW_BG, YLW, 1020), ("Minimal Review Set","only what matters", GRN_BG, GRN, 1380), ] bw, bh, by = 260, 90, 160 for name, sub, bg, sc, bx in boxes: els.append(R(bx, by, bw, bh, bg=bg, fs="solid", sc=sc)) els.append(TC(bx + bw/2, by + 18, name, 20, sc=sc)) els.append(TC(bx + bw/2, by + 50, sub, 14, sc=GRY)) arrow_labels = ["parse ASTs", "store", "query impacted files", "return set"] for i, label in enumerate(arrow_labels): ax = boxes[i][4] + bw + 8 ay = by + bh/2 gap = boxes[i+1][4] - ax - 8 els.append(A(ax, ay, [[0,0],[gap,0]])) els.append(TC(ax + gap/2, ay - 24, label, 13, sc=GRY)) # Bottom bracket lx, rx = boxes[0][4], boxes[-1][4] + bw bky = by + bh + 55 els.append(LN(lx, bky, [[0,0],[rx-lx,0]], sc=GRY, sw=1)) els.append(LN(lx, bky-8, [[0,0],[0,8]], sc=GRY, sw=1)) els.append(LN(rx, bky-8, [[0,0],[0,8]], sc=GRY, sw=1)) els.append(TC((lx+rx)/2, bky+12, "Persistent \u00b7 Incremental \u00b7 Local", 18, sc=GRY)) return els # ════════════════════════════════════════════ # DIAGRAM 3 — Blast Radius # ════════════════════════════════════════════ def d3(): els = [] cx, cy = 480, 400 els.append(TC(cx, 20, "Blast Radius of a Change", 36)) # Center node (CHANGED) els.append(E(cx-80, cy-40, 160, 80, bg=RED_BG, fs="solid", sc=RED)) els.append(TC(cx, cy-25, "auth.py::", 13, sc=RED)) els.append(TC(cx, cy-5, "login()", 20, sc=RED)) els.append(TC(cx, cy+22, "CHANGED", 12, sc=RED)) # Ring 1 (depth 1, orange) — 3 nodes r1 = 200 ring1_spec = [ ("validate_token()", "CALLS", 90), ("User", "DEPENDS_ON", 215), ("test_login()", "TESTED_BY", 325), ] ring1_pos = [] for name, edge_label, angle_deg in ring1_spec: a = math.radians(angle_deg) nx = cx + r1 * math.cos(a) ny = cy - r1 * math.sin(a) ring1_pos.append((nx, ny)) nw, nh = 175, 50 els.append(E(nx-nw/2, ny-nh/2, nw, nh, bg=ORG_BG, fs="solid", sc=ORG)) els.append(TC(nx, ny-8, name, 14, sc=ORG)) # Arrow from center dx, dy = nx-cx, ny-cy dist = math.sqrt(dx*dx+dy*dy) sf, ef = 50/dist, (dist-40)/dist sx, sy = cx+dx*sf, cy+dy*sf els.append(A(sx, sy, [[0,0],[dx*(ef-sf), dy*(ef-sf)]], sc=ORG, sw=1)) # Edge label at midpoint mx, my = cx+dx*0.55, cy+dy*0.55 els.append(T(mx+8, my-14, edge_label, 11, sc=ORG, op=70)) # Ring 2 (depth 2, yellow) — 3 nodes r2 = 370 ring2_spec = [ ("protected_route()", "CALLS", 65, "r1", 0), ("AuthMiddleware", "CALLS", 118, "r1", 0), ("test_protected()", "TESTED_BY", 340, "r2", 0), # from protected_route ] ring2_pos = [] for name, edge_label, angle_deg, _, _ in ring2_spec: a = math.radians(angle_deg) nx = cx + r2 * math.cos(a) ny = cy - r2 * math.sin(a) ring2_pos.append((nx, ny)) for i, (name, edge_label, angle_deg, parent_ring, pidx) in enumerate(ring2_spec): nx, ny = ring2_pos[i] nw, nh = 180, 50 els.append(E(nx-nw/2, ny-nh/2, nw, nh, bg=YLW_BG, fs="solid", sc=YLW)) els.append(TC(nx, ny-8, name, 14, sc=YLW)) # Parent position if parent_ring == "r1": px, py = ring1_pos[pidx] else: px, py = ring2_pos[pidx] dx, dy = nx-px, ny-py dist = math.sqrt(dx*dx+dy*dy) if dist > 0: sf, ef = 40/dist, (dist-45)/dist sx, sy = px+dx*sf, py+dy*sf els.append(A(sx, sy, [[0,0],[dx*(ef-sf), dy*(ef-sf)]], sc=YLW, sw=1)) mx, my = px+dx*0.5, py+dy*0.5 els.append(T(mx+8, my-14, edge_label, 11, sc=YLW, op=70)) # Outer gray nodes (NOT IMPACTED) outer = [("utils.py", 920, 180), ("config.py", 920, 320), ("database.py", 920, 460), ("static/...", 920, 600)] for name, ox, oy in outer: els.append(E(ox-60, oy-22, 120, 44, sc=GRY, bg=GRY_BG, fs="solid", op=35, ss="dashed")) els.append(TC(ox, oy-8, name, 12, sc=GRY, op=40)) els.append(TC(920, 400, "Unrelated files", 14, sc=GRY, op=50)) # Legend ly = 720 for i, (bg, sc, label) in enumerate([ (RED_BG, RED, "Changed"), (ORG_BG, ORG, "Direct dependents"), (YLW_BG, YLW, "Indirect dependents"), (GRY_BG, GRY, "Unrelated files"), ]): lx = 100 + i * 200 els.append(R(lx, ly, 22, 22, bg=bg, fs="solid", sc=sc, sw=1)) els.append(T(lx+30, ly+3, label, 14, sc=sc)) return els # ════════════════════════════════════════════ # DIAGRAM 4 — Incremental Update Flow # ════════════════════════════════════════════ def d4(): els = [] els.append(TC(450, 20, "Incremental Updates in < 2 Seconds", 34)) sx = 180 # step box x sw, sh = 370, 55 step_cx = sx + sw/2 # Step 1: Trigger y1 = 95 els.append(R(sx, y1, sw, sh, bg=BLU_BG, fs="solid", sc=BLU)) els.append(TC(step_cx, y1+12, "git commit / file save", 20, sc=BLU)) els.append(T(sx+sw+18, y1+18, "hook triggered", 13, sc=GRY)) els.append(A(step_cx, y1+sh+5, [[0,0],[0,30]])) # Step 2: Detect y2 = 190 els.append(R(sx, y2, sw, sh, bg=ORG_BG, fs="solid", sc=ORG)) els.append(TC(step_cx, y2+12, "git diff", 20, sc=ORG)) # Chips for i, f in enumerate(["auth.py", "routes.py"]): cx_chip = sx + sw + 18 + i * 115 els.append(R(cx_chip, y2+5, 100, 30, bg="#fff4e6", fs="solid", sc=ORG, sw=1)) els.append(T(cx_chip+8, y2+11, f, 13, sc=ORG)) els.append(T(sx+sw+18, y2+42, "2 changed files", 13, sc=ORG)) els.append(A(step_cx, y2+sh+5, [[0,0],[0,30]])) # Step 3: Cascade y3 = 285 els.append(R(sx, y3, sw, sh, bg=YLW_BG, fs="solid", sc=YLW)) els.append(TC(step_cx, y3+12, "Find dependent files", 20, sc=YLW)) for i, f in enumerate(["test_auth.py", "test_routes.py", "middleware.py"]): cx_chip = sx + sw + 18 + i * 122 els.append(R(cx_chip, y3+5, 112, 30, bg="#fff9db", fs="solid", sc=YLW, sw=1)) els.append(T(cx_chip+6, y3+11, f, 12, sc=YLW)) els.append(T(sx+sw+18, y3+42, "3 dependent files", 13, sc=YLW)) els.append(T(sx-160, y3+15, "SHA-256\nhash check", 13, sc=GRY, ta="right")) els.append(A(step_cx, y3+sh+5, [[0,0],[0,30]])) # Step 4: Re-parse y4 = 380 els.append(R(sx, y4, sw, sh+10, bg=GRN_BG, fs="solid", sc=GRN)) els.append(TC(step_cx, y4+8, "Re-parse 5 files", 20, sc=GRN)) els.append(TC(step_cx, y4+35, "Graph updated \u2713", 14, sc=GRN)) # Badge els.append(R(sx+sw+18, y4+8, 140, 42, bg=GRN_BG, fs="solid", sc=GRN)) els.append(TC(sx+sw+88, y4+17, "< 2 seconds", 17, sc=GRN)) # Right panel: skipped files skip_x, skip_y = 810, 120 els.append(R(skip_x, skip_y, 160, 300, sc=GRY, bg=GRY_BG, fs="solid", op=25, ss="dashed")) for i in range(9): fy = skip_y + 15 + i * 30 els.append(R(skip_x+15, fy, 130, 18, bg="#e9ecef", fs="solid", sc=GRY, sw=1, op=30, rough=0)) els.append(TC(skip_x+80, skip_y+300, "2,910 files", 15, sc=GRY)) els.append(TC(skip_x+80, skip_y+320, "skipped", 15, sc=GRY)) return els # ════════════════════════════════════════════ # DIAGRAM 5 — Benchmark Metric Board # ════════════════════════════════════════════ def d5(): els = [] els.append(TC(800, 15, "Benchmarks Across Real Repos", 36)) # Header: range number left, quality badge right els.append(TC(500, 75, "38\u00d7 \u2013 528\u00d7", 64, sc=BLU)) els.append(TC(500, 160, "fewer tokens across 6 tested repos", 20, sc=GRY)) els.append(R(820, 85, 340, 80, bg=GRN_BG, fs="solid", sc=GRN)) els.append(TC(990, 100, "100% recall, 0.71 F1", 22, sc=GRN)) els.append(TC(990, 132, "on impact detection (13 commits)", 14, sc=GRN)) # 3 repo cards \u2014 naive_corpus_tokens \u2192 avg graph_tokens across 5 questions # Pinned SHAs: gin@5c00df8a, flask@a29f88ce, fastapi@0227991a cards = [ {"name":"gin", "files":"Go web framework", "red":"92\u00d7", "tok":"166,868 \u2192 1,990 tokens", "c":BLU, "bg":BLU_BG}, {"name":"flask", "files":"Python web framework", "red":"71\u00d7", "tok":"125,022 \u2192 1,986 tokens", "c":ORG, "bg":ORG_BG}, {"name":"fastapi", "files":"Python web framework", "red":"528\u00d7", "tok":"951,071 \u2192 2,169 tokens", "c":GRN, "bg":GRN_BG}, ] cw, ch = 370, 200 gap = 50 total = 3*cw + 2*gap x0 = (1600 - total) / 2 cy = 230 for i, cd in enumerate(cards): cx = x0 + i*(cw+gap) ccx = cx + cw/2 els.append(R(cx, cy, cw, ch, bg=cd["bg"], fs="solid", sc=cd["c"], op=80)) els.append(TC(ccx, cy+15, cd["name"], 24, sc=cd["c"])) els.append(TC(ccx, cy+48, cd["files"], 14, sc=GRY)) els.append(TC(ccx, cy+75, cd["red"], 52, sc=cd["c"])) els.append(TC(ccx, cy+150, cd["tok"], 14, sc=DRK)) # Footnote — styled as a subtle callout fn_y = cy + ch + 20 els.append(LN(x0+80, fn_y, [[0,0],[total-160,0]], sc=GRY, sw=1, op=30)) els.append(TC(800, fn_y+10, "Reproducible: see docs/REPRODUCING.md (pinned SHAs, Leiden seed=42)", 16, sc=GRY)) return els # ════════════════════════════════════════════ # DIAGRAM 6 — Monorepo Funnel # ════════════════════════════════════════════ def d6(): els = [] els.append(TC(700, 15, "Whole Codebase or Targeted Answer?", 40)) # ── LEFT: dense grid ── gx, gy = 40, 110 cols, rows = 14, 9 dw, dh = 20, 16 gapx, gapy = 26, 22 for r in range(rows): for c in range(cols): fx = gx + c*gapx fy = gy + r*gapy shade = random.choice(["#e9ecef","#dee2e6","#ced4da","#d0d0d0"]) els.append(R(fx, fy, dw, dh, bg=shade, fs="solid", sc=GRY, sw=1, rough=0)) gcx = gx + (cols*gapx)/2 els.append(TC(gcx, gy-35, "code-review-graph", 22, sc=DRK)) els.append(TC(gcx, gy+rows*gapy+8, "1,326 nodes", 16, sc=GRY)) els.append(TC(gcx, gy+rows*gapy+30, "208,821 source tokens", 14, sc=RED)) # ── CENTER: funnel (rounded rect) ── fx, fy, fw, fh = 470, 120, 210, 180 els.append(R(fx, fy, fw, fh, bg=PRP_BG, fs="solid", sc=PRP)) fcx = fx + fw/2 els.append(TC(fcx, fy+25, "code-review-graph", 17, sc=PRP)) els.append(TC(fcx, fy+65, "parse \u2192", 13, sc=PRP, op=70)) els.append(TC(fcx, fy+85, "graph \u2192", 13, sc=PRP, op=70)) els.append(TC(fcx, fy+105, "blast radius", 13, sc=PRP, op=70)) # Arrow into funnel els.append(A(gx+cols*gapx+5, gy+(rows*gapy)/2, [[0,0],[fx-gx-cols*gapx-20, 0]])) # Arrow out of funnel rx = 740 els.append(A(fx+fw+5, fy+fh/2, [[0,0],[rx-fx-fw-10, 0]], sc=GRN)) # ── RIGHT: sparse green files ── rf_x, rf_y = 760, 130 file_w, file_h, file_gap = 50, 38, 52 for i in range(5): fy2 = rf_y + i*file_gap els.append(R(rf_x, fy2, file_w, file_h, bg=GRN_BG, fs="solid", sc=GRN)) lbl_x = rf_x + file_w + 20 els.append(T(lbl_x, rf_y, "Graph answer", 17, sc=GRN)) els.append(T(lbl_x, rf_y+22, "5 hits + edges", 14, sc=GRN)) els.append(T(lbl_x, rf_y + 4*file_gap + 5, "~2,495 tokens", 16, sc=GRN)) els.append(T(lbl_x, rf_y + 4*file_gap + 26, "avg over 5 questions", 12, sc=GRN)) # Big number at bottom els.append(TC(550, 400, "93\u00d7", 80, sc=BLU)) els.append(TC(550, 490, "fewer tokens per question", 24, sc=BLU)) els.append(TC(550, 525, "answer-shaped context, not file dumps", 18, sc=GRY)) return els # ════════════════════════════════════════════ # DIAGRAM 7 — MCP Integration Flow # ════════════════════════════════════════════ def d7(): els = [] els.append(TC(700, 20, "How Claude Code Uses the Graph", 36)) # ── Step boxes (vertical flow) ── bw, bh = 320, 65 sx = 100 rx = 550 # right column for annotations # Step 1: User asks y = 90 els.append(R(sx, y, bw, bh, bg=BLU_BG, fs="solid", sc=BLU)) els.append(TC(sx+bw/2, y+10, "User", 22, sc=BLU)) els.append(TC(sx+bw/2, y+38, '"Review my changes"', 14, sc=GRY)) els.append(A(sx+bw/2, y+bh+5, [[0,0],[0,30]], sc=GRY)) # Step 2: Claude Code y = 200 els.append(R(sx, y, bw, bh, bg=PRP_BG, fs="solid", sc=PRP)) els.append(TC(sx+bw/2, y+10, "Claude Code", 22, sc=PRP)) els.append(TC(sx+bw/2, y+38, "checks MCP tools", 14, sc=GRY)) # Right annotation: what Claude looks for els.append(R(rx, y-5, 380, 75, bg="#f8f9fa", fs="solid", sc=GRY, op=60)) els.append(T(rx+15, y+5, "Skills tell Claude:", 14, sc=GRY)) els.append(T(rx+15, y+25, '"Use get_review_context before\n scanning files manually"', 13, sc=PRP)) els.append(A(sx+bw/2, y+bh+5, [[0,0],[0,30]], sc=PRP)) # Step 3: MCP call y = 310 els.append(R(sx, y, bw, bh, bg=ORG_BG, fs="solid", sc=ORG)) els.append(TC(sx+bw/2, y+10, "MCP Server", 22, sc=ORG)) els.append(TC(sx+bw/2, y+38, "code-review-graph serve", 13, sc=GRY)) # Right annotation: what gets called els.append(R(rx, y-5, 380, 75, bg="#fff4e6", fs="solid", sc=ORG, op=60)) els.append(T(rx+15, y+5, "30 tools available:", 14, sc=ORG)) els.append(T(rx+15, y+25, "detect_changes \u2192 get_review_context\n\u2192 get_impact_radius \u2192 query_graph", 13, sc=ORG)) els.append(A(sx+bw/2, y+bh+5, [[0,0],[0,30]], sc=ORG)) # Step 4: Graph query y = 420 els.append(D(sx+bw/2-50, y, 100, 65, bg=GRN_BG, fs="solid", sc=GRN)) els.append(TC(sx+bw/2, y+18, "graph.db", 16, sc=GRN)) # Right annotation: what gets returned els.append(R(rx, y-5, 380, 75, bg="#ebfbee", fs="solid", sc=GRN, op=60)) els.append(T(rx+15, y+5, "Returns:", 14, sc=GRN)) els.append(T(rx+15, y+25, "Blast radius, affected flows,\ntest gaps, risk scores", 13, sc=GRN)) els.append(A(sx+bw/2, y+65+5, [[0,0],[0,30]], sc=GRN)) # Step 5: Claude responds y = 530 els.append(R(sx, y, bw, bh, bg=GRN_BG, fs="solid", sc=GRN)) els.append(TC(sx+bw/2, y+10, "Precise Review", 22, sc=GRN)) els.append(TC(sx+bw/2, y+38, "reads only what matters", 14, sc=GRN)) # ── Bottom banner ── els.append(R(200, 630, 600, 48, bg=RED_BG, fs="solid", sc=RED)) els.append(TC(500, 640, "Without skills/hooks: Claude ignores the graph entirely", 16, sc=RED)) return els # ════════════════════════════════════════════ # DIAGRAM 8 — Supported Platforms # ════════════════════════════════════════════ def d8(): els = [] els.append(TC(600, 20, "One Install, Every Platform", 36)) els.append(TC(600, 70, "code-review-graph install", 20, sc=PRP, ff=3)) # 14 platforms \u2014 matches code_review_graph/skills.py PLATFORMS dict platforms = [ # Row 1 (7) ("Claude Code", ".mcp.json", BLU, BLU_BG), ("Codex", "~/.codex/config.toml", PRP, PRP_BG), ("Cursor", ".cursor/mcp.json", ORG, ORG_BG), ("Windsurf", "~/.codeium/windsurf/mcp_config.json", GRN, GRN_BG), ("Zed", "Zed settings.json", YLW, YLW_BG), ("Continue", "~/.continue/config.json", RED, RED_BG), ("OpenCode", ".opencode.json", BLU, PRP_BG), # Row 2 (7) ("Antigravity", "~/.gemini/antigravity/mcp_config.json", GRY, GRY_BG), ("Gemini CLI", ".gemini/settings.json", PRP, BLU_BG), ("Qwen Code", "~/.qwen/settings.json", ORG, YLW_BG), ("Kiro", ".kiro/settings/mcp.json", GRN, ORG_BG), ("Qoder", ".qoder/mcp.json", YLW, GRN_BG), ("GitHub Copilot", ".vscode/mcp.json", RED, RED_BG), ("Copilot CLI", "~/.copilot/mcp-config.json", PRP, GRN_BG), ] # Central "install" node center_x, center_y = 600, 215 els.append(E(center_x-75, center_y-30, 150, 60, bg=PRP_BG, fs="solid", sc=PRP)) els.append(TC(center_x, center_y-10, "auto-detect", 16, sc=PRP)) # Two rows of 7 cards each cols, rows = 7, 2 card_w, card_h = 135, 85 gap_x, gap_y = 18, 35 total_w = cols * card_w + (cols - 1) * gap_x # 7*135 + 6*18 = 1053 x0 = center_x - total_w/2 row_y = [330, 330 + card_h + gap_y] for i, (name, cfg, sc, bg) in enumerate(platforms): row = i // cols col = i % cols cx = x0 + col * (card_w + gap_x) + card_w/2 cy = row_y[row] # Light arrow from center node only to the first row, to keep things readable. # Skip the center column (dx≈0): a width=0 arrow renders as nothing # anyway, and the Excalidraw VS Code extension rejects the file if # any arrow has width=0 OR height=0 (the excalidraw.com web app is # more lenient and accepts them). if row == 0: dx = cx - center_x dy = cy - center_y - 30 dist = math.sqrt(dx*dx + dy*dy) if dist > 0 and abs(dx) > 1.0: sf = 35/dist els.append(A(center_x + dx*sf, center_y + 30 + dy*sf*0.1, [[0,0], [dx*(1-sf*2), dy*(1-sf*1.5)]], sc=sc, sw=1, op=45)) # Platform card els.append(R(cx-card_w/2, cy, card_w, card_h, bg=bg, fs="solid", sc=sc)) els.append(TC(cx, cy+14, name, 14, sc=sc)) short_cfg = cfg if len(cfg) < 26 else "..." + cfg[-22:] els.append(TC(cx, cy+45, short_cfg, 9, sc=GRY, ff=3)) # Footer footer_y = row_y[1] + card_h + 30 els.append(TC(600, footer_y, "Auto-detects installed platforms \u00b7 Poetry / uv / uvx aware \u00b7 Writes correct config", 14, sc=GRY)) return els # ════════════════════════════════════════════ # DIAGRAM 9 — Language Coverage # ════════════════════════════════════════════ def d9(): els = [] els.append(TC(700, 15, "30+ Languages + Notebook Support", 34)) # Group languages by ecosystem \u2014 verified against parser.py EXTENSION_TO_LANGUAGE groups = [ ("Web", ["TypeScript", "JavaScript", "TSX", "Vue", "Svelte"], BLU, BLU_BG), ("Backend", ["Python", "Go", "Rust", "Java", "Scala", "Elixir"], GRN, GRN_BG), ("Systems", ["C", "C++", "C#", "Objective-C", "Zig"], ORG, ORG_BG), ("Mobile", ["Kotlin", "Swift", "Dart"], PRP, PRP_BG), ("Scripting", ["Ruby", "PHP", "Perl", "Lua", "R", "Julia"], YLW, YLW_BG), ("Shells", ["Bash", "PowerShell"], RED, RED_BG), ("Domain", ["Solidity", "SQL", "Verilog", "GDScript", "Nix"], GRY, GRY_BG), ("Other", ["ReScript", "Jupyter/.ipynb"], PRP, PRP_BG), ] gw = 130 # group width \u2014 narrower to fit 8 groups gap = 18 total_w = len(groups) * gw + (len(groups)-1) * gap # 8*130 + 7*18 = 1166 x0 = (1400 - total_w) / 2 # center on 700 gy = 75 for gi, (group_name, langs, sc, bg) in enumerate(groups): gx = x0 + gi * (gw + gap) gh = 55 + len(langs) * 32 # Group container els.append(R(gx, gy, gw, gh, bg=bg, fs="solid", sc=sc, op=60)) els.append(TC(gx+gw/2, gy+10, group_name, 16, sc=sc)) # Language pills for li, lang in enumerate(langs): ly = gy + 42 + li * 32 pw = gw - 20 els.append(R(gx+10, ly, pw, 24, bg="#ffffff", fs="solid", sc=sc, sw=1, rough=0, op=80)) els.append(TC(gx+gw/2, ly+4, lang, 12, sc=sc)) # Bottom: what each language gets max_gh = max(55 + len(g[1]) * 32 for g in groups) fy = gy + max_gh + 20 features = ["Functions", "Classes", "Imports", "Calls", "Inheritance", "Tests"] feat_w = total_w / len(features) for i, feat in enumerate(features): fx = x0 + i * feat_w + feat_w/2 els.append(TC(fx, fy, "\u2713 " + feat, 13, sc=GRN)) els.append(TC(700, fy+28, "Tree-sitter grammars + ReScript regex pass + Jupyter / Databricks notebook handling", 13, sc=GRY)) return els # ════════════════════════════════════════════ # GENERATE ALL # ════════════════════════════════════════════ if __name__ == "__main__": print("Generating diagrams...") for name, fn in [ ("diagram1_before_vs_after.excalidraw", d1), ("diagram2_architecture_pipeline.excalidraw", d2), ("diagram3_blast_radius.excalidraw", d3), ("diagram4_incremental_update.excalidraw", d4), ("diagram5_benchmark_board.excalidraw", d5), ("diagram6_monorepo_funnel.excalidraw", d6), ("diagram7_mcp_integration_flow.excalidraw", d7), ("diagram8_supported_platforms.excalidraw", d8), ("diagram9_language_coverage.excalidraw", d9), ]: save(f"{OUT}/{name}", fn()) print("\nDone! Open files in https://excalidraw.com")