autodocs-engine 0.10.2 → 0.10.3

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,291 @@
1
+ // src/visualizer.ts — Full-viewport codebase topology visualization.
2
+ // The graph IS the page. Structural imports + behavioral co-change edges
3
+ // rendered simultaneously on a dark canvas. Click to explore blast radius.
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 = buildGraphData(pkg);
9
+ const importData = (pkg.importChain ?? []).map((e) => [e.source, e.importer, e.symbolCount]);
10
+ const cochangeData = (pkg.gitHistory?.coChangeEdges ?? []).map((e) => [
11
+ e.file1,
12
+ e.file2,
13
+ Math.round(e.jaccard * 100),
14
+ ]);
15
+ const clusterData = pkg.coChangeClusters ?? [];
16
+ const statsHtml = [
17
+ ["Files", pkg.files.total],
18
+ ["Imports", pkg.importChain?.length ?? 0],
19
+ ["Co-changes", pkg.gitHistory?.coChangeEdges?.length ?? 0],
20
+ ["Clusters", pkg.coChangeClusters?.length ?? 0],
21
+ ["Flows", pkg.executionFlows?.length ?? 0],
22
+ ]
23
+ .map(([l, v]) => `<div class="s"><span class="sv">${v}</span><span class="sl">${l}</span></div>`)
24
+ .join("");
25
+ const flowsHtml = (pkg.executionFlows ?? [])
26
+ .slice(0, 6)
27
+ .map((f) => {
28
+ const conf = f.confidence >= 0.3
29
+ ? '<i class="dot dot-g"></i>'
30
+ : f.confidence > 0
31
+ ? '<i class="dot dot-a"></i>'
32
+ : '<i class="dot dot-m"></i>';
33
+ return `<div class="fl">${conf}<span>${esc(f.steps.join(" \u2192 "))}</span></div>`;
34
+ })
35
+ .join("");
36
+ const convHtml = [
37
+ ...(pkg.conventions ?? []).map((c) => `<div class="cv cv-do">${esc(c.name)} <span class="cd">${c.confidence.percentage}%</span></div>`),
38
+ ...(pkg.antiPatterns ?? []).map((a) => `<div class="cv cv-no">${esc(a.rule)}</div>`),
39
+ ].join("");
40
+ return `<!DOCTYPE html>
41
+ <html lang="en">
42
+ <head>
43
+ <meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
44
+ <title>${esc(pkg.name)} \u2014 Codebase Topology</title>
45
+ <style>
46
+ *{margin:0;padding:0;box-sizing:border-box}
47
+ html,body{height:100%;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0a0a0f;color:#e0e0e0}
48
+ svg{display:block;width:100%;height:100%}
49
+ .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}
50
+ .hdr>*{pointer-events:auto}
51
+ .hdr h1{font-size:18px;font-weight:700;letter-spacing:-0.02em;white-space:nowrap}
52
+ .hdr .tag{font-size:11px;padding:2px 8px;border-radius:99px;background:rgba(255,255,255,0.08);color:#888}
53
+ .stats{display:flex;gap:2px;margin-left:auto}
54
+ .s{text-align:center;padding:4px 12px}
55
+ .sv{display:block;font-size:16px;font-weight:700;color:#7aa2f7}
56
+ .sl{display:block;font-size:9px;text-transform:uppercase;letter-spacing:0.08em;color:#555}
57
+ .legend{position:fixed;bottom:16px;left:24px;display:flex;gap:16px;font-size:11px;color:#555;z-index:10}
58
+ .legend i{display:inline-block;width:20px;height:2px;vertical-align:middle;margin-right:4px;border-radius:1px}
59
+ .leg-imp{background:#333}
60
+ .leg-coc{background:#c59a28;opacity:0.7}
61
+ .leg-impl{background:#b44e8a;opacity:0.7}
62
+ .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}
63
+ .panel.open{transform:translateX(0)}
64
+ .panel h2{font-size:15px;font-weight:700;margin-bottom:2px}
65
+ .panel .sub{font-size:12px;color:#555;margin-bottom:16px}
66
+ .panel h3{font-size:10px;text-transform:uppercase;letter-spacing:0.08em;color:#444;margin:14px 0 6px}
67
+ .panel ul{list-style:none}
68
+ .panel li{font-size:12px;padding:4px 0;display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #111}
69
+ .panel li:last-child{border:none}
70
+ .panel code{font-size:11px;color:#8a8a9a}
71
+ .badge{font-size:9px;padding:1px 6px;border-radius:99px;font-weight:600}
72
+ .badge-b{background:rgba(122,162,247,0.15);color:#7aa2f7}
73
+ .badge-a{background:rgba(197,154,40,0.15);color:#c59a28}
74
+ .badge-p{background:rgba(180,78,138,0.15);color:#b44e8a}
75
+ .close-btn{position:absolute;top:12px;right:12px;background:none;border:none;color:#444;font-size:18px;cursor:pointer;padding:4px 8px}
76
+ .close-btn:hover{color:#888}
77
+ .drawer{position:fixed;bottom:0;left:0;right:0;z-index:10;pointer-events:none}
78
+ .drawer>*{pointer-events:auto}
79
+ .dtoggle{display:flex;justify-content:center;padding:8px}
80
+ .dtoggle button{font-size:11px;padding:4px 16px;border-radius:99px 99px 0 0;border:1px solid #1a1a24;border-bottom:none;background:#0d0d14;color:#666;cursor:pointer}
81
+ .dtoggle button:hover{color:#999}
82
+ .dcontent{background:#0d0d14;border-top:1px solid #1a1a24;max-height:0;overflow:hidden;transition:max-height 0.3s ease}
83
+ .dcontent.open{max-height:300px;overflow-y:auto}
84
+ .dcontent-inner{padding:16px 24px;display:flex;gap:32px}
85
+ .dcol{flex:1;min-width:200px}
86
+ .dcol h3{font-size:10px;text-transform:uppercase;letter-spacing:0.08em;color:#444;margin-bottom:8px}
87
+ .fl{font-size:11px;color:#666;padding:3px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
88
+ .fl span{margin-left:4px}
89
+ .dot{display:inline-block;width:6px;height:6px;border-radius:50%}
90
+ .dot-g{background:#4ade80}.dot-a{background:#c59a28}.dot-m{background:#333}
91
+ .cv{font-size:11px;padding:4px 8px;margin:2px 0;border-radius:4px;border-left:3px solid}
92
+ .cv-do{border-color:#4ade80;background:rgba(74,222,128,0.05);color:#777}
93
+ .cv-no{border-color:#f87171;background:rgba(248,113,113,0.05);color:#777}
94
+ .cd{font-size:9px;color:#555}
95
+ .hint{position:fixed;bottom:50px;left:50%;transform:translateX(-50%);font-size:12px;color:#333;z-index:5;transition:opacity 0.5s}
96
+ </style>
97
+ </head>
98
+ <body>
99
+ <div class="hdr">
100
+ <h1>${esc(pkg.name)}</h1>
101
+ <span class="tag">${pkg.architecture.packageType}</span>
102
+ <div class="stats">${statsHtml}</div>
103
+ </div>
104
+ <div class="legend">
105
+ <span><i class="leg-imp"></i>import</span>
106
+ <span><i class="leg-coc" style="border:1px dashed #c59a28;height:0;width:20px"></i>co-change</span>
107
+ <span><i class="leg-impl" style="border:1px dotted #b44e8a;height:0;width:20px"></i>implicit coupling</span>
108
+ </div>
109
+ <div class="hint" id="hint">click a module to explore its blast radius</div>
110
+ <svg id="graph"></svg>
111
+ <div class="panel" id="panel">
112
+ <button class="close-btn" onclick="closePanel()">&times;</button>
113
+ <div id="panel-content"></div>
114
+ </div>
115
+ <div class="drawer">
116
+ <div class="dtoggle"><button onclick="toggleDrawer()">Flows &amp; Conventions</button></div>
117
+ <div class="dcontent" id="drawer">
118
+ <div class="dcontent-inner">
119
+ <div class="dcol"><h3>Execution Flows</h3>${flowsHtml || '<div class="fl">No flows detected</div>'}</div>
120
+ <div class="dcol"><h3>Conventions</h3>${convHtml || '<div class="cv">No conventions detected</div>'}</div>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ <script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
125
+ <script>
126
+ const G=${JSON.stringify(graph)};
127
+ const IM=${JSON.stringify(importData)};
128
+ const CC=${JSON.stringify(cochangeData)};
129
+ const CL=${JSON.stringify(clusterData)};
130
+ const PAL=['#7aa2f7','#4ade80','#c59a28','#f87171','#a78bfa','#e879a0','#38bdf8','#fb923c'];
131
+ const W=innerWidth,H=innerHeight;
132
+ const svg=d3.select('#graph').attr('viewBox',[0,0,W,H]);
133
+ const maxW=Math.max(1,...G.edges.map(e=>e.weight));
134
+ const maxF=Math.max(1,...G.nodes.map(n=>n.files));
135
+ const NC=G.nodes.length;
136
+ const spread=Math.min(W,H)*0.35;
137
+ const sim=d3.forceSimulation(G.nodes)
138
+ .force('link',d3.forceLink(G.edges).id(d=>d.id).distance(220))
139
+ .force('charge',d3.forceManyBody().strength(NC>12?-1200:NC>6?-1800:-2400))
140
+ .force('center',d3.forceCenter(W/2,H/2))
141
+ .force('collision',d3.forceCollide().radius(d=>30+Math.sqrt(d.files/maxF)*35))
142
+ .force('x',d3.forceX(W/2).strength(0.02))
143
+ .force('y',d3.forceY(H/2).strength(0.02));
144
+ const link=svg.append('g').selectAll('line').data(G.edges).join('line')
145
+ .attr('stroke',d=>d.type==='cochange'?'#c59a28':d.type==='implicit'?'#b44e8a':'#222')
146
+ .attr('stroke-width',d=>d.type==='cochange'||d.type==='implicit'?1.5:Math.max(0.8,Math.sqrt(d.weight/maxW)*4))
147
+ .attr('stroke-dasharray',d=>d.type==='cochange'?'6,4':d.type==='implicit'?'2,3':'none')
148
+ .attr('opacity',d=>d.type==='import'?0.35:0.5);
149
+ const defs=svg.append('defs');
150
+ const glow=defs.append('filter').attr('id','glow');
151
+ glow.append('feGaussianBlur').attr('stdDeviation','3').attr('result','blur');
152
+ const m=glow.append('feMerge');m.append('feMergeNode').attr('in','blur');m.append('feMergeNode').attr('in','SourceGraphic');
153
+ const node=svg.append('g').selectAll('g').data(G.nodes).join('g')
154
+ .call(d3.drag().on('start',(e,d)=>{if(!e.active)sim.alphaTarget(.3).restart();d.fx=d.x;d.fy=d.y})
155
+ .on('drag',(e,d)=>{d.fx=e.x;d.fy=e.y}).on('end',(e,d)=>{if(!e.active)sim.alphaTarget(0);d.fx=null;d.fy=null}))
156
+ .on('click',(e,d)=>{e.stopPropagation();selectNode(d)})
157
+ .style('cursor','pointer');
158
+ node.append('circle')
159
+ .attr('r',d=>16+Math.sqrt(d.files/maxF)*32)
160
+ .attr('fill',d=>d.group>=0?PAL[d.group%PAL.length]+'22':'#151520')
161
+ .attr('stroke',d=>d.group>=0?PAL[d.group%PAL.length]:'#282830')
162
+ .attr('stroke-width',1.5);
163
+ function lbl(d){const p=d.id.split('/');if(p.length<=1)return d.id||'root';const l=p.at(-1);return G.nodes.filter(n=>n.id.split('/').at(-1)===l).length>1?p.slice(-2).join('/'):l}
164
+ node.append('text').text(d=>lbl(d))
165
+ .attr('text-anchor','middle').attr('dy',d=>-(22+Math.sqrt(d.files/maxF)*34))
166
+ .attr('font-size','13px').attr('fill','#888').attr('font-weight','600')
167
+ .attr('paint-order','stroke').attr('stroke','#0a0a0f').attr('stroke-width',4);
168
+ node.append('text').text(d=>d.files)
169
+ .attr('text-anchor','middle').attr('dy',6)
170
+ .attr('font-size','13px').attr('fill','#555').attr('font-weight','700');
171
+ sim.on('tick',()=>{
172
+ 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);
173
+ node.attr('transform',d=>\`translate(\${d.x},\${d.y})\`);
174
+ });
175
+ function selectNode(d){
176
+ document.getElementById('hint').style.opacity='0';
177
+ node.select('circle').attr('opacity',.15).attr('filter',null);
178
+ link.attr('opacity',.04);
179
+ node.filter(n=>n.id===d.id).select('circle').attr('opacity',1).attr('filter','url(#glow)').attr('stroke-width',2.5);
180
+ const conn=new Set();
181
+ G.edges.forEach(e=>{const s=typeof e.source==='object'?e.source.id:e.source,t=typeof e.target==='object'?e.target.id:e.target;if(s===d.id)conn.add(t);if(t===d.id)conn.add(s)});
182
+ node.filter(n=>conn.has(n.id)).select('circle').attr('opacity',.85);
183
+ link.filter(e=>{const s=typeof e.source==='object'?e.source.id:e.source,t=typeof e.target==='object'?e.target.id:e.target;return s===d.id||t===d.id}).attr('opacity',.9);
184
+ const dir=d.id;
185
+ const imp=IM.filter(e=>(e[0].startsWith(dir+'/')||e[0]===dir)&&!e[1].startsWith(dir+'/'));
186
+ const coc=CC.filter(e=>((e[0].startsWith(dir+'/')||e[0]===dir)&&!e[1].startsWith(dir+'/'))||((e[1].startsWith(dir+'/')||e[1]===dir)&&!e[0].startsWith(dir+'/')));
187
+ 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});
188
+ const cl=CL.filter(c=>c.some(f=>f.startsWith(dir+'/')||f===dir));
189
+ let h='<h2>'+dir+'/</h2>';
190
+ h+='<div class="sub">'+d.files+' files &middot; '+d.imports+' imported &middot; '+d.exports+' exported</div>';
191
+ if(imp.length){h+='<h3>Imported by ('+imp.length+')</h3><ul>';imp.slice(0,10).forEach(e=>{h+='<li><code>'+e[1].split('/').slice(-2).join('/')+'</code><span class="badge badge-b">'+e[2]+'</span></li>'});h+='</ul>'}
192
+ if(ucoc.length){h+='<h3>Co-change partners</h3><ul>';ucoc.slice(0,6).forEach(e=>{const p=(e[0].startsWith(dir+'/')||e[0]===dir)?e[1]:e[0];h+='<li><code>'+p.split('/').slice(-2).join('/')+'</code><span class="badge badge-a">'+e[2]+'%</span></li>'});h+='</ul>'}
193
+ if(cl.length){h+='<h3>Cluster membership</h3><ul>';cl.forEach(c=>{h+='<li><span>'+c.length+'-file cluster</span><span class="badge badge-p">clique</span></li>'});h+='</ul>'}
194
+ if(!imp.length&&!ucoc.length&&!cl.length){h+='<div style="color:#333;padding:16px 0">No external dependencies detected</div>'}
195
+ document.getElementById('panel-content').innerHTML=h;
196
+ document.getElementById('panel').classList.add('open');
197
+ }
198
+ function closePanel(){
199
+ document.getElementById('panel').classList.remove('open');
200
+ node.select('circle').attr('opacity',1).attr('filter',null).attr('stroke-width',1.5);
201
+ link.attr('opacity',d=>d.type==='import'?.35:.5);
202
+ }
203
+ svg.on('click',()=>closePanel());
204
+ function toggleDrawer(){document.getElementById('drawer').classList.toggle('open')}
205
+ </script>
206
+ </body>
207
+ </html>`;
208
+ }
209
+ function buildGraphData(pkg) {
210
+ const importEdges = pkg.importChain ?? [];
211
+ const coChangeEdges = pkg.gitHistory?.coChangeEdges ?? [];
212
+ const implicitEdges = pkg.implicitCoupling ?? [];
213
+ const clusters = pkg.coChangeClusters ?? [];
214
+ const ds = new Map();
215
+ const de = new Map();
216
+ for (const e of importEdges) {
217
+ const f = fdir(e.importer);
218
+ const t = fdir(e.source);
219
+ for (const d of [f, t])
220
+ if (!ds.has(d))
221
+ ds.set(d, { files: new Set(), imports: 0, exports: 0 });
222
+ ds.get(f).files.add(e.importer);
223
+ ds.get(t).files.add(e.source);
224
+ ds.get(f).imports += e.symbolCount;
225
+ ds.get(t).exports += e.symbolCount;
226
+ if (f !== t) {
227
+ const k = `${f}|${t}`;
228
+ de.set(k, (de.get(k) ?? 0) + e.symbolCount);
229
+ }
230
+ }
231
+ const ce = new Map();
232
+ for (const e of coChangeEdges) {
233
+ const a = fdir(e.file1);
234
+ const b = fdir(e.file2);
235
+ if (a === b)
236
+ continue;
237
+ const k = [a, b].sort().join("|");
238
+ ce.set(k, (ce.get(k) ?? 0) + e.jaccard);
239
+ }
240
+ const ie = new Map();
241
+ for (const e of implicitEdges) {
242
+ const a = fdir(e.file1);
243
+ const b = fdir(e.file2);
244
+ if (a === b)
245
+ continue;
246
+ const k = [a, b].sort().join("|");
247
+ ie.set(k, (ie.get(k) ?? 0) + e.jaccard);
248
+ }
249
+ const dc = new Map();
250
+ for (let i = 0; i < clusters.length; i++) {
251
+ for (const f of clusters[i]) {
252
+ const d = fdir(f);
253
+ if (!dc.has(d))
254
+ dc.set(d, i);
255
+ }
256
+ }
257
+ const nodes = [...ds.entries()].map(([id, s]) => ({
258
+ id,
259
+ files: s.files.size,
260
+ imports: s.imports,
261
+ exports: s.exports,
262
+ group: dc.get(id) ?? -1,
263
+ }));
264
+ const edges = [];
265
+ for (const [k, w] of de) {
266
+ const [s, t] = k.split("|");
267
+ edges.push({ source: s, target: t, weight: w, type: "import" });
268
+ }
269
+ for (const [k, w] of ce) {
270
+ const [s, t] = k.split("|");
271
+ if (!de.has(`${s}|${t}`) && !de.has(`${t}|${s}`)) {
272
+ edges.push({ source: s, target: t, weight: Math.round(w * 20), type: "cochange" });
273
+ }
274
+ }
275
+ for (const [k, w] of ie) {
276
+ const [s, t] = k.split("|");
277
+ const ck = [s, t].sort().join("|");
278
+ if (!ce.has(ck) && !de.has(`${s}|${t}`) && !de.has(`${t}|${s}`)) {
279
+ edges.push({ source: s, target: t, weight: Math.round(w * 15), type: "implicit" });
280
+ }
281
+ }
282
+ return { nodes, edges };
283
+ }
284
+ function esc(s) {
285
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
286
+ }
287
+ function fdir(p) {
288
+ const i = p.lastIndexOf("/");
289
+ return i >= 0 ? p.slice(0, i) : ".";
290
+ }
291
+ //# 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.3",
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",