autodocs-engine 0.10.2 → 0.10.4

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.
@@ -0,0 +1,500 @@
1
+ // src/visualizer.ts — Full-viewport file-level codebase topology visualization.
2
+ // Every file is a node. Files cluster by directory via force simulation.
3
+ // Import edges + co-change edges + implicit coupling rendered simultaneously.
4
+ export function generateReport(analysis) {
5
+ const pkg = analysis.packages[0];
6
+ if (!pkg)
7
+ return "<html><body><p>No packages found.</p></body></html>";
8
+ const graph = buildFileGraph(pkg);
9
+ const cochangeData = (pkg.gitHistory?.coChangeEdges ?? []).map((e) => [
10
+ e.file1,
11
+ e.file2,
12
+ Math.round(e.jaccard * 100),
13
+ ]);
14
+ const statsHtml = [
15
+ ["Files", pkg.files.total],
16
+ ["Imports", pkg.importChain?.length ?? 0],
17
+ ["Co-changes", pkg.gitHistory?.coChangeEdges?.length ?? 0],
18
+ ["Clusters", pkg.coChangeClusters?.length ?? 0],
19
+ ["Flows", pkg.executionFlows?.length ?? 0],
20
+ ]
21
+ .map(([l, v]) => `<div class="s"><span class="sv">${v}</span><span class="sl">${l}</span></div>`)
22
+ .join("");
23
+ const flowsHtml = (pkg.executionFlows ?? [])
24
+ .slice(0, 6)
25
+ .map((f) => {
26
+ const conf = f.confidence >= 0.3
27
+ ? '<i class="dot dot-g"></i>'
28
+ : f.confidence > 0
29
+ ? '<i class="dot dot-a"></i>'
30
+ : '<i class="dot dot-m"></i>';
31
+ return `<div class="fl">${conf}<span>${esc(f.steps.join(" \u2192 "))}</span></div>`;
32
+ })
33
+ .join("");
34
+ const convHtml = [
35
+ ...(pkg.conventions ?? []).map((c) => `<div class="cv cv-do">${esc(c.name)} <span class="cd">${c.confidence.percentage}%</span></div>`),
36
+ ...(pkg.antiPatterns ?? []).map((a) => `<div class="cv cv-no">${esc(a.rule)}</div>`),
37
+ ].join("");
38
+ return `<!DOCTYPE html>
39
+ <html lang="en">
40
+ <head>
41
+ <meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
42
+ <title>${esc(pkg.name)} \u2014 Codebase Topology</title>
43
+ <style>
44
+ *{margin:0;padding:0;box-sizing:border-box}
45
+ html,body{height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0a0a0f;color:#e0e0e0}
46
+ svg{display:block;width:100%;height:100%}
47
+ .hdr{position:fixed;top:0;left:0;right:0;padding:16px 24px;display:flex;align-items:center;gap:16px;z-index:10;pointer-events:none}
48
+ .hdr>*{pointer-events:auto}
49
+ .hdr h1{font-size:18px;font-weight:700;letter-spacing:-0.02em;white-space:nowrap}
50
+ .hdr .tag{font-size:11px;padding:2px 8px;border-radius:99px;background:rgba(255,255,255,0.08);color:#888}
51
+ .stats{display:flex;gap:2px;margin-left:auto}
52
+ .s{text-align:center;padding:4px 12px}
53
+ .sv{display:block;font-size:16px;font-weight:700;color:#7aa2f7}
54
+ .sl{display:block;font-size:9px;text-transform:uppercase;letter-spacing:0.08em;color:#555}
55
+ .legend{position:fixed;bottom:16px;left:24px;display:flex;gap:16px;font-size:11px;color:#555;z-index:10}
56
+ .legend i{display:inline-block;width:20px;height:2px;vertical-align:middle;margin-right:4px;border-radius:1px}
57
+ .leg-imp{background:#333}
58
+ .leg-coc{background:#c59a28;opacity:0.7}
59
+ .leg-impl{background:#b44e8a;opacity:0.7}
60
+ .panel{position:fixed;top:0;right:0;width:300px;height:100%;background:#0d0d14;border-left:1px solid #1a1a24;z-index:20;transform:translateX(100%);transition:transform 0.25s ease;overflow-y:auto;padding:20px}
61
+ .panel.open{transform:translateX(0)}
62
+ .panel h2{font-size:15px;font-weight:700;margin-bottom:2px}
63
+ .panel .sub{font-size:12px;color:#555;margin-bottom:16px}
64
+ .panel h3{font-size:10px;text-transform:uppercase;letter-spacing:0.08em;color:#444;margin:14px 0 6px}
65
+ .panel ul{list-style:none}
66
+ .panel li{font-size:12px;padding:4px 0;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #111;cursor:pointer;border-radius:4px;padding:4px 6px;transition:background 0.15s}
67
+ .panel li:hover{background:rgba(255,255,255,0.04)}
68
+ .panel li:last-child{border:none}
69
+ .panel code{font-size:11px;color:#8a8a9a}
70
+ .badge{font-size:9px;padding:1px 6px;border-radius:99px;font-weight:600}
71
+ .badge-b{background:rgba(122,162,247,0.15);color:#7aa2f7}
72
+ .badge-a{background:rgba(197,154,40,0.15);color:#c59a28}
73
+ .badge-p{background:rgba(180,78,138,0.15);color:#b44e8a}
74
+ .close-btn{position:absolute;top:12px;right:12px;background:none;border:none;color:#444;font-size:18px;cursor:pointer;padding:4px 8px}
75
+ .close-btn:hover{color:#888}
76
+ .drawer{position:fixed;bottom:0;left:0;right:0;z-index:10;pointer-events:none}
77
+ .drawer>*{pointer-events:auto}
78
+ .dtoggle{display:flex;justify-content:center}
79
+ .dtoggle button{font-size:11px;padding:4px 16px;border-radius:6px 6px 0 0;border:1px solid #1a1a24;border-bottom:none;background:#0d0d14;color:#666;cursor:pointer}
80
+ .dtoggle button:hover{color:#999}
81
+ .dcontent{background:#0d0d14;border-top:1px solid #1a1a24;max-height:0;overflow:hidden;transition:max-height 0.3s ease}
82
+ .dcontent.open{max-height:300px;overflow-y:auto}
83
+ .dcontent-inner{padding:16px 24px;display:flex;gap:32px}
84
+ .dcol{flex:1;min-width:200px}
85
+ .dcol h3{font-size:10px;text-transform:uppercase;letter-spacing:0.08em;color:#444;margin-bottom:8px}
86
+ .fl{font-size:11px;color:#666;padding:3px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
87
+ .fl span{margin-left:4px}
88
+ .dot{display:inline-block;width:6px;height:6px;border-radius:50%}
89
+ .dot-g{background:#4ade80}.dot-a{background:#c59a28}.dot-m{background:#333}
90
+ .cv{font-size:11px;padding:4px 8px;margin:2px 0;border-radius:4px;border-left:3px solid}
91
+ .cv-do{border-color:#4ade80;background:rgba(74,222,128,0.05);color:#777}
92
+ .cv-no{border-color:#f87171;background:rgba(248,113,113,0.05);color:#777}
93
+ .cd{font-size:9px;color:#555}
94
+ .hint{position:fixed;bottom:50px;left:50%;transform:translateX(-50%);font-size:12px;color:#333;z-index:5;transition:opacity 0.5s}
95
+ /* ── Default visual state (all via CSS so .sel class can override) ── */
96
+ .n-c{opacity:1;stroke-width:1px}
97
+ .n-t{opacity:1;font-size:8px;font-weight:500}
98
+ .edge-i{opacity:0.2;stroke-width:0.4px}
99
+ .edge-c{opacity:0.5;stroke-width:1.2px}
100
+ .edge-m{opacity:0.5;stroke-width:1.2px}
101
+ .hull-v{fill-opacity:0.05;stroke-opacity:0.15;stroke-width:1.5px}
102
+ .dir-l{opacity:0.7;font-size:13px}
103
+ /* ── Selection: dim everything with one class toggle ── */
104
+ svg.sel .n-c{opacity:0.12;filter:none}
105
+ svg.sel .n-t{opacity:0.15}
106
+ svg.sel .edge-i,svg.sel .edge-c,svg.sel .edge-m{opacity:0.03}
107
+ svg.sel .hull-v{fill-opacity:0.02;stroke-opacity:0.06;stroke-width:1.5px}
108
+ svg.sel .dir-l{opacity:0.25;font-size:13px}
109
+ /* ── Highlight tiers (override dim) ── */
110
+ svg.sel .hl>.n-c,svg.sel .n-c.hl{opacity:1}
111
+ svg.sel .hl>.n-t,svg.sel .n-t.hl{opacity:1}
112
+ svg.sel .hl-conn>.n-c{opacity:0.8}
113
+ svg.sel .hl-conn>.n-t{opacity:0.8}
114
+ svg.sel .hl-ext>.n-c{opacity:0.5}
115
+ svg.sel .hl-ext>.n-t{opacity:0.5}
116
+ svg.sel .hl-sel>.n-c{opacity:1;stroke-width:2px}
117
+ svg.sel .hl-sel>.n-t{opacity:1;font-size:11px;font-weight:700}
118
+ svg.sel .edge-i.hl{opacity:0.55;stroke-width:0.8px}
119
+ svg.sel .edge-c.hl,svg.sel .edge-m.hl{opacity:0.7;stroke-width:1.4px}
120
+ svg.sel .hull-v.hl{fill-opacity:0.12;stroke-opacity:0.6;stroke-width:2.5px}
121
+ svg.sel .dir-l.hl{opacity:1;font-size:15px}
122
+ </style>
123
+ </head>
124
+ <body>
125
+ <div class="hdr">
126
+ <h1>${esc(pkg.name)}</h1>
127
+ <span class="tag">${pkg.architecture.packageType}</span>
128
+ <div class="stats">${statsHtml}</div>
129
+ </div>
130
+ <div class="legend">
131
+ <span><i class="leg-imp"></i>import</span>
132
+ <span><i class="leg-coc" style="border:1px dashed #c59a28;height:0;width:20px"></i>co-change</span>
133
+ <span><i class="leg-impl" style="border:1px dotted #b44e8a;height:0;width:20px"></i>implicit coupling</span>
134
+ <span style="margin-left:8px;opacity:0.6">|</span>
135
+ <span>color = directory group</span>
136
+ </div>
137
+ <div class="hint" id="hint">click a file to explore its connections</div>
138
+ <svg id="graph"></svg>
139
+ <div class="panel" id="panel">
140
+ <button class="close-btn" onclick="closePanel()">&times;</button>
141
+ <div id="panel-content"></div>
142
+ </div>
143
+ <div class="drawer">
144
+ <div class="dtoggle"><button onclick="toggleDrawer()">Flows &amp; Conventions</button></div>
145
+ <div class="dcontent" id="drawer">
146
+ <div class="dcontent-inner">
147
+ <div class="dcol"><h3>Execution Flows</h3>${flowsHtml || '<div class="fl">No flows detected</div>'}</div>
148
+ <div class="dcol"><h3>Conventions</h3>${convHtml || '<div class="cv">No conventions detected</div>'}</div>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ <script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
153
+ <script>
154
+ const G=${JSON.stringify(graph)};
155
+ const CC=${JSON.stringify(cochangeData)};
156
+ const PAL=['#7aa2f7','#4ade80','#c59a28','#f87171','#a78bfa','#e879a0','#38bdf8','#fb923c'];
157
+ const W=innerWidth,H=innerHeight;
158
+ const svg=d3.select('#graph').attr('viewBox',[0,0,W,H]);
159
+
160
+ const NC=G.nodes.length;
161
+ const maxImp=Math.max(1,...G.nodes.map(n=>n.importedBy));
162
+
163
+ // Directory cluster centers — spread across the full viewport
164
+ const dirs=[...new Set(G.nodes.map(n=>n.dir))];
165
+ const dirCenters={};
166
+ dirs.forEach((d,i)=>{
167
+ const angle=(2*Math.PI*i)/dirs.length - Math.PI/2;
168
+ const rx=W*0.38,ry=H*0.36;
169
+ dirCenters[d]={x:W/2+Math.cos(angle)*rx, y:H/2+Math.sin(angle)*ry};
170
+ });
171
+
172
+ // Directory label colors
173
+ const dirColor={};
174
+ dirs.forEach((d,i)=>{dirColor[d]=PAL[i%PAL.length]});
175
+
176
+ // Force simulation — strong directory clustering, strong inter-cluster repulsion
177
+ const sim=d3.forceSimulation(G.nodes)
178
+ .force('link',d3.forceLink(G.edges).id(d=>d.id).distance(30).strength(0.15))
179
+ .force('charge',d3.forceManyBody().strength(-80))
180
+ .force('collision',d3.forceCollide().radius(14))
181
+ .force('x',d3.forceX(d=>dirCenters[d.dir].x).strength(0.35))
182
+ .force('y',d3.forceY(d=>dirCenters[d.dir].y).strength(0.35));
183
+
184
+ // ── Pre-computed indexes (built once, used everywhere) ──
185
+
186
+ // 1. dirNodes: dir → node[] (eliminates repeated G.nodes.filter per dir)
187
+ const dirNodes={};
188
+ dirs.forEach(d=>{dirNodes[d]=[]});
189
+ G.nodes.forEach(n=>dirNodes[n.dir].push(n));
190
+
191
+ // 2. Resolved edge IDs: after forceLink init, source/target are objects.
192
+ // Pre-resolve to avoid typeof checks at every call site (~15 occurrences).
193
+ G.edges.forEach(e=>{e._s=e.source.id||e.source;e._t=e.target.id||e.target});
194
+
195
+ // 3. adj: nodeId → edge[] (eliminates full edge scans on selection)
196
+ const adj={};
197
+ G.nodes.forEach(n=>{adj[n.id]=[]});
198
+ G.edges.forEach(e=>{adj[e._s].push(e);if(e._s!==e._t)adj[e._t].push(e)});
199
+
200
+ // 4. nodeById: fast node lookup for navTo
201
+ const nodeById={};
202
+ G.nodes.forEach(n=>{nodeById[n.id]=n});
203
+
204
+ // Directory group hulls
205
+ const hullLayer=svg.append('g');
206
+ const hullPad=30;
207
+ const hulls=hullLayer.selectAll('path').data(dirs).join('path')
208
+ .attr('class','hull-v')
209
+ .attr('fill',d=>dirColor[d]).attr('stroke',d=>dirColor[d])
210
+ .attr('stroke-linejoin','round').attr('pointer-events','none');
211
+
212
+ // Directory name labels — prominent, above hull
213
+ const dirLabels=hullLayer.selectAll('text').data(dirs).join('text')
214
+ .attr('class','dir-l')
215
+ .attr('text-anchor','middle')
216
+ .attr('fill',d=>dirColor[d])
217
+ .attr('font-weight','700')
218
+ .attr('paint-order','stroke').attr('stroke','#0a0a0f').attr('stroke-width',4)
219
+ .text(d=>{const p=d.split('/');return p.at(-1)||d})
220
+ .style('cursor','pointer')
221
+ .on('click',(e,d)=>{e.stopPropagation();selectDir(d)});
222
+
223
+ function computeHullPath(points,pad){
224
+ if(points.length<1)return'';
225
+ if(points.length===1)return'M'+(points[0][0]-pad)+','+(points[0][1]-pad)+' a'+pad+','+pad+' 0 1,0 '+(pad*2)+',0 a'+pad+','+pad+' 0 1,0 '+(-pad*2)+',0';
226
+ if(points.length===2){const[a,b]=points;const dx=b[0]-a[0],dy=b[1]-a[1],len=Math.sqrt(dx*dx+dy*dy)||1;const nx=-dy/len*pad,ny=dx/len*pad;const expanded=[[a[0]+nx,a[1]+ny],[b[0]+nx,b[1]+ny],[b[0]-nx,b[1]-ny],[a[0]-nx,a[1]-ny]];const cx=d3.mean(expanded,p=>p[0]),cy=d3.mean(expanded,p=>p[1]);const rounded=expanded.map(p=>{const ddx=p[0]-cx,ddy=p[1]-cy,l=Math.sqrt(ddx*ddx+ddy*ddy)||1;return[p[0]+ddx/l*pad*0.5,p[1]+ddy/l*pad*0.5]});return'M'+rounded.map(p=>p[0]+','+p[1]).join('L')+'Z'}
227
+ const hull=d3.polygonHull(points);
228
+ if(!hull)return'';
229
+ // Expand hull outward by pad
230
+ const cx=d3.mean(hull,p=>p[0]),cy=d3.mean(hull,p=>p[1]);
231
+ const expanded=hull.map(p=>{const dx=p[0]-cx,dy=p[1]-cy,len=Math.sqrt(dx*dx+dy*dy)||1;return[p[0]+dx/len*pad,p[1]+dy/len*pad]});
232
+ return'M'+expanded.map(p=>p[0]+','+p[1]).join('L')+'Z';
233
+ }
234
+
235
+ // Glow filters
236
+ const defs=svg.append('defs');
237
+ const glow=defs.append('filter').attr('id','glow');
238
+ glow.append('feGaussianBlur').attr('stdDeviation','3').attr('result','blur');
239
+ const mg=glow.append('feMerge');mg.append('feMergeNode').attr('in','blur');mg.append('feMergeNode').attr('in','SourceGraphic');
240
+
241
+ // Edges — invisible wide hit-area lines behind visible edges for easier clicking
242
+ const linkG=svg.append('g');
243
+ const linkHit=linkG.selectAll('line.hit').data(G.edges).join('line')
244
+ .attr('class','hit').attr('stroke','transparent').attr('stroke-width',12)
245
+ .style('cursor','pointer')
246
+ .on('click',(e,d)=>{e.stopPropagation();selectEdge(d)});
247
+ const link=linkG.selectAll('line.vis').data(G.edges).join('line')
248
+ .attr('class',d=>d.type==='cochange'?'vis edge-c':d.type==='implicit'?'vis edge-m':'vis edge-i')
249
+ .attr('stroke',d=>d.type==='cochange'?'#c59a28':d.type==='implicit'?'#b44e8a':'#1a1a1f')
250
+ .attr('stroke-dasharray',d=>d.type==='cochange'?'5,3':d.type==='implicit'?'2,3':'none')
251
+ .attr('pointer-events','none');
252
+
253
+ // Interactive hull overlay — between edges and nodes so hull gaps are draggable
254
+ const hullInteract=svg.append('g').selectAll('path').data(dirs).join('path')
255
+ .attr('fill','transparent').attr('stroke','none')
256
+ .attr('pointer-events','all').style('cursor','pointer')
257
+ .on('click',(e,d)=>{e.stopPropagation();selectDir(d)});
258
+
259
+ // File nodes — on top so individual node drag/click takes priority
260
+ const node=svg.append('g').selectAll('g').data(G.nodes).join('g')
261
+ .call(d3.drag().on('start',(e,d)=>{if(!e.active)sim.alphaTarget(.3).restart();d.fx=d.x;d.fy=d.y})
262
+ .on('drag',(e,d)=>{d.fx=e.x;d.fy=e.y}).on('end',(e,d)=>{if(!e.active)sim.alphaTarget(0)}))
263
+ .on('click',(e,d)=>{e.stopPropagation();selectFile(d)})
264
+ .style('cursor','pointer');
265
+
266
+ node.append('circle')
267
+ .attr('class','n-c')
268
+ .attr('r',d=>5+Math.sqrt(d.importedBy/maxImp)*14)
269
+ .attr('fill',d=>dirColor[d.dir]+'33')
270
+ .attr('stroke',d=>dirColor[d.dir]);
271
+
272
+ // ALL files get labels
273
+ node.append('text')
274
+ .attr('class','n-t')
275
+ .text(d=>d.name)
276
+ .attr('text-anchor','middle')
277
+ .attr('dy',d=>-(9+Math.sqrt(d.importedBy/maxImp)*14))
278
+ .attr('fill','#555')
279
+ .attr('paint-order','stroke').attr('stroke','#0a0a0f').attr('stroke-width',3);
280
+
281
+ const pad=50;
282
+ // Per-dir tick cache: hull path string, centroid x, min y (reused by hull + labels)
283
+ const dirTick={};dirs.forEach(d=>{dirTick[d]={path:'',cx:0,minY:0}});
284
+
285
+ sim.on('tick',()=>{
286
+ // Clamp nodes to viewport
287
+ G.nodes.forEach(d=>{d.x=Math.max(pad,Math.min(W-pad,d.x));d.y=Math.max(pad,Math.min(H-pad,d.y))});
288
+ // Update edge positions
289
+ link.attr('x1',d=>d.source.x).attr('y1',d=>d.source.y).attr('x2',d=>d.target.x).attr('y2',d=>d.target.y);
290
+ linkHit.attr('x1',d=>d.source.x).attr('y1',d=>d.source.y).attr('x2',d=>d.target.x).attr('y2',d=>d.target.y);
291
+ // Update node positions
292
+ node.attr('transform',d=>\`translate(\${d.x},\${d.y})\`);
293
+ // Compute per-dir hull + label data in a single pass (was 3 separate filter scans per dir)
294
+ for(const d of dirs){
295
+ const ns=dirNodes[d];
296
+ if(!ns.length){dirTick[d].path='';continue}
297
+ const pts=ns.map(n=>[n.x,n.y]);
298
+ dirTick[d].path=computeHullPath(pts,hullPad);
299
+ let sx=0,minY=Infinity;
300
+ for(let i=0;i<ns.length;i++){sx+=ns[i].x;if(ns[i].y<minY)minY=ns[i].y}
301
+ dirTick[d].cx=sx/ns.length;
302
+ dirTick[d].minY=minY;
303
+ }
304
+ hulls.attr('d',d=>dirTick[d].path);
305
+ hullInteract.attr('d',d=>dirTick[d].path);
306
+ dirLabels.attr('x',d=>dirTick[d].cx).attr('y',d=>dirTick[d].minY-(hullPad+8));
307
+ });
308
+
309
+ function clearHighlights(){
310
+ node.classed('hl',false).classed('hl-sel',false).classed('hl-conn',false).classed('hl-ext',false);
311
+ node.select('circle').attr('filter',null);
312
+ link.classed('hl',false).attr('stroke',d=>d.type==='cochange'?'#c59a28':d.type==='implicit'?'#b44e8a':'#1a1a1f');
313
+ hulls.classed('hl',false);
314
+ dirLabels.classed('hl',false);
315
+ }
316
+
317
+ function selectFile(d){
318
+ document.getElementById('hint').style.opacity='0';
319
+ clearHighlights();
320
+ svg.classed('sel',true);
321
+
322
+ // Highlight selected node (1 element)
323
+ node.filter(n=>n.id===d.id).classed('hl-sel',true).select('circle').attr('filter','url(#glow)');
324
+
325
+ // Build connected set + categorized edges from adjacency index
326
+ const myEdges=adj[d.id]||[];
327
+ const conn=new Set();
328
+ const imp=[],impBy=[];
329
+ for(const e of myEdges){
330
+ const other=e._s===d.id?e._t:e._s;
331
+ conn.add(other);
332
+ if(e.type==='import'){if(e._s===d.id)imp.push(e);else impBy.push(e)}
333
+ }
334
+ // Highlight connected nodes
335
+ node.filter(n=>conn.has(n.id)).classed('hl-conn',true);
336
+ // Highlight connected edges (brighter stroke, no filter, no raise)
337
+ link.filter(e=>e._s===d.id||e._t===d.id).classed('hl',true)
338
+ .attr('stroke',e=>e.type==='cochange'?'#e8b83a':e.type==='implicit'?'#d468a8':'#556');
339
+
340
+ // Panel
341
+ const path=d.id;
342
+ const coc=CC.filter(e=>e[0]===path||e[1]===path);
343
+ const seen=new Set();const ucoc=coc.filter(e=>{const k=e[0]+e[1];if(seen.has(k))return false;seen.add(k);return true});
344
+
345
+ let h='<h2>'+d.name+'</h2>';
346
+ h+='<div class="sub">'+d.dir+'/ &middot; imported by '+d.importedBy+' files</div>';
347
+ if(impBy.length){h+='<h3>Imported by ('+impBy.length+')</h3><ul>';impBy.slice(0,10).forEach(e=>{h+='<li onclick="navTo(\\''+e._s+'\\')"><code>'+e._s.split('/').pop()+'</code><span class="badge badge-b">'+e.weight+'</span></li>'});h+='</ul>'}
348
+ if(imp.length){h+='<h3>Imports ('+imp.length+')</h3><ul>';imp.slice(0,10).forEach(e=>{h+='<li onclick="navTo(\\''+e._t+'\\')"><code>'+e._t.split('/').pop()+'</code><span class="badge badge-b">'+e.weight+'</span></li>'});h+='</ul>'}
349
+ if(ucoc.length){h+='<h3>Co-changes</h3><ul>';ucoc.slice(0,8).forEach(e=>{const p=e[0]===path?e[1]:e[0];h+='<li onclick="navTo(\\''+p+'\\')"><code>'+p.split('/').pop()+'</code><span class="badge badge-a">'+e[2]+'%</span></li>'});h+='</ul>'}
350
+ document.getElementById('panel-content').innerHTML=h;
351
+ document.getElementById('panel').classList.add('open');
352
+ }
353
+
354
+ function selectDir(dir){
355
+ document.getElementById('hint').style.opacity='0';
356
+ clearHighlights();
357
+ svg.classed('sel',true);
358
+
359
+ // Highlight selected directory hull + label (2 elements)
360
+ hulls.filter(d=>d===dir).classed('hl',true);
361
+ dirLabels.filter(d=>d===dir).classed('hl',true);
362
+
363
+ // Highlight all files in this directory
364
+ node.filter(d=>d.dir===dir).classed('hl',true);
365
+
366
+ // Build dir file set from pre-computed dirNodes
367
+ const files=dirNodes[dir]||[];
368
+ const dirFileSet=new Set(files.map(n=>n.id));
369
+
370
+ // Highlight connected edges (brighter stroke, no filter, no raise)
371
+ link.filter(e=>dirFileSet.has(e._s)||dirFileSet.has(e._t)).classed('hl',true)
372
+ .attr('stroke',e=>e.type==='cochange'?'#e8b83a':e.type==='implicit'?'#d468a8':'#556');
373
+
374
+ // Find connected external nodes via adjacency index
375
+ const connNodes=new Set();
376
+ for(const f of files){
377
+ for(const e of adj[f.id]||[]){
378
+ const other=e._s===f.id?e._t:e._s;
379
+ if(!dirFileSet.has(other))connNodes.add(other);
380
+ }
381
+ }
382
+ node.filter(n=>connNodes.has(n.id)).classed('hl-ext',true);
383
+
384
+ // Panel
385
+ const totalImp=files.reduce((s,f)=>s+f.importedBy,0);
386
+ let h='<h2>'+dir+'/</h2>';
387
+ h+='<div class="sub">'+files.length+' files &middot; '+totalImp+' total imports</div>';
388
+ h+='<h3>Files</h3><ul>';
389
+ files.slice().sort((a,b)=>b.importedBy-a.importedBy).forEach(f=>{
390
+ h+='<li onclick="navTo(\\''+f.id+'\\')"><code>'+f.name+'</code><span class="badge badge-b">'+f.importedBy+'</span></li>';
391
+ });
392
+ h+='</ul>';
393
+ const dirPrefix=dir+'/';
394
+ const coc=CC.filter(e=>(e[0].startsWith(dirPrefix)&&!e[1].startsWith(dirPrefix))||(e[1].startsWith(dirPrefix)&&!e[0].startsWith(dirPrefix)));
395
+ if(coc.length){
396
+ const seen=new Set();const ucoc=coc.filter(e=>{const k=e[0]+e[1];if(seen.has(k))return false;seen.add(k);return true});
397
+ h+='<h3>Co-changes with other dirs</h3><ul>';
398
+ ucoc.slice(0,8).forEach(e=>{const p=e[0].startsWith(dirPrefix)?e[1]:e[0];h+='<li onclick="navTo(\\''+p+'\\')"><code>'+p.split('/').pop()+'</code><span class="badge badge-a">'+e[2]+'%</span></li>'});
399
+ h+='</ul>';
400
+ }
401
+ document.getElementById('panel-content').innerHTML=h;
402
+ document.getElementById('panel').classList.add('open');
403
+ }
404
+
405
+ function selectEdge(d){
406
+ document.getElementById('hint').style.opacity='0';
407
+ clearHighlights();
408
+ svg.classed('sel',true);
409
+ const s=d._s,t=d._t;
410
+ // Highlight the two endpoint nodes
411
+ node.filter(n=>n.id===s||n.id===t).classed('hl',true).select('circle').attr('filter','url(#glow)');
412
+ // Highlight the clicked edge
413
+ link.filter(e=>(e._s===s&&e._t===t)||(e._s===t&&e._t===s)).classed('hl',true)
414
+ .attr('stroke',d.type==='cochange'?'#e8b83a':d.type==='implicit'?'#d468a8':'#7aa2f7');
415
+ // Panel content
416
+ const sName=s.split('/').pop(), tName=t.split('/').pop();
417
+ const typeLabel=d.type==='cochange'?'Co-change':d.type==='implicit'?'Implicit Coupling':'Import';
418
+ const typeColor=d.type==='cochange'?'badge-a':d.type==='implicit'?'badge-p':'badge-b';
419
+ let h='<h2>Connection</h2>';
420
+ h+='<div class="sub"><span class="badge '+typeColor+'">'+typeLabel+'</span></div>';
421
+ h+='<h3>Source</h3><ul><li onclick="navTo(\\''+s+'\\')"><code>'+sName+'</code></li></ul>';
422
+ h+='<h3>Target</h3><ul><li onclick="navTo(\\''+t+'\\')"><code>'+tName+'</code></li></ul>';
423
+ if(d.type==='import'){h+='<h3>Symbols imported</h3><ul><li>'+d.weight+' symbol'+(d.weight!==1?'s':'')+'</li></ul>'}
424
+ if(d.type==='cochange'){h+='<h3>Co-change similarity</h3><ul><li>Jaccard: '+d.weight+'%</li></ul><p style="font-size:11px;color:#555;margin-top:8px">These files frequently change together in commits, suggesting a functional relationship.</p>'}
425
+ if(d.type==='implicit'){h+='<h3>Coupling strength</h3><ul><li>Jaccard: '+d.weight+'%</li></ul><p style="font-size:11px;color:#555;margin-top:8px">These files co-change in commits but have no direct import relationship\\u2014a potential hidden dependency.</p>'}
426
+ document.getElementById('panel-content').innerHTML=h;
427
+ document.getElementById('panel').classList.add('open');
428
+ }
429
+
430
+ function navTo(fileId){
431
+ const target=nodeById[fileId];
432
+ if(target)selectFile(target);
433
+ }
434
+
435
+ function closePanel(){
436
+ document.getElementById('panel').classList.remove('open');
437
+ svg.classed('sel',false);
438
+ clearHighlights();
439
+ }
440
+ svg.on('click',()=>closePanel());
441
+ function toggleDrawer(){document.getElementById('drawer').classList.toggle('open')}
442
+ </script>
443
+ </body>
444
+ </html>`;
445
+ }
446
+ function buildFileGraph(pkg) {
447
+ const importEdges = pkg.importChain ?? [];
448
+ const coChangeEdges = pkg.gitHistory?.coChangeEdges ?? [];
449
+ const implicitEdges = pkg.implicitCoupling ?? [];
450
+ // Collect all files and their import counts
451
+ const files = new Map(); // path → importedBy count
452
+ for (const e of importEdges) {
453
+ if (!files.has(e.importer))
454
+ files.set(e.importer, 0);
455
+ files.set(e.source, (files.get(e.source) ?? 0) + 1);
456
+ }
457
+ const nodes = [...files.entries()].map(([path, importedBy]) => ({
458
+ id: path,
459
+ name: path.split("/").pop(),
460
+ dir: fdir(path),
461
+ importedBy,
462
+ }));
463
+ const fileSet = new Set(files.keys());
464
+ const edges = [];
465
+ // Import edges (file to file)
466
+ for (const e of importEdges) {
467
+ edges.push({ source: e.importer, target: e.source, weight: e.symbolCount, type: "import" });
468
+ }
469
+ // Co-change edges (only between files that are in the graph)
470
+ for (const e of coChangeEdges) {
471
+ if (fileSet.has(e.file1) && fileSet.has(e.file2)) {
472
+ edges.push({
473
+ source: e.file1,
474
+ target: e.file2,
475
+ weight: Math.round(e.jaccard * 100),
476
+ type: "cochange",
477
+ });
478
+ }
479
+ }
480
+ // Implicit coupling edges
481
+ for (const e of implicitEdges) {
482
+ if (fileSet.has(e.file1) && fileSet.has(e.file2)) {
483
+ edges.push({
484
+ source: e.file1,
485
+ target: e.file2,
486
+ weight: Math.round(e.jaccard * 100),
487
+ type: "implicit",
488
+ });
489
+ }
490
+ }
491
+ return { nodes, edges };
492
+ }
493
+ function esc(s) {
494
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
495
+ }
496
+ function fdir(p) {
497
+ const i = p.lastIndexOf("/");
498
+ return i >= 0 ? p.slice(0, i) : ".";
499
+ }
500
+ //# sourceMappingURL=visualizer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"visualizer.js","sourceRoot":"","sources":["../src/visualizer.ts"],"names":[],"mappings":"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"}
@@ -156,48 +156,44 @@ function augment(snapshot, pattern) {
156
156
  if (pkgs.length === 0) return null;
157
157
  const pkg = pkgs.reduce((a, b) => ((a.publicAPI || []).length > (b.publicAPI || []).length ? a : b));
158
158
 
159
- const patternLower = pattern.toLowerCase();
160
-
161
- // Match against public API symbols AND call graph functions
162
- const apiMatches = (pkg.publicAPI || []).filter((e) => e.name.toLowerCase().includes(patternLower));
159
+ const q = pattern.toLowerCase();
160
+ const callGraph = pkg.callGraph || [];
161
+ const sections = [];
162
+
163
+ // Pre-build call graph indexes (single pass, O(1) lookups per symbol)
164
+ const callersOf = new Map();
165
+ const calleesOf = new Map();
166
+ for (const e of callGraph) {
167
+ if (!callersOf.has(e.to)) callersOf.set(e.to, []);
168
+ callersOf.get(e.to).push(e.from);
169
+ if (!calleesOf.has(e.from)) calleesOf.set(e.from, []);
170
+ calleesOf.get(e.from).push(e.to);
171
+ }
163
172
 
164
- // Also search call graph for internal functions not in publicAPI
165
- const apiNames = new Set(apiMatches.map((e) => e.name));
166
- const callGraphFns = new Set();
167
- for (const e of pkg.callGraph || []) {
168
- if (!apiNames.has(e.from) && e.from.toLowerCase().includes(patternLower)) callGraphFns.add(e.from);
169
- if (!apiNames.has(e.to) && e.to.toLowerCase().includes(patternLower)) callGraphFns.add(e.to);
173
+ // ── Pass 1: Public API symbols ──
174
+ const apiMatches = (pkg.publicAPI || []).filter((e) => e.name.toLowerCase().includes(q));
175
+ const matchedNames = new Set(apiMatches.map((e) => e.name));
176
+
177
+ // ── Pass 2: Internal call graph functions not in public API ──
178
+ for (const e of callGraph) {
179
+ for (const fn of [e.from, e.to]) {
180
+ if (matchedNames.has(fn) || !fn.toLowerCase().includes(q)) continue;
181
+ matchedNames.add(fn);
182
+ const file = e.from === fn ? e.fromFile : e.toFile;
183
+ apiMatches.push({ name: fn, kind: "function", sourceFile: file, importCount: 0 });
184
+ }
170
185
  }
171
186
 
172
- // Build unified match list: API symbols first, then call graph functions
173
- const matches = [
174
- ...apiMatches.map((e) => ({ name: e.name, kind: e.kind, sourceFile: e.sourceFile, importCount: e.importCount })),
175
- ...[...callGraphFns].slice(0, 3).map((name) => {
176
- const edge = (pkg.callGraph || []).find((e) => e.from === name || e.to === name);
177
- return { name, kind: "function", sourceFile: edge ? (edge.from === name ? edge.fromFile : edge.toFile) : "unknown", importCount: 0 };
178
- }),
179
- ];
180
- if (matches.length === 0) return null;
181
-
182
- const results = [];
183
- for (const exp of matches.slice(0, 3)) {
187
+ // Format symbol results (top 3) with call graph + co-change + flow context
188
+ for (const exp of apiMatches.slice(0, 3)) {
184
189
  const lines = [`**${exp.name}** (${exp.kind}) — \`${exp.sourceFile}\``];
185
190
 
186
- // Callers from call graph
187
- const callers = (pkg.callGraph || [])
188
- .filter((e) => e.to === exp.name)
189
- .map((e) => e.from)
190
- .slice(0, 3);
191
+ const callers = (callersOf.get(exp.name) || []).slice(0, 3);
191
192
  if (callers.length > 0) lines.push(` Called by: ${callers.join(", ")}`);
192
193
 
193
- // Callees from call graph
194
- const callees = (pkg.callGraph || [])
195
- .filter((e) => e.from === exp.name)
196
- .map((e) => e.to)
197
- .slice(0, 3);
194
+ const callees = (calleesOf.get(exp.name) || []).slice(0, 3);
198
195
  if (callees.length > 0) lines.push(` Calls: ${callees.join(", ")}`);
199
196
 
200
- // Co-change partners
201
197
  const coChanges = (pkg.gitHistory?.coChangeEdges || [])
202
198
  .filter((e) => e.file1 === exp.sourceFile || e.file2 === exp.sourceFile)
203
199
  .sort((a, b) => b.jaccard - a.jaccard)
@@ -208,7 +204,6 @@ function augment(snapshot, pattern) {
208
204
  });
209
205
  if (coChanges.length > 0) lines.push(` Co-changes with: ${coChanges.join(", ")}`);
210
206
 
211
- // Execution flows
212
207
  const flows = (pkg.executionFlows || [])
213
208
  .filter((f) => f.steps.includes(exp.name))
214
209
  .slice(0, 2)
@@ -218,14 +213,49 @@ function augment(snapshot, pattern) {
218
213
  });
219
214
  if (flows.length > 0) lines.push(` Flows: ${flows.join("; ")}`);
220
215
 
221
- // Import count
222
216
  if (exp.importCount > 0) lines.push(` Imported by: ${exp.importCount} files`);
223
217
 
224
- results.push(lines.join("\n"));
218
+ sections.push(lines.join("\n"));
219
+ }
220
+
221
+ // ── Pass 3: File paths from import chain ──
222
+ const coveredFiles = new Set(apiMatches.slice(0, 3).map((e) => e.sourceFile));
223
+ const seenFiles = new Set();
224
+ const fileResults = [];
225
+
226
+ for (const edge of pkg.importChain || []) {
227
+ for (const fp of [edge.importer, edge.source]) {
228
+ if (seenFiles.has(fp) || coveredFiles.has(fp) || !fp.toLowerCase().includes(q)) continue;
229
+ seenFiles.add(fp);
230
+
231
+ let context = "";
232
+ const coChange = (pkg.gitHistory?.coChangeEdges || []).find((e) => e.file1 === fp || e.file2 === fp);
233
+ if (coChange) {
234
+ const partner = coChange.file1 === fp ? coChange.file2 : coChange.file1;
235
+ context = ` — co-changes with ${partner} (${Math.round(coChange.jaccard * 100)}%)`;
236
+ }
237
+ fileResults.push(`\`${fp}\`${context}`);
238
+ }
239
+ }
240
+ if (fileResults.length > 0) {
241
+ sections.push(`**Files:** ${fileResults.slice(0, 3).join(", ")}`);
242
+ }
243
+
244
+ // ── Pass 4: Conventions and workflow rules ──
245
+ for (const conv of pkg.conventions || []) {
246
+ if (!conv.name.toLowerCase().includes(q) && !(conv.description || "").toLowerCase().includes(q)) continue;
247
+ sections.push(`**Convention:** ${conv.name} — ${conv.description}`);
248
+ break; // At most 1 convention match to keep output concise
249
+ }
250
+
251
+ for (const rule of snapshot.workflowRules || []) {
252
+ if (!rule.trigger.toLowerCase().includes(q) && !rule.action.toLowerCase().includes(q)) continue;
253
+ sections.push(`**Rule:** ${rule.trigger} → ${rule.action}`);
254
+ break; // At most 1 rule match
225
255
  }
226
256
 
227
- if (results.length === 0) return null;
228
- return `[autodocs] ${results.length} related symbol${results.length > 1 ? "s" : ""} found:\n\n${results.join("\n\n")}`;
257
+ if (sections.length === 0) return null;
258
+ return `[autodocs] ${sections.length} result${sections.length > 1 ? "s" : ""} found:\n\n${sections.join("\n\n")}`;
229
259
  }
230
260
 
231
261
  // ─── Output ────────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autodocs-engine",
3
- "version": "0.10.2",
3
+ "version": "0.10.4",
4
4
  "description": "Codebase intelligence for AI coding agents — MCP server with git co-change analysis, convention detection, execution flows, and type-aware analysis",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",