diff --git a/README.md b/README.md index 9f1e11f0..1952acbc 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,10 @@ A knowledge graph is not a vector index: it is **deterministic**, **every edge i ## See it in action +

+ graphify path query: a terminal asks for the shortest path between FastAPI and ModelField, and the answer lights up hop by hop across the knowledge graph +

+ Once the graph is built you query it instead of reading files. Real output, graphify run on the FastAPI codebase shown above: ```text diff --git a/docs/demo-path.svg b/docs/demo-path.svg new file mode 100644 index 00000000..4bf10a6e --- /dev/null +++ b/docs/demo-path.svg @@ -0,0 +1 @@ +graphify$graphifypath"FastAPI""ModelField"Shortest path (3 hops):FastAPI --uses--> DefaultPlaceholder <--references-- get_request_handler() --references--> ModelField3 hops. Zero files opened.usesreferencesreferencesFastAPIDefaultPlaceholderget_request_handler()ModelField \ No newline at end of file diff --git a/scripts/gen_demo_path.py b/scripts/gen_demo_path.py new file mode 100644 index 00000000..acb0dae7 --- /dev/null +++ b/scripts/gen_demo_path.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +"""Generate docs/demo-path.svg: the animated 'path lights up' README hero-companion. + +Concept 1 ("The Path Lights Up"): a terminal types `graphify path ...` on the +left; on the right the same answer draws itself as a graph, a pulse igniting +each hop while the rest of the constellation stays dim. + +Constraints honored: +- Pure SMIL/CSS inside an -referenced SVG (survives GitHub's sanitizer, + no JS, no external fonts). +- Brand palette only: muted emerald green (#4db18f family) as the single + accent on a dark green-black card. No shiny/neon colors. +- One 10s master period; every is dur=10s repeatCount=indefinite and + fades back to its start state before the loop point, so the loop is seamless. + +Run: python3 scripts/gen_demo_path.py -> writes docs/demo-path.svg +""" +import math +import os +import random + +STATIC = bool(os.environ.get("STATIC")) # bake the lit held-frame for visual QA + +T = 10.0 # master loop seconds +HOLD_END = 0.96 # everything fades out over the last 0.4s +FADE = 0.12 # generic fade-in duration (fraction handled per item) + +# ---- brand palette (sampled from docs/logo.png) ---------------------------- +BG = "#0c1712" # dark green-black card +BG2 = "#0f1e18" # subtle gradient partner +BORDER = "#20342b" +DIVIDER = "#1c2c25" +DIM_NODE = "#2c3a34" # unlit nodes +DIM_EDGE = "#22302a" # unlit edges +GREEN = "#4db18f" # brand green (lit) +GREEN_HI = "#62c4a2" # brand green light (accents/caption) +GREEN_DK = "#41a884" # brand green deep +TXT = "#c6d6ce" # primary terminal text +TXT_DIM = "#6f8a7f" # secondary terminal text +PROMPT = "#4db18f" +DOTS = ["#ff5f56", "#ffbd2e", "#27c93f"] # macOS traffic lights: close / minimize / zoom + +W, H = 900, 360 +CHARW = 6.62 # mono advance at 11px +FS = 11 +LX = 20 # left text margin +PANEL = 336 # terminal panel width + +def kt(*pairs): + """pairs of (keyTime, value) -> (values_str, keyTimes_str).""" + ks = ";".join(f"{k:.4f}".rstrip("0").rstrip(".") if k not in (0, 1) else str(int(k)) for k, _ in pairs) + vs = ";".join(str(v) for _, v in pairs) + return vs, ks + +def op0(): + """initial opacity for a revealable element (1 when baking a static frame).""" + return "1" if STATIC else "0" + +def reveal(t, hold_val="1", start_val="0"): + """opacity reveal at time t (s), hold, fade out before loop.""" + if STATIC: + return "" + a = t / T + b = min((t + FADE) / T, HOLD_END - 0.001) + vs, ks = kt((0, start_val), (a, start_val), (b, hold_val), (HOLD_END, hold_val), (1, start_val)) + return f'' + +out = [] +def e(s): out.append(s) + +# ---------------------------------------------------------------- svg header +e(f'') +e('') +e(f'' + f'') +e(f'' + f'' + f'') +e('') + +# card +e(f'') +e(f'') + +# window dots (muted) +for i in range(3): + e(f'') +e(f'graphify') + +# ---------------------------------------------------------------- terminal +line_y = 74 +prompt_x = LX +# static prompt "$" +e(f'$') +cmd = 'graphify path "FastAPI" "ModelField"' +cmd_x0 = prompt_x + CHARW * 2 +type_start = 0.35 +type_iv = 0.058 +for i, ch in enumerate(cmd): + if ch == " ": + continue + cx = cmd_x0 + i * CHARW + t = type_start + i * type_iv + disp = ch.replace("&", "&").replace("<", "<").replace(">", ">") + e(f'{disp}{reveal(t)}') +type_end = type_start + len(cmd) * type_iv + +# blinking caret that steps along while typing, then blinks at the end +caret_xs = [cmd_x0 + i * CHARW for i in range(len(cmd) + 1)] +cvals = ";".join(f"{x:.1f}" for x in caret_xs) + f";{caret_xs[-1]:.1f}" +ckeys = ";".join(f"{(type_start + i*type_iv)/T:.4f}" for i in range(len(cmd) + 1)) + ";1" +# prepend a 0 keyframe +cvals = f"{caret_xs[0]:.1f};" + cvals +ckeys = "0;" + ckeys +if not STATIC: + e(f'' + f'' + f'') + +# output lines: (text, color, appear_time) +oy = line_y + 30 +out_lines = [ + ("Shortest path (3 hops):", TXT_DIM, 2.95), + ("FastAPI", GREEN_HI, 3.25), + (" --uses--> DefaultPlaceholder", TXT, 3.95), + (" <--references-- get_request_handler()", TXT, 4.65), + (" --references--> ModelField", TXT, 5.35), +] +for i, (txt, col, t) in enumerate(out_lines): + y = oy + i * 22 + disp = txt.replace("&", "&").replace("<", "<").replace(">", ">") + e(f'{disp}{reveal(t)}') + +# caption in the terminal footer +cap_y = H - 30 +e(f'' + f'3 hops. Zero files opened.{reveal(5.95)}') + +# ---------------------------------------------------------------- graph +# path nodes (visiting order, reads left-to-right) +GX0, GY0, GXW, GYH = PANEL, 0, W - PANEL, H +path_nodes = [ + ("FastAPI", (PANEL + 70, 205), "below"), + ("DefaultPlaceholder", (PANEL + 195, 118), "above"), + ("get_request_handler()", (PANEL + 335, 210), "below"), + ("ModelField", (PANEL + 470, 120), "above"), +] +path_light_t = [3.25, 3.95, 4.65, 5.35] +rel_labels = ["uses", "references", "references"] + +# distractor constellation +random.seed(42) +distractors = [] +attempts = 0 +avoid = [p[1] for p in path_nodes] +while len(distractors) < 26 and attempts < 4000: + attempts += 1 + x = random.uniform(PANEL + 30, W - 28) + y = random.uniform(30, H - 26) + if all((x-ax)**2 + (y-ay)**2 > 44**2 for ax, ay in avoid) and \ + all((x-dx)**2 + (y-dy)**2 > 34**2 for dx, dy in distractors): + distractors.append((x, y)) + +# dim edges: connect each distractor to a couple of near neighbors / path nodes +allpts = distractors + [p[1] for p in path_nodes] +dim_edges = set() +for i, (x, y) in enumerate(distractors): + dists = sorted(range(len(allpts)), key=lambda j: (allpts[j][0]-x)**2 + (allpts[j][1]-y)**2) + for j in dists[1:3]: + a, b = min(i, j + 0 if j < len(distractors) else j), max(i, j) + dim_edges.add((i, j)) + +e('') +for i, j in dim_edges: + x1, y1 = allpts[i]; x2, y2 = allpts[j] + e(f'') +for (x, y) in distractors: + e(f'') +e('') + +# path base (dim) edges + green draw-on overlays +def line_len(p, q): + return math.hypot(q[0]-p[0], q[1]-p[1]) + +for i in range(3): + p = path_nodes[i][1]; q = path_nodes[i+1][1] + e(f'') + +for i in range(3): + p = path_nodes[i][1]; q = path_nodes[i+1][1] + L = line_len(p, q) + start = path_light_t[i] + 0.05 + draw = 0.55 + a = start / T; b = (start + draw) / T + vs, ks = kt((0, f"{L:.1f}"), (a, f"{L:.1f}"), (b, "0"), (HOLD_END, "0"), (1, f"{L:.1f}")) + off0 = "0" if STATIC else f"{L:.1f}" + anim = "" if STATIC else f'' + e(f'{anim}') + # relationship label at edge midpoint + mx, my = (p[0]+q[0])/2, (p[1]+q[1])/2 - 6 + e(f'{rel_labels[i]}{reveal(start + 0.25)}') + +# path nodes: glow + circle pop + label +for (name, (x, y), pos), t in zip(path_nodes, path_light_t): + a = t / T + # glow halo + ga, gb = a, (t + 0.15) / T + gvs, gks = kt((0, "0"), (ga, "0"), (gb, "1"), (HOLD_END, "1"), (1, "0")) + ganim = "" if STATIC else f'' + e(f'{ganim}') + # node: fill dim->green, radius pop + fvs, fks = kt((0, DIM_NODE), (a, DIM_NODE), ((t+0.06)/T, GREEN_HI), ((t+0.2)/T, GREEN), (HOLD_END, GREEN), (1, DIM_NODE)) + rvs, rks = kt((0, "4"), (a, "4"), ((t+0.05)/T, "8"), ((t+0.18)/T, "5.6"), (HOLD_END, "5.6"), (1, "4")) + nfill = GREEN if STATIC else DIM_NODE + nr = "5.6" if STATIC else "4" + nanim = "" if STATIC else ( + f'' + f'') + e(f'{nanim}') + # label + ly = y - 14 if pos == "above" else y + 22 + e(f'{name}{reveal(t + 0.1)}') + +e('') + +with open("docs/demo-path.svg", "w", encoding="utf-8") as f: + f.write("".join(out)) +print("wrote docs/demo-path.svg", sum(len(s) for s in out), "bytes")