universal-ast-mapper 2.0.0 → 2.0.2
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/CHANGELOG.md +15 -0
- package/README.md +261 -12
- package/dist/ai-refactor.js +185 -0
- package/dist/ai-testgen.js +105 -0
- package/dist/analysis.js +134 -0
- package/dist/arch-rules.js +82 -0
- package/dist/callgraph.js +467 -0
- package/dist/check.js +112 -0
- package/dist/cli.js +2284 -0
- package/dist/complexity.js +98 -0
- package/dist/config.js +53 -0
- package/dist/contextpack.js +79 -0
- package/dist/coupling.js +35 -0
- package/dist/covmerge.js +176 -0
- package/dist/crosslang.js +425 -0
- package/dist/dashboard.js +259 -0
- package/dist/diagram.js +264 -0
- package/dist/diskcache.js +97 -0
- package/dist/docgen.js +156 -0
- package/dist/embeddings.js +136 -0
- package/dist/explain.js +123 -0
- package/dist/explorer.js +123 -0
- package/dist/extractors/c.js +204 -0
- package/dist/extractors/common.js +56 -0
- package/dist/extractors/cpp.js +272 -0
- package/dist/extractors/csharp.js +209 -0
- package/dist/extractors/go.js +212 -0
- package/dist/extractors/java.js +152 -0
- package/dist/extractors/kotlin.js +159 -0
- package/dist/extractors/php.js +208 -0
- package/dist/extractors/python.js +153 -0
- package/dist/extractors/ruby.js +146 -0
- package/dist/extractors/rust.js +249 -0
- package/dist/extractors/swift.js +192 -0
- package/dist/extractors/typescript.js +577 -0
- package/dist/fix.js +92 -0
- package/dist/gitdiff.js +178 -0
- package/dist/graph-analysis.js +279 -0
- package/dist/graph.js +165 -0
- package/dist/history.js +36 -0
- package/dist/html.js +658 -0
- package/dist/incremental.js +122 -0
- package/dist/index.js +1945 -0
- package/dist/indexstore.js +105 -0
- package/dist/layers.js +36 -0
- package/dist/lsp.js +238 -0
- package/dist/modulecoupling.js +0 -0
- package/dist/parser.js +84 -0
- package/dist/patch.js +199 -0
- package/dist/plugins.js +88 -0
- package/dist/pool.js +114 -0
- package/dist/prompts.js +67 -0
- package/dist/registry.js +87 -0
- package/dist/report.js +441 -0
- package/dist/resolver.js +222 -0
- package/dist/roots.js +47 -0
- package/dist/search.js +68 -0
- package/dist/security.js +178 -0
- package/dist/semantic.js +365 -0
- package/dist/serve.js +328 -0
- package/dist/sfc.js +27 -0
- package/dist/similar.js +98 -0
- package/dist/skeleton.js +132 -0
- package/dist/smells.js +285 -0
- package/dist/sourcemap.js +60 -0
- package/dist/testgen.js +280 -0
- package/dist/testmap.js +167 -0
- package/dist/tsconfig.js +212 -0
- package/dist/typeflow.js +124 -0
- package/dist/types.js +5 -0
- package/dist/unused-params.js +127 -0
- package/dist/webapp.js +646 -0
- package/dist/worker.js +27 -0
- package/dist/workspace.js +330 -0
- package/package.json +2 -1
package/dist/explain.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import https from "node:https";
|
|
2
|
+
// ─── Structural analysis ──────────────────────────────────────────────────────
|
|
3
|
+
export function buildExplainResult(symbolName, skel, graph, impact, smells, complexityRating) {
|
|
4
|
+
// Find the symbol node in the skeleton
|
|
5
|
+
const sym = findSymbolNode(skel.symbols, symbolName);
|
|
6
|
+
// Callers = files that transitively depend on this symbol
|
|
7
|
+
const callerFiles = impact
|
|
8
|
+
? [...new Set([...impact.direct, ...impact.transitive].map((n) => n.file))]
|
|
9
|
+
: [];
|
|
10
|
+
// Dependencies = what this file imports that relate to this symbol
|
|
11
|
+
const dependsOn = (skel.imports ?? [])
|
|
12
|
+
.filter((imp) => imp.symbol || imp.from)
|
|
13
|
+
.map((imp) => imp.symbol ? `${imp.symbol} from ${imp.from}` : imp.from)
|
|
14
|
+
.slice(0, 10);
|
|
15
|
+
const lineCount = sym ? sym.range.endLine - sym.range.startLine + 1 : 0;
|
|
16
|
+
const isAsync = !!(sym?.signature?.includes("async "));
|
|
17
|
+
const isExported = sym?.exported !== false;
|
|
18
|
+
return {
|
|
19
|
+
symbol: symbolName,
|
|
20
|
+
file: skel.file,
|
|
21
|
+
kind: sym?.kind ?? "unknown",
|
|
22
|
+
signature: sym?.signature,
|
|
23
|
+
summary: {
|
|
24
|
+
callerFiles: callerFiles.slice(0, 20),
|
|
25
|
+
callerCount: impact ? impact.totalFiles : 0,
|
|
26
|
+
dependsOn,
|
|
27
|
+
childCount: sym?.children.length ?? 0,
|
|
28
|
+
lineCount,
|
|
29
|
+
isExported,
|
|
30
|
+
isAsync,
|
|
31
|
+
},
|
|
32
|
+
smells,
|
|
33
|
+
complexityRating,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function findSymbolNode(symbols, name) {
|
|
37
|
+
for (const sym of symbols) {
|
|
38
|
+
if (sym.name === name)
|
|
39
|
+
return sym;
|
|
40
|
+
const child = findSymbolNode(sym.children, name);
|
|
41
|
+
if (child)
|
|
42
|
+
return child;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
// ─── AI explanation ───────────────────────────────────────────────────────────
|
|
47
|
+
function buildPrompt(result, sourceCode) {
|
|
48
|
+
const callers = result.summary.callerFiles.slice(0, 8).join(", ") || "none detected";
|
|
49
|
+
const deps = result.summary.dependsOn.slice(0, 6).join(", ") || "none";
|
|
50
|
+
const smellsStr = result.smells.length ? result.smells.join(", ") : "none";
|
|
51
|
+
return `You are a senior software engineer explaining a codebase symbol to a teammate.
|
|
52
|
+
|
|
53
|
+
## Symbol: \`${result.symbol}\`
|
|
54
|
+
- Kind: ${result.kind}
|
|
55
|
+
- File: ${result.file}
|
|
56
|
+
- Signature: ${result.signature ?? "(no signature)"}
|
|
57
|
+
- Exported: ${result.summary.isExported}
|
|
58
|
+
- Async: ${result.summary.isAsync}
|
|
59
|
+
- Line count: ${result.summary.lineCount}
|
|
60
|
+
- Complexity rating: ${result.complexityRating ?? "unknown"}
|
|
61
|
+
- Code smells: ${smellsStr}
|
|
62
|
+
- Depends on: ${deps}
|
|
63
|
+
- Used by (${result.summary.callerCount} files): ${callers}
|
|
64
|
+
|
|
65
|
+
## Source snippet (first 60 lines):
|
|
66
|
+
\`\`\`
|
|
67
|
+
${sourceCode.split("\n").slice(0, 60).join("\n")}
|
|
68
|
+
\`\`\`
|
|
69
|
+
|
|
70
|
+
Explain this symbol in 3–5 concise sentences covering:
|
|
71
|
+
1. What it does (purpose, not implementation)
|
|
72
|
+
2. When/why callers use it
|
|
73
|
+
3. Key dependencies or side effects worth knowing
|
|
74
|
+
4. Change risk: what breaks if this symbol is modified or removed
|
|
75
|
+
|
|
76
|
+
Do NOT include code. Plain prose only. Be specific, not generic.`;
|
|
77
|
+
}
|
|
78
|
+
async function callClaude(prompt, opts) {
|
|
79
|
+
const apiKey = opts.apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
80
|
+
if (!apiKey)
|
|
81
|
+
throw new Error("No Anthropic API key — set ANTHROPIC_API_KEY or pass --api-key");
|
|
82
|
+
const body = JSON.stringify({
|
|
83
|
+
model: opts.model ?? "claude-sonnet-4-6",
|
|
84
|
+
max_tokens: opts.maxTokens ?? 1024,
|
|
85
|
+
messages: [{ role: "user", content: prompt }],
|
|
86
|
+
});
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
const req = https.request({
|
|
89
|
+
hostname: "api.anthropic.com",
|
|
90
|
+
path: "/v1/messages",
|
|
91
|
+
method: "POST",
|
|
92
|
+
headers: {
|
|
93
|
+
"content-type": "application/json",
|
|
94
|
+
"x-api-key": apiKey,
|
|
95
|
+
"anthropic-version": "2023-06-01",
|
|
96
|
+
"content-length": Buffer.byteLength(body),
|
|
97
|
+
},
|
|
98
|
+
}, (res) => {
|
|
99
|
+
const chunks = [];
|
|
100
|
+
res.on("data", (c) => chunks.push(c));
|
|
101
|
+
res.on("end", () => {
|
|
102
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
103
|
+
try {
|
|
104
|
+
const parsed = JSON.parse(raw);
|
|
105
|
+
if (parsed.error)
|
|
106
|
+
reject(new Error(`Anthropic API: ${parsed.error.message}`));
|
|
107
|
+
else
|
|
108
|
+
resolve(parsed.content?.[0]?.text ?? "");
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
reject(new Error(`Unexpected API response`));
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
req.on("error", reject);
|
|
116
|
+
req.write(body);
|
|
117
|
+
req.end();
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
export async function aiExplain(result, sourceCode, opts = {}) {
|
|
121
|
+
const aiExplanation = await callClaude(buildPrompt(result, sourceCode), opts);
|
|
122
|
+
return { ...result, aiExplanation };
|
|
123
|
+
}
|
package/dist/explorer.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/** Derive a file-level dependency graph (nodes = files, edges = imports). */
|
|
2
|
+
function deriveFileGraph(graph) {
|
|
3
|
+
const nodeMap = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
4
|
+
// top-level symbol names per file (for the detail panel).
|
|
5
|
+
const fileSyms = new Map();
|
|
6
|
+
for (const n of graph.nodes) {
|
|
7
|
+
if (n.nodeType !== "symbol")
|
|
8
|
+
continue;
|
|
9
|
+
const s = n;
|
|
10
|
+
if (s.id.indexOf("::") !== s.id.lastIndexOf("::"))
|
|
11
|
+
continue; // skip nested (one :: only)
|
|
12
|
+
const arr = fileSyms.get(s.file) ?? [];
|
|
13
|
+
if (arr.length < 60)
|
|
14
|
+
arr.push(s.kind + " " + s.symbol);
|
|
15
|
+
fileSyms.set(s.file, arr);
|
|
16
|
+
}
|
|
17
|
+
const nodes = [];
|
|
18
|
+
for (const n of graph.nodes) {
|
|
19
|
+
if (n.nodeType !== "file")
|
|
20
|
+
continue;
|
|
21
|
+
const f = n;
|
|
22
|
+
const parts = f.id.split("/");
|
|
23
|
+
nodes.push({ id: f.id, symbols: f.symbolCount, group: parts.length > 1 ? parts[0] : "(root)", lang: f.language, syms: fileSyms.get(f.id) ?? [], ca: 0, ce: 0, inst: 0 });
|
|
24
|
+
}
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
const links = [];
|
|
27
|
+
for (const e of graph.edges) {
|
|
28
|
+
if (e.edgeType !== "imports")
|
|
29
|
+
continue;
|
|
30
|
+
const to = nodeMap.get(e.to);
|
|
31
|
+
const toFile = to ? (to.nodeType === "file" ? to.id : to.file) : null;
|
|
32
|
+
if (!toFile || e.from === toFile)
|
|
33
|
+
continue;
|
|
34
|
+
const key = e.from + "|" + toFile;
|
|
35
|
+
if (seen.has(key))
|
|
36
|
+
continue;
|
|
37
|
+
seen.add(key);
|
|
38
|
+
links.push({ source: e.from, target: toFile });
|
|
39
|
+
}
|
|
40
|
+
// Per-file coupling (Ca = fan-in, Ce = fan-out, I = Ce/(Ca+Ce)) from the deduped links.
|
|
41
|
+
const outSet = new Map();
|
|
42
|
+
const inSet = new Map();
|
|
43
|
+
for (const l of links) {
|
|
44
|
+
(outSet.get(l.source) ?? outSet.set(l.source, new Set()).get(l.source)).add(l.target);
|
|
45
|
+
(inSet.get(l.target) ?? inSet.set(l.target, new Set()).get(l.target)).add(l.source);
|
|
46
|
+
}
|
|
47
|
+
for (const n of nodes) {
|
|
48
|
+
n.ce = outSet.get(n.id)?.size ?? 0;
|
|
49
|
+
n.ca = inSet.get(n.id)?.size ?? 0;
|
|
50
|
+
n.inst = n.ca + n.ce === 0 ? 0 : Math.round((n.ce / (n.ca + n.ce)) * 100) / 100;
|
|
51
|
+
}
|
|
52
|
+
return { nodes, links };
|
|
53
|
+
}
|
|
54
|
+
const STYLE = "body{margin:0;font-family:system-ui,sans-serif;color:#222;background:#fafafa}" +
|
|
55
|
+
"#bar{position:fixed;top:0;left:0;right:0;height:48px;display:flex;align-items:center;gap:12px;padding:0 14px;background:#fff;border-bottom:1px solid #e5e5e5;z-index:4;box-sizing:border-box}" +
|
|
56
|
+
"#bar h1{font-size:14px;margin:0;font-weight:600}#bar .muted{color:#888;font-size:12px}" +
|
|
57
|
+
"#q{flex:0 0 200px;padding:6px 10px;border:1px solid #ddd;border-radius:6px;font-size:13px}" +
|
|
58
|
+
"#cv{position:fixed;top:48px;left:0;right:0;bottom:0;display:block;cursor:grab}" +
|
|
59
|
+
"#tip{position:fixed;pointer-events:none;background:#222;color:#fff;font-size:12px;padding:4px 8px;border-radius:5px;display:none;z-index:5}" +
|
|
60
|
+
"#panel{position:fixed;top:48px;right:0;bottom:0;width:300px;background:#fff;border-left:1px solid #e5e5e5;z-index:3;overflow-y:auto;padding:14px 16px;box-sizing:border-box;display:none;font-size:13px}" +
|
|
61
|
+
"#panel h2{font-size:14px;margin:0 0 2px;word-break:break-all}#panel .path{color:#888;font-size:11px;margin-bottom:10px;word-break:break-all}" +
|
|
62
|
+
"#panel .meta{color:#555;margin-bottom:12px}#panel h3{font-size:11px;text-transform:uppercase;letter-spacing:.04em;color:#999;margin:14px 0 6px}" +
|
|
63
|
+
"#panel .row{padding:3px 6px;border-radius:5px;cursor:pointer;word-break:break-all;line-height:1.5}#panel .row:hover{background:#f0f0f0}" +
|
|
64
|
+
"#panel .sym{color:#444;padding:2px 6px;word-break:break-all}#panel .k{color:#999;font-size:11px}" +
|
|
65
|
+
"#close{position:absolute;top:10px;right:12px;cursor:pointer;color:#999;font-size:18px;line-height:1;border:none;background:none}" +
|
|
66
|
+
"#mode{padding:5px 8px;border:1px solid #ddd;border-radius:6px;font-size:12px;background:#fff;color:#222}" +
|
|
67
|
+
"#leg{position:fixed;left:14px;bottom:14px;z-index:3;background:#fff;border:1px solid #e5e5e5;border-radius:8px;padding:8px 12px;font-size:11px;color:#555;display:none}" +
|
|
68
|
+
"#leg .bar{width:150px;height:8px;border-radius:4px;background:linear-gradient(90deg,hsl(120,65%,46%),hsl(60,75%,50%),hsl(0,70%,52%));margin:5px 0 3px}" +
|
|
69
|
+
"@media(prefers-color-scheme:dark){body{color:#ddd;background:#161616}#bar,#panel{background:#1e1e1e;border-color:#333}#q{background:#2a2a2a;border-color:#444;color:#ddd}#panel .row:hover{background:#2a2a2a}#panel .sym{color:#bbb}#mode{background:#2a2a2a;border-color:#444;color:#ddd}#leg{background:#1e1e1e;border-color:#333;color:#bbb}}";
|
|
70
|
+
const CLIENT = "var c=document.getElementById('cv'),ctx=c.getContext('2d'),tip=document.getElementById('tip'),panel=document.getElementById('panel');" +
|
|
71
|
+
"var PANELW=300,panelOpen=false;" +
|
|
72
|
+
"var W,H;function resize(){var r=devicePixelRatio||1;W=innerWidth||c.clientWidth||800;H=(innerHeight-48)||c.clientHeight||600;c.width=W*r;c.height=H*r;ctx.setTransform(r,0,0,r,0,0);}addEventListener('resize',function(){resize();});resize();" +
|
|
73
|
+
"function availW(){return W-(panelOpen?PANELW:0);}" +
|
|
74
|
+
"var nodes=DATA.nodes,links=DATA.links,byId={};nodes.forEach(function(n){byId[n.id]=n;n.vx=0;n.vy=0;});" +
|
|
75
|
+
"var deg={},out={},inn={};links.forEach(function(l){deg[l.source]=(deg[l.source]||0)+1;deg[l.target]=(deg[l.target]||0)+1;(out[l.source]=out[l.source]||[]).push(l.target);(inn[l.target]=inn[l.target]||[]).push(l.source);});" +
|
|
76
|
+
"var sim=nodes.filter(function(n){return deg[n.id];}),orphans=nodes.filter(function(n){return !deg[n.id];});" +
|
|
77
|
+
"sim.forEach(function(n){n.x=W/2+(Math.random()-0.5)*240;n.y=H/2+(Math.random()-0.5)*240;});" +
|
|
78
|
+
"var groups={},gi=0;function color(g){if(groups[g]==null)groups[g]=gi++;return 'hsl('+((groups[g]*67)%360)+',58%,55%)';}" +
|
|
79
|
+
"var adj={};links.forEach(function(l){(adj[l.source]=adj[l.source]||[]).push(l.target);(adj[l.target]=adj[l.target]||[]).push(l.source);});" +
|
|
80
|
+
"var view={x:0,y:0,k:1},sel=null,hover=null,drag=null,pan=null,q='',autofit=true,mode='group';" +
|
|
81
|
+
"function instColor(i){return 'hsl('+Math.round((1-i)*120)+',65%,'+Math.round(46+i*8)+'%)';}" +
|
|
82
|
+
"function radius(n){return 4+Math.sqrt(n.symbols||0)*1.7;}" +
|
|
83
|
+
"function tick(){if(!sim.length)return;var k=0.0016;for(var i=0;i<sim.length;i++){var a=sim[i];a.vx+=(W/2-a.x)*k;a.vy+=(H/2-a.y)*k;for(var j=i+1;j<sim.length;j++){var b=sim[j];var dx=a.x-b.x,dy=a.y-b.y,d2=dx*dx+dy*dy;if(d2<100)d2=100;var d=Math.sqrt(d2),f=2200/d2,fx=f*dx/d,fy=f*dy/d;a.vx+=fx;a.vy+=fy;b.vx-=fx;b.vy-=fy;}}" +
|
|
84
|
+
"links.forEach(function(l){var a=byId[l.source],b=byId[l.target];if(!a||!b)return;var dx=b.x-a.x,dy=b.y-a.y,d=Math.sqrt(dx*dx+dy*dy)+0.01,f=(d-90)*0.02,fx=f*dx/d,fy=f*dy/d;a.vx+=fx;a.vy+=fy;b.vx-=fx;b.vy-=fy;});" +
|
|
85
|
+
"for(var i=0;i<sim.length;i++){var n=sim[i];if(n===drag)continue;n.vx*=0.85;n.vy*=0.85;if(n.vx>30)n.vx=30;if(n.vx<-30)n.vx=-30;if(n.vy>30)n.vy=30;if(n.vy<-30)n.vy=-30;n.x+=n.vx;n.y+=n.vy;}}" +
|
|
86
|
+
"function bb4(arr){var a=1e9,b=1e9,c2=-1e9,d2=-1e9;for(var i=0;i<arr.length;i++){var n=arr[i];if(n.x<a)a=n.x;if(n.y<b)b=n.y;if(n.x>c2)c2=n.x;if(n.y>d2)d2=n.y;}return[a,b,c2,d2];}" +
|
|
87
|
+
"function layoutOrphans(){if(!orphans.length)return;var bb=sim.length?bb4(sim):[W*0.3,H*0.3,W*0.7,H*0.5];var left=bb[0],bottom=bb[3]+46,wide=Math.max(bb[2]-bb[0],260);var cols=Math.max(1,Math.ceil(Math.sqrt(orphans.length*1.8)));var gap=Math.max(22,wide/cols);for(var i=0;i<orphans.length;i++){orphans[i].x=left+(i%cols)*gap;orphans[i].y=bottom+Math.floor(i/cols)*22;}}" +
|
|
88
|
+
"function fitView(){var bb=bb4(nodes);var aw=availW();var bw=Math.max(bb[2]-bb[0],1),bh=Math.max(bb[3]-bb[1],1),k=Math.min(aw/(bw+70),H/(bh+70));k=Math.max(0.12,Math.min(k,2.2));view.k=k;view.x=aw/2-((bb[0]+bb[2])/2)*k;view.y=H/2-((bb[1]+bb[3])/2)*k;}" +
|
|
89
|
+
"function center(n){view.k=Math.max(view.k,0.7);view.x=availW()/2-n.x*view.k;view.y=H/2-n.y*view.k;}" +
|
|
90
|
+
"function esc(t){return String(t).replace(/&/g,'&').replace(/</g,'<');}" +
|
|
91
|
+
"function rowList(ids){if(!ids||!ids.length)return '<div class=\"sym\" style=\"color:#aaa\">none</div>';return ids.slice().sort().map(function(id){return '<div class=\"row\" data-id=\"'+esc(id)+'\">'+esc(id)+'</div>';}).join('');}" +
|
|
92
|
+
"function showPanel(n){sel=n;panelOpen=true;var imp=out[n.id]||[],impBy=inn[n.id]||[];var syms=(n.syms||[]).map(function(s){var i=s.indexOf(' ');return '<div class=\"sym\"><span class=\"k\">'+esc(s.slice(0,i))+'</span> '+esc(s.slice(i+1))+'</div>';}).join('')||'<div class=\"sym\" style=\"color:#aaa\">none</div>';" +
|
|
93
|
+
"panel.innerHTML='<button id=\"close\">×</button>'+'<h2>'+esc(n.id.split('/').pop())+'</h2><div class=\"path\">'+esc(n.id)+'</div>'+'<div class=\"meta\">'+esc(n.lang)+' · '+(n.symbols||0)+' symbols'+(deg[n.id]?' · Ca '+(n.ca||0)+' · Ce '+(n.ce||0)+' · I '+(n.inst||0):' · no in-scope deps')+'</div>'+'<h3>Imports ('+imp.length+')</h3>'+rowList(imp)+'<h3>Imported by ('+impBy.length+')</h3>'+rowList(impBy)+'<h3>Symbols</h3>'+syms;" +
|
|
94
|
+
"panel.style.display='block';}" +
|
|
95
|
+
"panel.addEventListener('click',function(e){if(e.target.id==='close'){panelOpen=false;sel=null;panel.style.display='none';autofit=true;return;}var id=e.target.getAttribute('data-id');if(id&&byId[id]){showPanel(byId[id]);center(byId[id]);}});" +
|
|
96
|
+
"function draw(){ctx.clearRect(0,0,W,H);ctx.save();ctx.translate(view.x,view.y);ctx.scale(view.k,view.k);ctx.lineWidth=0.8;" +
|
|
97
|
+
"links.forEach(function(l){var a=byId[l.source],b=byId[l.target];if(!a||!b)return;var on=sel&&(l.source===sel.id||l.target===sel.id);ctx.strokeStyle=on?'rgba(110,110,240,0.9)':'rgba(150,150,150,0.18)';ctx.beginPath();ctx.moveTo(a.x,a.y);ctx.lineTo(b.x,b.y);ctx.stroke();});" +
|
|
98
|
+
"function dot(n,orphan){var dim=(sel&&n!==sel&&(adj[sel.id]||[]).indexOf(n.id)<0)||(q&&n.id.toLowerCase().indexOf(q)<0);ctx.globalAlpha=dim?0.14:(orphan?0.55:1);ctx.beginPath();ctx.arc(n.x,n.y,orphan?3.2:radius(n),0,6.2832);ctx.fillStyle=mode==='inst'?(deg[n.id]?instColor(n.inst):'#999'):color(n.group);ctx.fill();if(n===sel||n===hover){ctx.lineWidth=2;ctx.strokeStyle='#fff';ctx.stroke();ctx.lineWidth=0.8;}}" +
|
|
99
|
+
"orphans.forEach(function(n){dot(n,true);});sim.forEach(function(n){dot(n,false);});" +
|
|
100
|
+
"ctx.globalAlpha=1;ctx.fillStyle=getComputedStyle(document.body).color;ctx.font='11px system-ui';sim.forEach(function(n){if(n===sel||n===hover||n.symbols>=14){ctx.fillText(n.id.split('/').pop(),n.x+radius(n)+3,n.y+3);}});ctx.restore();}" +
|
|
101
|
+
"function loop(){var w=innerWidth,hh=innerHeight-48;if(w&&hh&&(w!==W||hh!==H))resize();tick();tick();layoutOrphans();if(autofit)fitView();draw();var bx=bb4(nodes);document.getElementById(\"dbg\").textContent=\"W=\"+W+\" H=\"+H+\" iw=\"+innerWidth+\"x\"+innerHeight+\" dpr=\"+(devicePixelRatio||1)+\" k=\"+view.k.toFixed(2)+\" vx=\"+Math.round(view.x)+\" vy=\"+Math.round(view.y)+\" fit=\"+autofit+\" sim=\"+sim.length+\" orph=\"+orphans.length+\" worldBox=\"+Math.round(bx[0])+\",\"+Math.round(bx[1])+\"..\"+Math.round(bx[2])+\",\"+Math.round(bx[3]);requestAnimationFrame(loop);}" +
|
|
102
|
+
"function world(e){return{x:(e.clientX-view.x)/view.k,y:(e.clientY-48-view.y)/view.k};}" +
|
|
103
|
+
"function pick(p){var all=sim.concat(orphans);for(var i=all.length-1;i>=0;i--){var n=all[i];var r=(deg[n.id]?radius(n):3.2)+5;if((p.x-n.x)*(p.x-n.x)+(p.y-n.y)*(p.y-n.y)<=r*r)return n;}return null;}" +
|
|
104
|
+
"c.addEventListener('mousedown',function(e){autofit=false;var n=pick(world(e));if(n){drag=n;showPanel(n);}else{pan={x:e.clientX-view.x,y:e.clientY-view.y};}});" +
|
|
105
|
+
"c.addEventListener('dblclick',function(){panelOpen=false;sel=null;panel.style.display='none';autofit=true;});" +
|
|
106
|
+
"addEventListener('mousemove',function(e){var p=world(e);if(drag){drag.x=p.x;drag.y=p.y;drag.vx=0;drag.vy=0;}else if(pan){view.x=e.clientX-pan.x;view.y=e.clientY-pan.y;}else{hover=pick(p);if(hover){tip.style.display='block';tip.style.left=(e.clientX+12)+'px';tip.style.top=(e.clientY+12)+'px';tip.textContent=hover.id+' · '+(hover.symbols||0)+' symbols · '+hover.lang+(deg[hover.id]?' · Ca '+hover.ca+' Ce '+hover.ce+' I '+hover.inst:'');}else tip.style.display='none';}});" +
|
|
107
|
+
"addEventListener('mouseup',function(){drag=null;pan=null;});" +
|
|
108
|
+
"c.addEventListener('wheel',function(e){e.preventDefault();autofit=false;var s=e.deltaY<0?1.1:0.9;var mx=e.clientX,my=e.clientY-48;view.x=mx-(mx-view.x)*s;view.y=my-(my-view.y)*s;view.k*=s;},{passive:false});" +
|
|
109
|
+
"document.getElementById('q').addEventListener('input',function(e){q=e.target.value.toLowerCase();});" +
|
|
110
|
+
"document.getElementById('mode').addEventListener('change',function(e){mode=e.target.value;document.getElementById('leg').style.display=mode==='inst'?'block':'none';});" +
|
|
111
|
+
"addEventListener('keydown',function(e){if(e.key==='d'&&e.target.tagName!=='INPUT'){var x=document.getElementById('dbg');x.style.display=x.style.display==='none'?'block':'none';}});loop();";
|
|
112
|
+
/** Build a self-contained, dependency-free HTML graph explorer. */
|
|
113
|
+
export function buildExplorerHtml(graph, root) {
|
|
114
|
+
const data = deriveFileGraph(graph);
|
|
115
|
+
const dataJson = JSON.stringify(data);
|
|
116
|
+
const title = root.split(/[\\/]/).filter(Boolean).pop() || "project";
|
|
117
|
+
return ("<!doctype html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">" +
|
|
118
|
+
"<title>AST-MCP — " + title + " graph</title><style>" + STYLE + "</style></head><body>" +
|
|
119
|
+
"<div id=\"bar\"><h1>AST-MCP graph</h1><span class=\"muted\">" + data.nodes.length + " files · " + data.links.length + " edges · drag / scroll / click</span>" +
|
|
120
|
+
"<input id=\"q\" placeholder=\"filter files…\" /><select id=\"mode\"><option value=\"group\">color: folder</option><option value=\"inst\">color: coupling</option></select></div>" +
|
|
121
|
+
"<canvas id=\"cv\"></canvas><div id=\"tip\"></div><div id=\"panel\"></div><div id=\"leg\"><b>Instability I = Ce/(Ca+Ce)</b><div class=\"bar\"></div><div style=\"display:flex;justify-content:space-between\"><span>0 = stable</span><span>1 = volatile</span></div></div><div id=\"dbg\" style=\"position:fixed;left:8px;bottom:8px;font:11px monospace;color:#e07;z-index:6;pointer-events:none;white-space:pre;display:none\"></div>" +
|
|
122
|
+
"<script>var DATA=" + dataJson + ";</script><script>" + CLIENT + "</script></body></html>");
|
|
123
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { namedChildren, nameOf, headerSignature, leadingComment } from "../parser.js";
|
|
2
|
+
import { makeSymbol } from "./common.js";
|
|
3
|
+
/* ─── helpers ─────────────────────────────────────────────────────────────── */
|
|
4
|
+
function childOfType(node, type) {
|
|
5
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
6
|
+
const c = node.child(i);
|
|
7
|
+
if (c && c.type === type)
|
|
8
|
+
return c;
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
/** Recursively unwrap a declarator chain to get the identifier text. */
|
|
13
|
+
function nameFromDeclarator(node) {
|
|
14
|
+
if (!node)
|
|
15
|
+
return null;
|
|
16
|
+
switch (node.type) {
|
|
17
|
+
case "identifier":
|
|
18
|
+
case "field_identifier":
|
|
19
|
+
case "type_identifier":
|
|
20
|
+
return node.text;
|
|
21
|
+
case "pointer_declarator":
|
|
22
|
+
case "array_declarator":
|
|
23
|
+
case "parenthesized_declarator":
|
|
24
|
+
return nameFromDeclarator(node.childForFieldName("declarator"));
|
|
25
|
+
case "function_declarator": {
|
|
26
|
+
const d = node.childForFieldName("declarator");
|
|
27
|
+
return nameFromDeclarator(d);
|
|
28
|
+
}
|
|
29
|
+
case "init_declarator":
|
|
30
|
+
return nameFromDeclarator(node.childForFieldName("declarator"));
|
|
31
|
+
default:
|
|
32
|
+
// best-effort: find first identifier-like child
|
|
33
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
34
|
+
const c = node.namedChild(i);
|
|
35
|
+
if (c && (c.type === "identifier" || c.type === "field_identifier" || c.type === "type_identifier"))
|
|
36
|
+
return c.text;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function hasStaticStorage(node) {
|
|
42
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
43
|
+
const c = node.child(i);
|
|
44
|
+
if (c && c.type === "storage_class_specifier" && c.text === "static")
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
/* ─── imports (#include) ──────────────────────────────────────────────────── */
|
|
50
|
+
export function extractImportsC(root, _source) {
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const child of namedChildren(root)) {
|
|
53
|
+
if (child.type !== "preproc_include")
|
|
54
|
+
continue;
|
|
55
|
+
const pathNode = childOfType(child, "system_lib_string") ?? childOfType(child, "string_literal");
|
|
56
|
+
if (!pathNode)
|
|
57
|
+
continue;
|
|
58
|
+
const raw = pathNode.text.replace(/^[<"]|[>"]$/g, "");
|
|
59
|
+
const base = raw.split("/").pop() ?? raw;
|
|
60
|
+
const sym = base.replace(/\.[hH]$|\.hpp$|\.hxx$|\.hh$/, "");
|
|
61
|
+
out.push({ symbol: sym, from: raw });
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
/* ─── symbol extraction ───────────────────────────────────────────────────── */
|
|
66
|
+
export function extractC(root, _source) {
|
|
67
|
+
return collect(namedChildren(root));
|
|
68
|
+
}
|
|
69
|
+
function collect(nodes) {
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const n of nodes) {
|
|
72
|
+
const res = handle(n);
|
|
73
|
+
if (Array.isArray(res))
|
|
74
|
+
out.push(...res);
|
|
75
|
+
else if (res)
|
|
76
|
+
out.push(res);
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
function handle(node) {
|
|
81
|
+
switch (node.type) {
|
|
82
|
+
case "struct_specifier":
|
|
83
|
+
case "union_specifier": {
|
|
84
|
+
const name = nameOf(node);
|
|
85
|
+
if (!name)
|
|
86
|
+
return null;
|
|
87
|
+
const body = node.childForFieldName("body");
|
|
88
|
+
return makeSymbol({
|
|
89
|
+
name,
|
|
90
|
+
kind: "struct",
|
|
91
|
+
node,
|
|
92
|
+
rawKind: node.type,
|
|
93
|
+
doc: leadingComment(node),
|
|
94
|
+
children: body ? structFields(body) : [],
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
case "enum_specifier": {
|
|
98
|
+
const name = nameOf(node);
|
|
99
|
+
if (!name)
|
|
100
|
+
return null;
|
|
101
|
+
return makeSymbol({
|
|
102
|
+
name,
|
|
103
|
+
kind: "enum",
|
|
104
|
+
node,
|
|
105
|
+
rawKind: node.type,
|
|
106
|
+
doc: leadingComment(node),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
case "function_definition": {
|
|
110
|
+
const decl = node.childForFieldName("declarator");
|
|
111
|
+
const name = nameFromDeclarator(decl);
|
|
112
|
+
if (!name)
|
|
113
|
+
return null;
|
|
114
|
+
const isStatic = hasStaticStorage(node);
|
|
115
|
+
return makeSymbol({
|
|
116
|
+
name,
|
|
117
|
+
kind: "function",
|
|
118
|
+
node,
|
|
119
|
+
rawKind: node.type,
|
|
120
|
+
signature: headerSignature(node, node.childForFieldName("body")),
|
|
121
|
+
visibility: isStatic ? "private" : "public",
|
|
122
|
+
exported: !isStatic,
|
|
123
|
+
doc: leadingComment(node),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
case "declaration": {
|
|
127
|
+
// top-level variable/function declarations (prototypes, externs, etc.)
|
|
128
|
+
const decl = node.childForFieldName("declarator");
|
|
129
|
+
const name = nameFromDeclarator(decl);
|
|
130
|
+
if (!name)
|
|
131
|
+
return null;
|
|
132
|
+
// skip function prototypes — focus on real defs (function_definition)
|
|
133
|
+
if (decl && containsFunctionDeclarator(decl))
|
|
134
|
+
return null;
|
|
135
|
+
return makeSymbol({
|
|
136
|
+
name,
|
|
137
|
+
kind: "var",
|
|
138
|
+
node,
|
|
139
|
+
rawKind: node.type,
|
|
140
|
+
signature: node.text.replace(/\s+/g, " ").replace(/;$/, "").trim(),
|
|
141
|
+
visibility: hasStaticStorage(node) ? "private" : "public",
|
|
142
|
+
exported: !hasStaticStorage(node),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
case "preproc_def":
|
|
146
|
+
case "preproc_function_def": {
|
|
147
|
+
const name = nameOf(node);
|
|
148
|
+
if (!name)
|
|
149
|
+
return null;
|
|
150
|
+
return makeSymbol({
|
|
151
|
+
name,
|
|
152
|
+
kind: "const",
|
|
153
|
+
node,
|
|
154
|
+
rawKind: node.type,
|
|
155
|
+
signature: node.text.replace(/\s+/g, " ").trim(),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
case "type_definition": {
|
|
159
|
+
// typedef — name is in the declarator (last identifier)
|
|
160
|
+
const decl = node.childForFieldName("declarator");
|
|
161
|
+
const name = nameFromDeclarator(decl);
|
|
162
|
+
if (!name)
|
|
163
|
+
return null;
|
|
164
|
+
return makeSymbol({
|
|
165
|
+
name,
|
|
166
|
+
kind: "type",
|
|
167
|
+
node,
|
|
168
|
+
rawKind: node.type,
|
|
169
|
+
signature: node.text.replace(/\s+/g, " ").replace(/;$/, "").trim(),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
default:
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function containsFunctionDeclarator(node) {
|
|
177
|
+
if (node.type === "function_declarator")
|
|
178
|
+
return true;
|
|
179
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
180
|
+
const c = node.namedChild(i);
|
|
181
|
+
if (c && containsFunctionDeclarator(c))
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
function structFields(body) {
|
|
187
|
+
const out = [];
|
|
188
|
+
for (const field of namedChildren(body)) {
|
|
189
|
+
if (field.type !== "field_declaration")
|
|
190
|
+
continue;
|
|
191
|
+
const decl = field.childForFieldName("declarator");
|
|
192
|
+
const name = nameFromDeclarator(decl);
|
|
193
|
+
if (!name)
|
|
194
|
+
continue;
|
|
195
|
+
out.push(makeSymbol({
|
|
196
|
+
name,
|
|
197
|
+
kind: "field",
|
|
198
|
+
node: field,
|
|
199
|
+
rawKind: field.type,
|
|
200
|
+
signature: field.text.replace(/\s+/g, " ").replace(/;$/, "").trim(),
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export function lineRange(node) {
|
|
2
|
+
return { startLine: node.startPosition.row + 1, endLine: node.endPosition.row + 1 };
|
|
3
|
+
}
|
|
4
|
+
/** True if the identifier begins with an uppercase letter (Go export rule). */
|
|
5
|
+
export function startsUpper(name) {
|
|
6
|
+
const c = name[0];
|
|
7
|
+
return !!c && c !== c.toLowerCase() && c === c.toUpperCase();
|
|
8
|
+
}
|
|
9
|
+
/** Python convention: a single leading underscore (but not dunder) means private. */
|
|
10
|
+
export function pythonVisibility(name) {
|
|
11
|
+
if (name.startsWith("__") && name.endsWith("__"))
|
|
12
|
+
return "public"; // dunder
|
|
13
|
+
return name.startsWith("_") ? "private" : "public";
|
|
14
|
+
}
|
|
15
|
+
export function makeSymbol(init) {
|
|
16
|
+
const sym = {
|
|
17
|
+
name: init.name,
|
|
18
|
+
kind: init.kind,
|
|
19
|
+
visibility: init.visibility ?? "public",
|
|
20
|
+
range: lineRange(init.node),
|
|
21
|
+
children: init.children ?? [],
|
|
22
|
+
};
|
|
23
|
+
if (init.rawKind !== undefined)
|
|
24
|
+
sym.rawKind = init.rawKind;
|
|
25
|
+
if (init.signature !== undefined)
|
|
26
|
+
sym.signature = init.signature;
|
|
27
|
+
if (init.exported !== undefined)
|
|
28
|
+
sym.exported = init.exported;
|
|
29
|
+
if (init.doc !== undefined)
|
|
30
|
+
sym.doc = init.doc;
|
|
31
|
+
return sym;
|
|
32
|
+
}
|
|
33
|
+
/** Count a symbol tree, including nested children. */
|
|
34
|
+
export function countSymbols(symbols) {
|
|
35
|
+
let n = 0;
|
|
36
|
+
for (const s of symbols)
|
|
37
|
+
n += 1 + countSymbols(s.children);
|
|
38
|
+
return n;
|
|
39
|
+
}
|
|
40
|
+
/** Strip signature/doc/rawKind for the compact "outline" detail level. */
|
|
41
|
+
export function toOutline(symbols) {
|
|
42
|
+
return symbols.map((s) => {
|
|
43
|
+
const out = {
|
|
44
|
+
name: s.name,
|
|
45
|
+
kind: s.kind,
|
|
46
|
+
visibility: s.visibility,
|
|
47
|
+
range: s.range,
|
|
48
|
+
children: toOutline(s.children),
|
|
49
|
+
};
|
|
50
|
+
if (s.exported !== undefined)
|
|
51
|
+
out.exported = s.exported;
|
|
52
|
+
if (s.decorators)
|
|
53
|
+
out.decorators = s.decorators;
|
|
54
|
+
return out;
|
|
55
|
+
});
|
|
56
|
+
}
|