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.
- package/README.md +31 -4
- package/dist/bin/autodocs-engine.js +10 -1
- package/dist/bin/autodocs-engine.js.map +1 -1
- package/dist/bin/serve.d.ts +1 -0
- package/dist/bin/serve.js +11 -5
- package/dist/bin/serve.js.map +1 -1
- package/dist/bin/visualize.d.ts +5 -0
- package/dist/bin/visualize.js +29 -0
- package/dist/bin/visualize.js.map +1 -0
- package/dist/git-history.js +6 -4
- package/dist/git-history.js.map +1 -1
- package/dist/mcp/cache.js +15 -1
- package/dist/mcp/cache.js.map +1 -1
- package/dist/mcp/queries.d.ts +60 -0
- package/dist/mcp/queries.js +254 -10
- package/dist/mcp/queries.js.map +1 -1
- package/dist/mcp/server.d.ts +5 -2
- package/dist/mcp/server.js +293 -24
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.d.ts +10 -0
- package/dist/mcp/tools.js +173 -4
- package/dist/mcp/tools.js.map +1 -1
- package/dist/pipeline.js +9 -1
- package/dist/pipeline.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/dist/types.js.map +1 -1
- package/dist/visualizer.d.ts +2 -0
- package/dist/visualizer.js +500 -0
- package/dist/visualizer.js.map +1 -0
- package/hooks/autodocs-hook.cjs +68 -38
- package/package.json +1 -1
|
@@ -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()">×</button>
|
|
141
|
+
<div id="panel-content"></div>
|
|
142
|
+
</div>
|
|
143
|
+
<div class="drawer">
|
|
144
|
+
<div class="dtoggle"><button onclick="toggleDrawer()">Flows & 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+'/ · 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 · '+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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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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"}
|
package/hooks/autodocs-hook.cjs
CHANGED
|
@@ -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
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
//
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
//
|
|
173
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
228
|
-
return `[autodocs] ${
|
|
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.
|
|
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",
|