codegraph-network 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codegraph/__init__.py +1 -1
- codegraph/cli.py +7 -0
- codegraph/ui.html +262 -0
- codegraph/ui.py +196 -0
- {codegraph_network-0.1.0.dist-info → codegraph_network-0.2.0.dist-info}/METADATA +13 -1
- {codegraph_network-0.1.0.dist-info → codegraph_network-0.2.0.dist-info}/RECORD +10 -8
- {codegraph_network-0.1.0.dist-info → codegraph_network-0.2.0.dist-info}/WHEEL +0 -0
- {codegraph_network-0.1.0.dist-info → codegraph_network-0.2.0.dist-info}/entry_points.txt +0 -0
- {codegraph_network-0.1.0.dist-info → codegraph_network-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {codegraph_network-0.1.0.dist-info → codegraph_network-0.2.0.dist-info}/top_level.txt +0 -0
codegraph/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.2.0"
|
codegraph/cli.py
CHANGED
|
@@ -44,6 +44,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
44
44
|
ap.add_argument("-r", "--root", default=".", help="repository root (default: cwd)")
|
|
45
45
|
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
46
46
|
sub.add_parser("serve", help="run the MCP server over stdio")
|
|
47
|
+
ui = sub.add_parser("ui", help="open an interactive graph explorer in the browser (local only)")
|
|
48
|
+
ui.add_argument("--port", type=int, default=0, help="port to listen on (default: any free port)")
|
|
49
|
+
ui.add_argument("--no-open", action="store_true", help="print the URL instead of opening a browser")
|
|
47
50
|
bench = sub.add_parser("bench", help="measure tokens and recall (lexical vs hybrid) on a tasks file")
|
|
48
51
|
bench.add_argument("tasks", help="YAML/JSON list of {task: ..., expect: [symbols]}")
|
|
49
52
|
bench.add_argument("-k", type=int, default=10, help="results per search (default 10)")
|
|
@@ -94,6 +97,10 @@ def main(argv=None):
|
|
|
94
97
|
from .server import main as serve
|
|
95
98
|
return serve(args.root)
|
|
96
99
|
|
|
100
|
+
if args.cmd == "ui":
|
|
101
|
+
from .ui import serve as ui_serve
|
|
102
|
+
return ui_serve(args.root, args.port, not args.no_open)
|
|
103
|
+
|
|
97
104
|
if args.cmd == "bench":
|
|
98
105
|
from .bench import run as bench
|
|
99
106
|
print(bench(args.root, args.tasks, args.k))
|
codegraph/ui.html
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
3
|
+
<title>Codegraph Explorer</title>
|
|
4
|
+
<style>
|
|
5
|
+
:root{--bg:#fafafa;--panel:#fff;--fg:#1a1a1a;--mute:#6b7280;--line:#d1d5db;--edge:#9ca3af;--acc:#2563eb;--hot:#dc2626;--code:#f3f4f6}
|
|
6
|
+
@media (prefers-color-scheme:dark){:root{--bg:#0f1115;--panel:#171a21;--fg:#e5e7eb;--mute:#9ca3af;--line:#2d333d;--edge:#6b7280;--acc:#60a5fa;--hot:#f87171;--code:#0b0d11}}
|
|
7
|
+
*{box-sizing:border-box}
|
|
8
|
+
html,body{margin:0;height:100%;background:var(--bg);color:var(--fg);font:13px/1.4 system-ui,sans-serif;overflow:hidden}
|
|
9
|
+
#top{position:fixed;left:0;right:0;top:0;height:44px;display:flex;gap:8px;align-items:center;padding:0 12px;background:var(--panel);border-bottom:1px solid var(--line);z-index:5}
|
|
10
|
+
#top b{margin-right:4px}
|
|
11
|
+
input,select,button{font:inherit;color:var(--fg);background:var(--bg);border:1px solid var(--line);border-radius:6px;padding:5px 9px}
|
|
12
|
+
button{cursor:pointer}button:hover{border-color:var(--acc)}
|
|
13
|
+
button.on{border-color:var(--acc);color:var(--acc)}
|
|
14
|
+
#q{width:260px}
|
|
15
|
+
#res{position:fixed;top:44px;left:96px;width:380px;max-height:60vh;overflow:auto;background:var(--panel);border:1px solid var(--line);border-radius:6px;display:none;z-index:6}
|
|
16
|
+
#res div{padding:6px 10px;cursor:pointer;border-bottom:1px solid var(--line)}#res div:hover{background:var(--code)}
|
|
17
|
+
#res small,.mute{color:var(--mute)}
|
|
18
|
+
#wrap{position:fixed;left:0;top:44px;bottom:0;right:380px}
|
|
19
|
+
canvas{display:block;width:100%;height:100%;cursor:grab}
|
|
20
|
+
canvas.drag{cursor:grabbing}
|
|
21
|
+
#side{position:fixed;top:44px;right:0;bottom:0;width:380px;background:var(--panel);border-left:1px solid var(--line);overflow:auto;padding:14px}
|
|
22
|
+
#side h3{margin:0 0 2px;word-break:break-all}
|
|
23
|
+
#side pre{background:var(--code);padding:10px;border-radius:6px;overflow:auto;max-height:340px;font:12px/1.45 ui-monospace,Menlo,monospace}
|
|
24
|
+
#side .btns{display:flex;flex-wrap:wrap;gap:6px;margin:10px 0}
|
|
25
|
+
#side .nb{padding:3px 0;cursor:pointer;word-break:break-all}#side .nb:hover{color:var(--acc)}
|
|
26
|
+
#side h4{margin:14px 0 4px;color:var(--mute);font-weight:600;text-transform:uppercase;font-size:11px;letter-spacing:.04em}
|
|
27
|
+
#hud{position:fixed;left:12px;bottom:10px;color:var(--mute);background:var(--bg);padding:4px 8px;border-radius:6px;opacity:.92;pointer-events:none}
|
|
28
|
+
#tip{position:fixed;pointer-events:none;background:var(--panel);border:1px solid var(--line);padding:3px 8px;border-radius:5px;display:none;z-index:7;max-width:50ch;word-break:break-all}
|
|
29
|
+
@media (max-width:800px){#side{width:100%;top:auto;height:45%}#wrap{right:0;bottom:45%}}
|
|
30
|
+
</style></head><body>
|
|
31
|
+
<div id="top"><b>Codegraph</b>
|
|
32
|
+
<input id="q" placeholder="Search a symbol and add it to the graph" autocomplete="off">
|
|
33
|
+
<button id="fit" title="Zoom to fit everything">Fit</button>
|
|
34
|
+
<button id="unpin" title="Release all dragged nodes">Unpin all</button>
|
|
35
|
+
<button id="reset" title="Back to the file overview">Reset</button>
|
|
36
|
+
<select id="color" title="Colour by"><option value="c">colour: community</option><option value="d">colour: folder</option></select>
|
|
37
|
+
<button id="lbl" class="on" title="Show labels">Labels</button>
|
|
38
|
+
</div>
|
|
39
|
+
<div id="res"></div>
|
|
40
|
+
<div id="wrap"><canvas id="c"></canvas></div>
|
|
41
|
+
<div id="side"></div>
|
|
42
|
+
<div id="hud"></div><div id="tip"></div>
|
|
43
|
+
<script>
|
|
44
|
+
"use strict";
|
|
45
|
+
const $=id=>document.getElementById(id), cv=$('c'), cx=cv.getContext('2d'), side=$('side'), tip=$('tip'), hud=$('hud');
|
|
46
|
+
const api=(p,q={})=>fetch('/api/'+p+'?'+new URLSearchParams(q)).then(r=>r.json());
|
|
47
|
+
const el=(t,props={},...kids)=>{const e=document.createElement(t);Object.assign(e,props);kids.forEach(k=>e.append(k));return e};
|
|
48
|
+
|
|
49
|
+
// ---- state ------------------------------------------------------------------------------------
|
|
50
|
+
let nodes=new Map(), links=[], fileLinks=[], symEdges=new Map(), symInfo=new Map(), total=0;
|
|
51
|
+
let sel=null, hov=null, impact=new Set(), alpha=1, ticks=0, auto=true, showLabels=true, colorBy='c', dirty=true;
|
|
52
|
+
const cam={x:0,y:0,k:1}; let W=0,H=0;
|
|
53
|
+
const dpr=devicePixelRatio||1;
|
|
54
|
+
let css={};
|
|
55
|
+
function readCss(){const s=getComputedStyle(document.documentElement);['bg','fg','mute','edge','acc','hot','line'].forEach(k=>css[k]=s.getPropertyValue('--'+k).trim())}
|
|
56
|
+
matchMedia('(prefers-color-scheme:dark)').addEventListener('change',()=>{readCss();dirty=true});readCss();
|
|
57
|
+
|
|
58
|
+
function resize(){const r=cv.getBoundingClientRect();W=r.width;H=r.height;cv.width=W*dpr;cv.height=H*dpr;dirty=true}
|
|
59
|
+
addEventListener('resize',resize);resize();
|
|
60
|
+
|
|
61
|
+
// ---- colours ----------------------------------------------------------------------------------
|
|
62
|
+
const hue=n=>`hsl(${(n*137.5)%360},62%,52%)`;
|
|
63
|
+
function hash(s){let h=0;for(let i=0;i<s.length;i++)h=(h*31+s.charCodeAt(i))|0;return Math.abs(h)}
|
|
64
|
+
const dir=f=>f.split('/').slice(0,-1).slice(0,2).join('/')||'.';
|
|
65
|
+
function colorOf(n){
|
|
66
|
+
if(colorBy==='d')return hue(hash(dir(n.file)));
|
|
67
|
+
return n.c<0?'#9ca3af':hue(n.c)}
|
|
68
|
+
|
|
69
|
+
// ---- graph building ---------------------------------------------------------------------------
|
|
70
|
+
const rnd=()=>(Math.random()-.5);
|
|
71
|
+
function addFile(f){
|
|
72
|
+
nodes.set('f:'+f.id,{key:'f:'+f.id,t:'f',label:f.id.split('/').pop(),file:f.id,c:f.c,n:f.n,loc:f.loc,rank:f.rank,
|
|
73
|
+
r:7+Math.sqrt(f.n)*1.7,x:rnd()*W*.8,y:rnd()*H*.8,vx:0,vy:0})}
|
|
74
|
+
function symNode(s,ax,ay){
|
|
75
|
+
const k='s:'+s.id;symInfo.set(s.id,s);
|
|
76
|
+
if(nodes.has(k))return nodes.get(k);
|
|
77
|
+
const n={key:k,t:'s',id:s.id,label:s.name,file:s.file,c:s.c,kind:s.kind,rank:s.rank,
|
|
78
|
+
r:3.5+Math.min(14,Math.sqrt(s.rank*2500)),x:ax+rnd()*40,y:ay+rnd()*40,vx:0,vy:0};
|
|
79
|
+
nodes.set(k,n);return n}
|
|
80
|
+
const touched=f=>{for(const n of nodes.values())if(n.t==='s'&&n.file===f)return true;return false};
|
|
81
|
+
function entity(id){
|
|
82
|
+
const k='s:'+id;if(nodes.has(k))return nodes.get(k);
|
|
83
|
+
const s=symInfo.get(id);return s&&nodes.get('f:'+s.file)||null}
|
|
84
|
+
function rebuild(){
|
|
85
|
+
const m=new Map(), tf=new Set();
|
|
86
|
+
for(const n of nodes.values())if(n.t==='s')tf.add(n.file);
|
|
87
|
+
const add=(a,b,w,k,q)=>{if(!a||!b||a===b)return;const key=a.key+'>'+b.key,o=m.get(key);
|
|
88
|
+
if(o){o.w+=w;if(q==='extracted')o.q=q}else m.set(key,{a,b,w,k,q})};
|
|
89
|
+
for(const l of fileLinks)if(!tf.has(l.s)&&!tf.has(l.t))add(nodes.get('f:'+l.s),nodes.get('f:'+l.t),l.w,l.k,l.k==='import'?'import':'extracted');
|
|
90
|
+
for(const e of symEdges.values())add(entity(e.s),entity(e.t),1,e.k,e.q);
|
|
91
|
+
links=[...m.values()];dirty=true}
|
|
92
|
+
function reheat(a=.5){alpha=Math.max(alpha,a);dirty=true}
|
|
93
|
+
|
|
94
|
+
async function loadOverview(){
|
|
95
|
+
nodes=new Map();symEdges=new Map();symInfo=new Map();sel=null;impact=new Set();auto=true;ticks=0;
|
|
96
|
+
hud.textContent='loading...';
|
|
97
|
+
const d=await api('overview');
|
|
98
|
+
total=d.total;fileLinks=d.links;d.files.forEach(f=>{fileMeta.set(f.id,f);addFile(f)});rebuild();reheat(1);panel();status()}
|
|
99
|
+
function status(){
|
|
100
|
+
const ns=[...nodes.values()].filter(n=>n.t==='s').length, nf=nodes.size-ns;
|
|
101
|
+
hud.textContent=`${nf} files, ${ns} symbols shown · ${total} files indexed · drag a node to pin it · double-click to expand · wheel to zoom · drag background to pan`}
|
|
102
|
+
|
|
103
|
+
async function expandFile(path){
|
|
104
|
+
const d=await api('file',{path});const fn=nodes.get('f:'+path);
|
|
105
|
+
const ax=fn?fn.x:0, ay=fn?fn.y:0;
|
|
106
|
+
d.symbols.forEach(s=>symNode(s,ax,ay));d.far.forEach(s=>symInfo.set(s.id,s));
|
|
107
|
+
d.edges.forEach(e=>symEdges.set(e.s+'>'+e.t+e.k,e));
|
|
108
|
+
nodes.delete('f:'+path);rebuild();reheat(.8);status();return d}
|
|
109
|
+
function collapseFile(path){
|
|
110
|
+
let x=0,y=0,c=0;
|
|
111
|
+
for(const [k,n] of nodes)if(n.t==='s'&&n.file===path){x+=n.x;y+=n.y;c++;nodes.delete(k)}
|
|
112
|
+
if(!c)return;
|
|
113
|
+
const info=fileMeta.get(path)||{id:path,n:c,loc:0,rank:0,c:-1};
|
|
114
|
+
addFile(info);const n=nodes.get('f:'+path);n.x=x/c;n.y=y/c;
|
|
115
|
+
if(sel&&!nodes.has(sel))sel='f:'+path;
|
|
116
|
+
rebuild();reheat(.5);status();panel()}
|
|
117
|
+
const fileMeta=new Map();
|
|
118
|
+
async function expandSymbol(id){
|
|
119
|
+
const d=await api('symbol',{id});if(d.error)return;
|
|
120
|
+
const me=symNode(d,W/2,H/2);
|
|
121
|
+
d.far.sort((a,b)=>b.rank-a.rank).slice(0,40).forEach(s=>symNode(s,me.x,me.y));
|
|
122
|
+
d.edges.forEach(e=>symEdges.set(e.s+'>'+e.t+e.k,e));
|
|
123
|
+
rebuild();reheat(.7);status();return d}
|
|
124
|
+
async function showImpact(id){
|
|
125
|
+
const d=await api('impact',{id});impact=new Set([id]);
|
|
126
|
+
d.symbols.forEach(s=>{symNode(s,nodes.get('s:'+id).x,nodes.get('s:'+id).y);impact.add(s.id)});
|
|
127
|
+
d.edges.forEach(e=>symEdges.set(e.s+'>'+e.t+e.k,e));
|
|
128
|
+
rebuild();reheat(.7);status();panel()}
|
|
129
|
+
|
|
130
|
+
// ---- force layout -----------------------------------------------------------------------------
|
|
131
|
+
function step(){
|
|
132
|
+
const arr=[...nodes.values()], n=arr.length;
|
|
133
|
+
const cen=new Map(); // symbols of one file are pulled together so an expanded file reads as a cluster
|
|
134
|
+
for(const a of arr)if(a.t==='s'){const c=cen.get(a.file)||(cen.set(a.file,{x:0,y:0,n:0}),cen.get(a.file));c.x+=a.x;c.y+=a.y;c.n++}
|
|
135
|
+
for(let i=0;i<n;i++){const a=arr[i];
|
|
136
|
+
for(let j=i+1;j<n;j++){const b=arr[j];let dx=a.x-b.x,dy=a.y-b.y,d2=dx*dx+dy*dy+.01;
|
|
137
|
+
if(d2<90000){const f=(1400+(a.r+b.r)*30)/d2;dx*=f;dy*=f;a.vx+=dx;a.vy+=dy;b.vx-=dx;b.vy-=dy}}
|
|
138
|
+
a.vx-=a.x*.004;a.vy-=a.y*.004;
|
|
139
|
+
if(a.t==='s'){const c=cen.get(a.file);if(c.n>1){a.vx+=(c.x/c.n-a.x)*.01;a.vy+=(c.y/c.n-a.y)*.01}}}
|
|
140
|
+
for(const l of links){const a=l.a,b=l.b;let dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;
|
|
141
|
+
const rest=a.r+b.r+(a.t==='s'&&a.file===b.file?22:70),f=(d-rest)*.012*(l.q==='extracted'?1:.6);
|
|
142
|
+
dx*=f/d;dy*=f/d;a.vx+=dx;a.vy+=dy;b.vx-=dx;b.vy-=dy}
|
|
143
|
+
for(const a of arr){
|
|
144
|
+
if(a.pin){a.vx=a.vy=0;continue}
|
|
145
|
+
a.vx*=.8;a.vy*=.8;a.x+=a.vx*alpha;a.y+=a.vy*alpha}
|
|
146
|
+
alpha=Math.max(.0,alpha*.985);ticks++;
|
|
147
|
+
if(auto&&nodes.size&&(ticks===60||alpha<.013)){fit()}}
|
|
148
|
+
|
|
149
|
+
// ---- drawing ----------------------------------------------------------------------------------
|
|
150
|
+
const toS=(x,y)=>[(x-cam.x)*cam.k+W/2,(y-cam.y)*cam.k+H/2], toW=(x,y)=>[(x-W/2)/cam.k+cam.x,(y-H/2)/cam.k+cam.y];
|
|
151
|
+
function fit(){
|
|
152
|
+
if(!nodes.size)return;let x0=1e9,y0=1e9,x1=-1e9,y1=-1e9;
|
|
153
|
+
for(const n of nodes.values()){x0=Math.min(x0,n.x-n.r);x1=Math.max(x1,n.x+n.r);y0=Math.min(y0,n.y-n.r);y1=Math.max(y1,n.y+n.r)}
|
|
154
|
+
cam.k=Math.min(2.5,.9*Math.min(W/(x1-x0||1),H/(y1-y0||1)));cam.x=(x0+x1)/2;cam.y=(y0+y1)/2;dirty=true}
|
|
155
|
+
function neighbours(key){const s=new Set([key]);for(const l of links){if(l.a.key===key)s.add(l.b.key);if(l.b.key===key)s.add(l.a.key)}return s}
|
|
156
|
+
function arrow(x,y,ang,sz){cx.beginPath();cx.moveTo(x,y);cx.lineTo(x-sz*Math.cos(ang-.45),y-sz*Math.sin(ang-.45));
|
|
157
|
+
cx.lineTo(x-sz*Math.cos(ang+.45),y-sz*Math.sin(ang+.45));cx.closePath();cx.fill()}
|
|
158
|
+
function draw(){
|
|
159
|
+
cx.setTransform(dpr,0,0,dpr,0,0);cx.clearRect(0,0,W,H);
|
|
160
|
+
const focus=sel&&nodes.has(sel)?neighbours(sel):null, k=cam.k;
|
|
161
|
+
// soft hulls around expanded files
|
|
162
|
+
const groups=new Map();
|
|
163
|
+
for(const n of nodes.values())if(n.t==='s'){const g=groups.get(n.file)||(groups.set(n.file,{x:0,y:0,n:0,m:[]}),groups.get(n.file));g.x+=n.x;g.y+=n.y;g.n++;g.m.push(n)}
|
|
164
|
+
for(const [f,g] of groups){const mx=g.x/g.n,my=g.y/g.n;let r=0;g.m.forEach(m=>r+=Math.hypot(m.x-mx,m.y-my));r=r/g.n*1.5+10;
|
|
165
|
+
const [sx,sy]=toS(mx,my);cx.globalAlpha=.07;cx.fillStyle=colorOf(g.m[0]);cx.beginPath();cx.arc(sx,sy,(r+14)*k,0,6.3);cx.fill();
|
|
166
|
+
cx.globalAlpha=.7;cx.fillStyle=css.mute;cx.font='11px system-ui';cx.textAlign='center';cx.fillText(f.split('/').pop(),sx,sy-(r+18)*k)}
|
|
167
|
+
for(const l of links){
|
|
168
|
+
const on=!focus||(focus.has(l.a.key)&&focus.has(l.b.key)), imp=impact.size&&l.a.id!==undefined&&impact.has(l.a.id)&&impact.has(l.b.id);
|
|
169
|
+
const [ax,ay]=toS(l.a.x,l.a.y),[bx,by]=toS(l.b.x,l.b.y);
|
|
170
|
+
cx.globalAlpha=on?(imp?.9:focus?.6:.3):.04;cx.strokeStyle=cx.fillStyle=imp?css.hot:css.edge;
|
|
171
|
+
cx.lineWidth=Math.min(5,1+Math.log2(l.w))*(imp?1.6:1);
|
|
172
|
+
cx.setLineDash(l.k==='import'?[1,4]:l.q==='extracted'?[]:[5,4]);
|
|
173
|
+
cx.beginPath();cx.moveTo(ax,ay);cx.lineTo(bx,by);cx.stroke();
|
|
174
|
+
if(l.k!=='import'&&k>.35){cx.setLineDash([]);const ang=Math.atan2(by-ay,bx-ax),rr=l.b.r*k+1;arrow(bx-Math.cos(ang)*rr,by-Math.sin(ang)*rr,ang,6)}}
|
|
175
|
+
cx.setLineDash([]);
|
|
176
|
+
for(const n of nodes.values()){
|
|
177
|
+
const [sx,sy]=toS(n.x,n.y), r=n.r*k, dim=focus&&!focus.has(n.key);
|
|
178
|
+
cx.globalAlpha=dim?.12:1;cx.fillStyle=colorOf(n);
|
|
179
|
+
cx.beginPath();
|
|
180
|
+
if(n.t==='f'){const s=Math.max(r*1.6,4);cx.roundRect(sx-s/2,sy-s/2,s,s,s*.22)}else cx.arc(sx,sy,Math.max(r,1.5),0,6.3);
|
|
181
|
+
cx.fill();
|
|
182
|
+
if(n.pin){cx.strokeStyle=css.fg;cx.lineWidth=1.2;cx.stroke()}
|
|
183
|
+
if(n.key===sel||n.key===hov||(n.t==='s'&&impact.has(n.id))){cx.strokeStyle=n.key===sel?css.acc:css.hot;cx.lineWidth=2.5;cx.beginPath();
|
|
184
|
+
cx.arc(sx,sy,Math.max(r,3)+(n.t==='f'?r*.4:2)+2,0,6.3);cx.stroke()}
|
|
185
|
+
const show=showLabels&&!dim&&(n.key===sel||n.key===hov||(focus&&focus.has(n.key))||k>1.5||(n.t==='f'&&(n.r>13||nodes.size<80))||(n.t==='s'&&k>.9&&n.r>7));
|
|
186
|
+
if(show){cx.globalAlpha=1;cx.fillStyle=css.fg;cx.font=(n.key===sel?'bold ':'')+'12px system-ui';cx.textAlign='left';cx.fillText(n.label,sx+Math.max(r,3)+4,sy+4)}}
|
|
187
|
+
cx.globalAlpha=1}
|
|
188
|
+
(function loop(){if(alpha>.012){step();dirty=true}if(dirty){draw();dirty=false}requestAnimationFrame(loop)})();
|
|
189
|
+
|
|
190
|
+
// ---- interaction ------------------------------------------------------------------------------
|
|
191
|
+
function pick(ev){const r=cv.getBoundingClientRect(),[wx,wy]=toW(ev.clientX-r.left,ev.clientY-r.top);let best=null,bd=1e9;
|
|
192
|
+
for(const n of nodes.values()){const d=Math.hypot(n.x-wx,n.y-wy),rr=Math.max(n.r,4/cam.k)+3/cam.k;if(d<rr&&d<bd){bd=d;best=n}}return best}
|
|
193
|
+
let drag=null, pan=null, moved=false;
|
|
194
|
+
cv.addEventListener('pointerdown',e=>{cv.setPointerCapture(e.pointerId);moved=false;const n=pick(e);
|
|
195
|
+
if(n){drag=n;cv.classList.add('drag')}else{pan={x:e.clientX,y:e.clientY,cx:cam.x,cy:cam.y};cv.classList.add('drag')}});
|
|
196
|
+
cv.addEventListener('pointermove',e=>{const r=cv.getBoundingClientRect();
|
|
197
|
+
if(drag){moved=true;auto=false;const [wx,wy]=toW(e.clientX-r.left,e.clientY-r.top);drag.x=wx;drag.y=wy;drag.pin=true;reheat(.25);return}
|
|
198
|
+
if(pan){moved=true;auto=false;cam.x=pan.cx-(e.clientX-pan.x)/cam.k;cam.y=pan.cy-(e.clientY-pan.y)/cam.k;dirty=true;return}
|
|
199
|
+
const n=pick(e);if(n&&n.key!==hov||!n&&hov){hov=n?n.key:null;dirty=true}
|
|
200
|
+
if(n){tip.textContent=n.t==='f'?`${n.file} (${n.n} symbols, ${n.loc} loc)`:`${n.kind} ${symInfo.get(n.id).qn} ${n.file}`;
|
|
201
|
+
tip.style.display='block';tip.style.left=e.clientX+14+'px';tip.style.top=e.clientY+14+'px'}else tip.style.display='none'});
|
|
202
|
+
cv.addEventListener('pointerup',e=>{cv.classList.remove('drag');
|
|
203
|
+
if(drag&&!moved){select(drag.key)}else if(!drag&&pan&&!moved){select(null)}
|
|
204
|
+
drag=pan=null});
|
|
205
|
+
cv.addEventListener('dblclick',async e=>{const n=pick(e);if(!n)return;n.pin=false;
|
|
206
|
+
if(n.t==='f')await expandFile(n.file);else await expandSymbol(n.id);select(n.t==='f'?null:n.key)});
|
|
207
|
+
cv.addEventListener('wheel',e=>{e.preventDefault();auto=false;const r=cv.getBoundingClientRect(),mx=e.clientX-r.left,my=e.clientY-r.top;
|
|
208
|
+
const [wx,wy]=toW(mx,my);cam.k=Math.min(8,Math.max(.05,cam.k*Math.exp(-e.deltaY*.0015)));
|
|
209
|
+
cam.x=wx-(mx-W/2)/cam.k;cam.y=wy-(my-H/2)/cam.k;dirty=true},{passive:false});
|
|
210
|
+
$('fit').onclick=()=>{auto=true;fit()};
|
|
211
|
+
$('unpin').onclick=()=>{nodes.forEach(n=>n.pin=false);reheat(.6)};
|
|
212
|
+
$('reset').onclick=()=>loadOverview();
|
|
213
|
+
$('color').onchange=e=>{colorBy=e.target.value;dirty=true};
|
|
214
|
+
$('lbl').onclick=e=>{showLabels=!showLabels;e.target.classList.toggle('on',showLabels);dirty=true};
|
|
215
|
+
|
|
216
|
+
// ---- side panel -------------------------------------------------------------------------------
|
|
217
|
+
function select(key){sel=key;dirty=true;panel()}
|
|
218
|
+
function btn(text,fn,title=''){return el('button',{textContent:text,title,onclick:fn})}
|
|
219
|
+
async function focusSymbol(id,s){ // bring any symbol into the graph and select it
|
|
220
|
+
if(!nodes.has('s:'+id)){if(s)symInfo.set(id,s);await expandSymbol(id)}
|
|
221
|
+
const n=nodes.get('s:'+id);if(n){cam.x=n.x;cam.y=n.y;cam.k=Math.max(cam.k,1);select(n.key)}}
|
|
222
|
+
async function panel(){
|
|
223
|
+
side.replaceChildren();const n=sel&&nodes.get(sel);
|
|
224
|
+
if(!n){side.append(el('h3',{textContent:'Explore'}),el('p',{className:'mute',textContent:
|
|
225
|
+
'Each square is a file, sized by symbol count; lines are calls and imports between files. Double-click a file to open it into its functions and classes, double-click a function to pull in everything it calls and everything that calls it. Drag nodes to rearrange and pin them. Search to jump straight to a symbol.'}));return}
|
|
226
|
+
if(n.t==='f'){
|
|
227
|
+
side.append(el('h3',{textContent:n.file}),el('div',{className:'mute',textContent:`${n.n} symbols · ${n.loc} lines`}),
|
|
228
|
+
el('div',{className:'btns'},btn('Expand into symbols',async()=>{await expandFile(n.file);select(null)}),
|
|
229
|
+
btn(n.pin?'Unpin':'Pin',()=>{n.pin=!n.pin;reheat(.1);panel()})));
|
|
230
|
+
const near=[];links.forEach(l=>{if(l.a===n)near.push([l.b,'→',l.w]);else if(l.b===n)near.push([l.a,'←',l.w])});
|
|
231
|
+
if(near.length){side.append(el('h4',{textContent:'Connected files'}));
|
|
232
|
+
near.sort((a,b)=>b[2]-a[2]).slice(0,25).forEach(([o,d,w])=>side.append(el('div',{className:'nb',textContent:`${d} ${o.file||o.label} ×${w}`,onclick:()=>{if(o.t==='f'||o.t==='s')select(o.key)}})))}
|
|
233
|
+
return}
|
|
234
|
+
const d=await api('symbol',{id:n.id});if(sel!==n.key)return;
|
|
235
|
+
side.append(el('h3',{textContent:d.qn}),el('div',{className:'mute',textContent:`${d.kind} · ${d.file}:${d.l1}-${d.l2}`}));
|
|
236
|
+
if(d.sig)side.append(el('div',{style:'margin-top:6px;font-family:ui-monospace,Menlo,monospace;word-break:break-all',textContent:d.sig}));
|
|
237
|
+
if(d.doc)side.append(el('p',{className:'mute',textContent:d.doc.slice(0,400)}));
|
|
238
|
+
side.append(el('div',{className:'btns'},
|
|
239
|
+
btn('Expand neighbours',async()=>{await expandSymbol(n.id);panel()},'pull in callers and callees'),
|
|
240
|
+
btn(impact.has(n.id)&&impact.size>1?'Hide impact':'Show impact',async()=>{if(impact.size>1){impact=new Set();dirty=true;panel()}else await showImpact(n.id)},'transitive callers: what breaks if this changes'),
|
|
241
|
+
btn('Collapse file',()=>collapseFile(n.file),'fold this file back into one node'),
|
|
242
|
+
btn(n.pin?'Unpin':'Pin',()=>{n.pin=!n.pin;reheat(.1);panel()})));
|
|
243
|
+
const info=id=>symInfo.get(id)||d.far.find(s=>s.id===id);
|
|
244
|
+
const callers=d.edges.filter(e=>e.t===n.id).map(e=>[info(e.s),e]), callees=d.edges.filter(e=>e.s===n.id).map(e=>[info(e.t),e]);
|
|
245
|
+
const list=(title,rows)=>{if(!rows.length)return;side.append(el('h4',{textContent:`${title} (${rows.length})`}));
|
|
246
|
+
rows.slice(0,40).forEach(([s,e])=>s&&side.append(el('div',{className:'nb',onclick:()=>focusSymbol(s.id,s),
|
|
247
|
+
textContent:`${s.qn}${e.q&&e.q!=='extracted'?' ?':''} ${s.file}`})))};
|
|
248
|
+
list('Called by',callers);list('Calls / uses',callees);
|
|
249
|
+
side.append(el('h4',{textContent:'Source'}),el('pre',{textContent:d.source||'(no source)'}))}
|
|
250
|
+
|
|
251
|
+
// ---- search -----------------------------------------------------------------------------------
|
|
252
|
+
let tm;const res=$('res');
|
|
253
|
+
$('q').addEventListener('input',e=>{clearTimeout(tm);const v=e.target.value.trim();
|
|
254
|
+
if(!v){res.style.display='none';return}
|
|
255
|
+
tm=setTimeout(async()=>{const d=await api('search',{q:v});res.replaceChildren();
|
|
256
|
+
d.symbols.forEach(s=>res.append(el('div',{onclick:()=>{res.style.display='none';focusSymbol(s.id,s)}},
|
|
257
|
+
el('span',{textContent:s.qn}),el('small',{textContent:` ${s.kind} · ${s.file}`}))));
|
|
258
|
+
if(!d.symbols.length)res.append(el('div',{className:'mute',textContent:'no matches'}));res.style.display='block'},180)});
|
|
259
|
+
addEventListener('keydown',e=>{if(e.key==='Escape'){res.style.display='none';select(null)}if(e.key==='/'&&document.activeElement!==$('q')){e.preventDefault();$('q').focus()}});
|
|
260
|
+
|
|
261
|
+
loadOverview();
|
|
262
|
+
</script></body></html>
|
codegraph/ui.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""`codegraph ui`: an interactive, local-only graph explorer served from the existing index.
|
|
2
|
+
|
|
3
|
+
Stdlib only. The page (ui.html) starts from the file-level network and lets you drag nodes, expand a
|
|
4
|
+
file into its symbols, pull in a symbol's neighbours and read source, all through the JSON API below.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
import webbrowser
|
|
12
|
+
from collections import Counter, defaultdict, deque
|
|
13
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
14
|
+
from importlib import resources
|
|
15
|
+
from urllib.parse import parse_qs, urlparse
|
|
16
|
+
|
|
17
|
+
from .indexer import index_repo
|
|
18
|
+
from .queries import Graph
|
|
19
|
+
|
|
20
|
+
OVERVIEW_FILES = 500 # a force layout in a browser canvas stays smooth up to roughly this many nodes
|
|
21
|
+
IMPACT_LIMIT = 80
|
|
22
|
+
HIDDEN = ("module", "impl", "namespace")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _sym(r) -> dict:
|
|
26
|
+
return {"id": r["id"], "name": r["name"], "qn": r["qualname"], "kind": r["kind"], "file": r["file"],
|
|
27
|
+
"l1": r["line_start"], "l2": r["line_end"], "rank": r["rank"], "c": r["community"]}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Api:
|
|
31
|
+
"""Read-only queries over one Graph. Every method returns JSON-serialisable data."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, graph: Graph):
|
|
34
|
+
self.g = graph
|
|
35
|
+
|
|
36
|
+
def overview(self, limit: int = OVERVIEW_FILES) -> dict:
|
|
37
|
+
with self.g._lock:
|
|
38
|
+
db = self.g.db
|
|
39
|
+
files = {}
|
|
40
|
+
for r in db.execute(
|
|
41
|
+
f"SELECT file, COUNT(*) n, SUM(rank) rank FROM symbols WHERE kind NOT IN {HIDDEN!r} GROUP BY file"):
|
|
42
|
+
files[r["file"]] = {"id": r["file"], "n": r["n"], "rank": r["rank"] or 0.0, "loc": 0, "c": -1}
|
|
43
|
+
for r in db.execute("SELECT path, loc, lang FROM files"):
|
|
44
|
+
if r["path"] in files:
|
|
45
|
+
files[r["path"]].update(loc=r["loc"] or 0, lang=r["lang"])
|
|
46
|
+
votes: dict[str, Counter] = defaultdict(Counter)
|
|
47
|
+
for r in db.execute(f"SELECT file, community FROM symbols WHERE community>=0 AND kind NOT IN {HIDDEN!r}"):
|
|
48
|
+
votes[r["file"]][r["community"]] += 1
|
|
49
|
+
for f, v in votes.items():
|
|
50
|
+
if f in files:
|
|
51
|
+
files[f]["c"] = v.most_common(1)[0][0]
|
|
52
|
+
keep = {f["id"] for f in sorted(files.values(), key=lambda f: -f["rank"])[:limit]}
|
|
53
|
+
pair: Counter = Counter()
|
|
54
|
+
for r in db.execute("SELECT a.file fa, b.file fb FROM edges e JOIN symbols a ON a.id=e.src "
|
|
55
|
+
"JOIN symbols b ON b.id=e.dst WHERE a.file != b.file"):
|
|
56
|
+
if r["fa"] in keep and r["fb"] in keep:
|
|
57
|
+
pair[(r["fa"], r["fb"])] += 1
|
|
58
|
+
links = [{"s": a, "t": b, "w": w, "k": "call"} for (a, b), w in pair.items()]
|
|
59
|
+
seen = set(pair)
|
|
60
|
+
for r in db.execute("SELECT file, dst FROM imports"):
|
|
61
|
+
k = (r["file"], r["dst"])
|
|
62
|
+
if r["file"] in keep and r["dst"] in keep and k not in seen and r["file"] != r["dst"]:
|
|
63
|
+
seen.add(k)
|
|
64
|
+
links.append({"s": k[0], "t": k[1], "w": 1, "k": "import"})
|
|
65
|
+
return {"files": [files[f] for f in keep], "links": links, "total": len(files)}
|
|
66
|
+
|
|
67
|
+
def file(self, path: str) -> dict:
|
|
68
|
+
"""Symbols of one file plus every edge that touches them (the far end is described too)."""
|
|
69
|
+
with self.g._lock:
|
|
70
|
+
db = self.g.db
|
|
71
|
+
syms = [_sym(r) for r in db.execute(
|
|
72
|
+
f"SELECT * FROM symbols WHERE file=? AND kind NOT IN {HIDDEN!r} ORDER BY line_start", (path,))]
|
|
73
|
+
ids = [s["id"] for s in syms]
|
|
74
|
+
return {"symbols": syms, **self._edges_of(ids)}
|
|
75
|
+
|
|
76
|
+
def _edges_of(self, ids) -> dict:
|
|
77
|
+
db = self.g.db
|
|
78
|
+
edges, far = [], {}
|
|
79
|
+
if not ids:
|
|
80
|
+
return {"edges": edges, "far": []}
|
|
81
|
+
mark = ",".join("?" * len(ids))
|
|
82
|
+
mine = set(ids)
|
|
83
|
+
for r in db.execute(f"SELECT src, dst, kind, conf FROM edges WHERE src IN ({mark}) OR dst IN ({mark})",
|
|
84
|
+
(*ids, *ids)):
|
|
85
|
+
edges.append({"s": r["src"], "t": r["dst"], "k": r["kind"], "q": r["conf"]})
|
|
86
|
+
for o in (r["src"], r["dst"]):
|
|
87
|
+
if o not in mine:
|
|
88
|
+
far[o] = None
|
|
89
|
+
if far:
|
|
90
|
+
m2 = ",".join("?" * len(far))
|
|
91
|
+
far_rows = db.execute(f"SELECT * FROM symbols WHERE id IN ({m2}) AND kind NOT IN {HIDDEN!r}",
|
|
92
|
+
tuple(far)).fetchall()
|
|
93
|
+
ok = {r["id"] for r in far_rows}
|
|
94
|
+
edges = [e for e in edges if (e["s"] in mine or e["s"] in ok) and (e["t"] in mine or e["t"] in ok)]
|
|
95
|
+
return {"edges": edges, "far": [_sym(r) for r in far_rows]}
|
|
96
|
+
return {"edges": edges, "far": []}
|
|
97
|
+
|
|
98
|
+
def symbol(self, sid: int) -> dict:
|
|
99
|
+
with self.g._lock:
|
|
100
|
+
db = self.g.db
|
|
101
|
+
r = db.execute("SELECT * FROM symbols WHERE id=?", (sid,)).fetchone()
|
|
102
|
+
if r is None:
|
|
103
|
+
return {"error": "unknown symbol"}
|
|
104
|
+
out = _sym(r)
|
|
105
|
+
out.update(sig=r["sig"] or "", doc=r["doc"] or "", source=self.g._read(r)[:12000])
|
|
106
|
+
nb = self._edges_of([sid])
|
|
107
|
+
out["edges"], out["far"] = nb["edges"], nb["far"]
|
|
108
|
+
return out
|
|
109
|
+
|
|
110
|
+
def impact(self, sid: int, depth: int = 3) -> dict:
|
|
111
|
+
"""Transitive callers (what breaks if this changes), nearest first."""
|
|
112
|
+
with self.g._lock:
|
|
113
|
+
seen, q, order = {sid}, deque([(sid, 0)]), []
|
|
114
|
+
while q and len(order) < IMPACT_LIMIT:
|
|
115
|
+
u, d = q.popleft()
|
|
116
|
+
if d >= depth:
|
|
117
|
+
continue
|
|
118
|
+
for v in self.g._in.get(u, ()):
|
|
119
|
+
if v not in seen:
|
|
120
|
+
seen.add(v)
|
|
121
|
+
order.append(v)
|
|
122
|
+
q.append((v, d + 1))
|
|
123
|
+
order = order[:IMPACT_LIMIT]
|
|
124
|
+
if not order:
|
|
125
|
+
return {"symbols": [], "edges": []}
|
|
126
|
+
mark = ",".join("?" * len(order))
|
|
127
|
+
rows = self.g.db.execute(f"SELECT * FROM symbols WHERE id IN ({mark}) AND kind NOT IN {HIDDEN!r}",
|
|
128
|
+
tuple(order)).fetchall()
|
|
129
|
+
ids = {r["id"] for r in rows} | {sid}
|
|
130
|
+
edges = [{"s": u, "t": v, "k": "call", "q": "inferred"} for u in ids for v in self.g._out.get(u, ())
|
|
131
|
+
if v in ids]
|
|
132
|
+
return {"symbols": [_sym(r) for r in rows], "edges": edges}
|
|
133
|
+
|
|
134
|
+
def search(self, q: str) -> dict:
|
|
135
|
+
with self.g._lock:
|
|
136
|
+
rows = self.g._search_rows(q, 25) if q.strip() else []
|
|
137
|
+
return {"symbols": [_sym(r) for r in rows if r["kind"] not in HIDDEN]}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class Handler(BaseHTTPRequestHandler):
|
|
141
|
+
api: Api
|
|
142
|
+
port: int
|
|
143
|
+
page: bytes
|
|
144
|
+
|
|
145
|
+
def log_message(self, *a): # keep the terminal quiet
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
def _send(self, body: bytes, ctype: str, code: int = 200):
|
|
149
|
+
self.send_response(code)
|
|
150
|
+
self.send_header("Content-Type", ctype)
|
|
151
|
+
self.send_header("Content-Length", str(len(body)))
|
|
152
|
+
self.send_header("Cache-Control", "no-store")
|
|
153
|
+
self.end_headers()
|
|
154
|
+
self.wfile.write(body)
|
|
155
|
+
|
|
156
|
+
def do_GET(self):
|
|
157
|
+
# DNS-rebinding guard: the source of your repo must only reach pages served from this host:port
|
|
158
|
+
if self.headers.get("Host", "").split(":")[0] not in ("127.0.0.1", "localhost"):
|
|
159
|
+
return self._send(b"forbidden", "text/plain", 403)
|
|
160
|
+
u = urlparse(self.path)
|
|
161
|
+
a = {k: v[0] for k, v in parse_qs(u.query).items()}
|
|
162
|
+
try:
|
|
163
|
+
if u.path == "/":
|
|
164
|
+
return self._send(self.page, "text/html; charset=utf-8")
|
|
165
|
+
routes = {
|
|
166
|
+
"/api/overview": lambda: self.api.overview(int(a.get("limit", OVERVIEW_FILES))),
|
|
167
|
+
"/api/file": lambda: self.api.file(a["path"]),
|
|
168
|
+
"/api/symbol": lambda: self.api.symbol(int(a["id"])),
|
|
169
|
+
"/api/impact": lambda: self.api.impact(int(a["id"]), int(a.get("depth", 3))),
|
|
170
|
+
"/api/search": lambda: self.api.search(a.get("q", "")),
|
|
171
|
+
}
|
|
172
|
+
if u.path not in routes:
|
|
173
|
+
return self._send(b"not found", "text/plain", 404)
|
|
174
|
+
return self._send(json.dumps(routes[u.path]()).encode(), "application/json")
|
|
175
|
+
except (KeyError, ValueError):
|
|
176
|
+
return self._send(b'{"error":"bad request"}', "application/json", 400)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def serve(root: str, port: int = 0, open_browser: bool = True) -> None:
|
|
180
|
+
print(f"codegraph: indexing {root} ...", file=sys.stderr, flush=True)
|
|
181
|
+
index_repo(root)
|
|
182
|
+
graph = Graph(root)
|
|
183
|
+
Handler.api = Api(graph)
|
|
184
|
+
Handler.page = resources.files("codegraph").joinpath("ui.html").read_bytes()
|
|
185
|
+
server = ThreadingHTTPServer(("127.0.0.1", port), Handler) # loopback only: never expose source on the LAN
|
|
186
|
+
url = f"http://127.0.0.1:{server.server_address[1]}/"
|
|
187
|
+
print(f"codegraph ui: {url} (Ctrl+C to stop)", flush=True)
|
|
188
|
+
if open_browser:
|
|
189
|
+
threading.Timer(0.3, webbrowser.open, (url,)).start()
|
|
190
|
+
try:
|
|
191
|
+
server.serve_forever()
|
|
192
|
+
except KeyboardInterrupt:
|
|
193
|
+
pass
|
|
194
|
+
finally:
|
|
195
|
+
server.server_close()
|
|
196
|
+
graph.close()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codegraph-network
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Graph-based code understanding for LLM agents: fewer tokens, better answers
|
|
5
5
|
Author: Oviing
|
|
6
6
|
License-Expression: MIT
|
|
@@ -25,6 +25,7 @@ Requires-Dist: mcp>=1.2
|
|
|
25
25
|
Provides-Extra: dev
|
|
26
26
|
Requires-Dist: pytest>=8; extra == "dev"
|
|
27
27
|
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
28
|
+
Requires-Dist: numpy>=1.24; extra == "dev"
|
|
28
29
|
Provides-Extra: embed
|
|
29
30
|
Requires-Dist: fastembed>=0.4; extra == "embed"
|
|
30
31
|
Requires-Dist: numpy>=1.24; extra == "embed"
|
|
@@ -62,6 +63,17 @@ Or in any MCP client config:
|
|
|
62
63
|
The index lives in `<repo>/.codegraph/graph.db` and updates incrementally on every server start;
|
|
63
64
|
`reindex` re-scans after edits made outside the session. `.gitignore` it.
|
|
64
65
|
|
|
66
|
+
## Explore visually
|
|
67
|
+
|
|
68
|
+
codegraph -r /path/to/repo ui
|
|
69
|
+
|
|
70
|
+
Opens an interactive map of the repo in your browser (served from `127.0.0.1` only, nothing leaves your machine).
|
|
71
|
+
You start from the file-level network: lines are calls and imports between files, colour is community (or folder).
|
|
72
|
+
Drag nodes to rearrange and pin them, scroll to zoom, drag the background to pan. Double-click a file to open it
|
|
73
|
+
into its functions and classes; double-click a symbol to pull in everything it calls and everything that calls it.
|
|
74
|
+
The side panel shows the source, callers and callees (click to jump), and **Show impact** highlights the transitive
|
|
75
|
+
callers, i.e. what breaks if the symbol changes. Press `/` to search.
|
|
76
|
+
|
|
65
77
|
## Tools
|
|
66
78
|
|
|
67
79
|
| Tool | Use it to |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
codegraph/__init__.py,sha256=
|
|
1
|
+
codegraph/__init__.py,sha256=Zn1KFblwuFHiDRdRAiRnDBRkbPttWh44jKa5zG2ov0E,22
|
|
2
2
|
codegraph/bench.py,sha256=-2_kV76N8gjM58mW5Qb3tvMGGxfB-XEj_MsJTg7-t8s,3880
|
|
3
|
-
codegraph/cli.py,sha256=
|
|
3
|
+
codegraph/cli.py,sha256=kY3T6jpCbMkWIzcgMEFfPgAP2cuEmLLAWvFKA0Gx9OE,5317
|
|
4
4
|
codegraph/community.py,sha256=HtsQK58zDfUkySLE1mvuEoup1GR9RIIwh7UoY4iRDnw,3737
|
|
5
5
|
codegraph/docs.py,sha256=YHzg24v9tviiz5PqSG_nqHsnQhPntVgk8ABx2pmarcI,6126
|
|
6
6
|
codegraph/embed.py,sha256=LVryGlwSkktpWgPJzVtRFYJny4yWWu3AtuF-RQXruyo,9891
|
|
@@ -17,9 +17,11 @@ codegraph/review.py,sha256=yh5hNWtnsppc40hidL8Kb4kzSb9gugO5MJrIxY-37kM,6380
|
|
|
17
17
|
codegraph/server.py,sha256=lTmdhnHE-r2aLmNwWKu_svHBZ5XJ1j7higQDoT9W-TY,3094
|
|
18
18
|
codegraph/store.py,sha256=rOxweItAg1jMvb8D6W_rt4TTZSVkUOrj2tjurW_OVqY,30847
|
|
19
19
|
codegraph/tools.py,sha256=UNsiuta7TFaC1AgGMTDCOHb5jV0Czvz-5kh7nXoUB70,4338
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
codegraph_network-0.
|
|
23
|
-
codegraph_network-0.
|
|
24
|
-
codegraph_network-0.
|
|
25
|
-
codegraph_network-0.
|
|
20
|
+
codegraph/ui.html,sha256=ZgJ68K72xWOgFUIuxWVPne6RE6ErrZARKdbf_SGG9jk,19305
|
|
21
|
+
codegraph/ui.py,sha256=r3MCOHIwqozeLwK68-LdQFDj4MgQmmO7uLjaMQLAw2I,9019
|
|
22
|
+
codegraph_network-0.2.0.dist-info/licenses/LICENSE,sha256=2g3-GEuR7lJgSNW_8hDmQ67ottLWLC7TB7pWmiVYcdQ,1063
|
|
23
|
+
codegraph_network-0.2.0.dist-info/METADATA,sha256=87Fkaq_gDUkIJ5EJGBwPXzAok8-SJ8B1_AB_o_M6frM,13896
|
|
24
|
+
codegraph_network-0.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
25
|
+
codegraph_network-0.2.0.dist-info/entry_points.txt,sha256=3f2dJK7oR3dBzP21qRk_KuQa6Li8MVbVXeKcx3UjQ6c,49
|
|
26
|
+
codegraph_network-0.2.0.dist-info/top_level.txt,sha256=RqBj9sPbifZTb9aeHtnbxTgKJvfHIQdmYE1Brv8Wdkg,10
|
|
27
|
+
codegraph_network-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|