code2schema 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code2schema/__init__.py +10 -0
- code2schema/analyzer/__init__.py +1 -0
- code2schema/analyzer/cqrs.py +166 -0
- code2schema/analyzer/events.py +162 -0
- code2schema/analyzer/graph.py +146 -0
- code2schema/cli.py +105 -0
- code2schema/codegen/__init__.py +117 -0
- code2schema/codegen/visualizer.py +317 -0
- code2schema/core/__init__.py +1 -0
- code2schema/core/extractor.py +160 -0
- code2schema/core/models.py +95 -0
- code2schema-0.1.1.dist-info/METADATA +211 -0
- code2schema-0.1.1.dist-info/RECORD +17 -0
- code2schema-0.1.1.dist-info/WHEEL +5 -0
- code2schema-0.1.1.dist-info/entry_points.txt +2 -0
- code2schema-0.1.1.dist-info/licenses/LICENSE +201 -0
- code2schema-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""
|
|
2
|
+
code2schema.codegen
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Generatory wyjściowe:
|
|
5
|
+
- JSON (domyślny)
|
|
6
|
+
- Proto (gRPC .proto)
|
|
7
|
+
- Markdown summary
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Literal
|
|
14
|
+
|
|
15
|
+
from code2schema.core.models import CQRSRole, SchemaIR
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ── JSON ──────────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
def to_json(schema: SchemaIR, indent: int = 2) -> str:
|
|
21
|
+
"""Serializuje SchemaIR do JSON."""
|
|
22
|
+
return json.dumps(schema.model_dump(), indent=indent, default=str)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def write_json(schema: SchemaIR, path: Path) -> None:
|
|
26
|
+
path.write_text(to_json(schema), encoding="utf-8")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ── Proto ─────────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
def to_proto(schema: SchemaIR) -> str:
|
|
32
|
+
"""Generuje .proto z modelu CQRS."""
|
|
33
|
+
lines: list[str] = [
|
|
34
|
+
'syntax = "proto3";',
|
|
35
|
+
"",
|
|
36
|
+
f"// Generated by code2schema v0.1",
|
|
37
|
+
f"// Modules: {len(schema.modules)}, "
|
|
38
|
+
f"Commands: {len(schema.commands())}, "
|
|
39
|
+
f"Queries: {len(schema.queries())}",
|
|
40
|
+
"",
|
|
41
|
+
"package code2schema;",
|
|
42
|
+
"",
|
|
43
|
+
"message Request { string payload = 1; }",
|
|
44
|
+
"message Response { string result = 1; bool success = 2; }",
|
|
45
|
+
"",
|
|
46
|
+
"service Code2Schema {",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
for func in schema.all_functions():
|
|
50
|
+
if func.role in (CQRSRole.QUERY, CQRSRole.COMMAND, CQRSRole.ORCHESTRATOR):
|
|
51
|
+
suffix = func.role.value.capitalize()
|
|
52
|
+
safe_name = _safe_proto_name(func.name)
|
|
53
|
+
lines.append(
|
|
54
|
+
f" rpc {safe_name}{suffix} (Request) returns (Response);"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
lines += ["}", ""]
|
|
58
|
+
return "\n".join(lines)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def write_proto(schema: SchemaIR, path: Path) -> None:
|
|
62
|
+
path.write_text(to_proto(schema), encoding="utf-8")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ── Markdown summary ──────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
def to_markdown(schema: SchemaIR) -> str:
|
|
68
|
+
"""Zwraca czytelne Markdown podsumowanie."""
|
|
69
|
+
funcs = schema.all_functions()
|
|
70
|
+
total = len(funcs)
|
|
71
|
+
commands = len(schema.commands())
|
|
72
|
+
queries = len(schema.queries())
|
|
73
|
+
orchestrators = len(schema.orchestrators())
|
|
74
|
+
|
|
75
|
+
lines: list[str] = [
|
|
76
|
+
"# Code2Schema Report",
|
|
77
|
+
"",
|
|
78
|
+
f"**Modules:** {len(schema.modules)} "
|
|
79
|
+
f"**Functions:** {total} "
|
|
80
|
+
f"**Workflows:** {len(schema.workflows)}",
|
|
81
|
+
"",
|
|
82
|
+
"## CQRS Distribution",
|
|
83
|
+
"",
|
|
84
|
+
f"| Role | Count |",
|
|
85
|
+
f"|------|-------|",
|
|
86
|
+
f"| Query | {queries} |",
|
|
87
|
+
f"| Command | {commands} |",
|
|
88
|
+
f"| Orchestrator | {orchestrators} |",
|
|
89
|
+
"",
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
if schema.rules:
|
|
93
|
+
lines += ["## Quality Rules", ""]
|
|
94
|
+
for rule in schema.rules:
|
|
95
|
+
icon = "🔴" if rule.severity == "error" else "🟡"
|
|
96
|
+
lines.append(f"- {icon} **{rule.id}** `{rule.target}` — {rule.condition}")
|
|
97
|
+
lines.append("")
|
|
98
|
+
|
|
99
|
+
if schema.workflows:
|
|
100
|
+
lines += ["## Workflows (Orchestrators)", ""]
|
|
101
|
+
for wf in schema.workflows[:20]: # max 20
|
|
102
|
+
steps = " → ".join(s.callee for s in wf.steps[:8])
|
|
103
|
+
lines.append(f"- **{wf.name}**: `{wf.entry}` → {steps}")
|
|
104
|
+
lines.append("")
|
|
105
|
+
|
|
106
|
+
return "\n".join(lines)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def write_markdown(schema: SchemaIR, path: Path) -> None:
|
|
110
|
+
path.write_text(to_markdown(schema), encoding="utf-8")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ── helpers ───────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
def _safe_proto_name(name: str) -> str:
|
|
116
|
+
"""CamelCase dla nazw proto RPC."""
|
|
117
|
+
return "".join(part.capitalize() for part in name.split("_"))
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"""
|
|
2
|
+
code2schema.codegen.visualizer
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Generuje interaktywny HTML z grafem CQRS (D3.js force layout).
|
|
5
|
+
Bez zewnętrznych zależności poza stdlib — D3 ładowany z CDN.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from code2schema.core.models import CQRSRole, SchemaIR
|
|
14
|
+
|
|
15
|
+
# ── Kolory ról ────────────────────────────────────────────────────────────────
|
|
16
|
+
ROLE_COLOR: dict[str, str] = {
|
|
17
|
+
CQRSRole.QUERY: "#4ade80", # zielony
|
|
18
|
+
CQRSRole.COMMAND: "#fb923c", # pomarańczowy
|
|
19
|
+
CQRSRole.ORCHESTRATOR: "#a78bfa", # fioletowy
|
|
20
|
+
CQRSRole.UNKNOWN: "#94a3b8", # szary
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
ROLE_EMOJI: dict[str, str] = {
|
|
24
|
+
CQRSRole.QUERY: "🔍",
|
|
25
|
+
CQRSRole.COMMAND: "✏️",
|
|
26
|
+
CQRSRole.ORCHESTRATOR: "🔀",
|
|
27
|
+
CQRSRole.UNKNOWN: "❓",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _build_graph_data(schema: SchemaIR) -> dict[str, Any]:
|
|
32
|
+
"""Buduje nodes/links dla D3 force graph."""
|
|
33
|
+
nodes: list[dict] = []
|
|
34
|
+
links: list[dict] = []
|
|
35
|
+
node_ids: dict[str, int] = {}
|
|
36
|
+
|
|
37
|
+
for func in schema.all_functions():
|
|
38
|
+
idx = len(nodes)
|
|
39
|
+
node_ids[func.name] = idx
|
|
40
|
+
nodes.append({
|
|
41
|
+
"id": idx,
|
|
42
|
+
"name": func.name,
|
|
43
|
+
"module": func.module,
|
|
44
|
+
"role": func.role.value,
|
|
45
|
+
"color": ROLE_COLOR.get(func.role, "#94a3b8"),
|
|
46
|
+
"emoji": ROLE_EMOJI.get(func.role, "❓"),
|
|
47
|
+
"fan_out": func.fan_out,
|
|
48
|
+
"lines": func.lines,
|
|
49
|
+
"side_effects": [s.value for s in func.side_effects],
|
|
50
|
+
"is_async": func.is_async,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
for func in schema.all_functions():
|
|
54
|
+
src = node_ids.get(func.name)
|
|
55
|
+
for callee in func.calls:
|
|
56
|
+
dst = node_ids.get(callee)
|
|
57
|
+
if src is not None and dst is not None and src != dst:
|
|
58
|
+
links.append({"source": src, "target": dst})
|
|
59
|
+
|
|
60
|
+
rules_by_target = {}
|
|
61
|
+
for r in schema.rules:
|
|
62
|
+
rules_by_target.setdefault(r.target.split(".")[-1], []).append(r.id)
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
"nodes": nodes,
|
|
66
|
+
"links": links,
|
|
67
|
+
"rules": rules_by_target,
|
|
68
|
+
"stats": {
|
|
69
|
+
"modules": len(schema.modules),
|
|
70
|
+
"functions": len(nodes),
|
|
71
|
+
"commands": len(schema.commands()),
|
|
72
|
+
"queries": len(schema.queries()),
|
|
73
|
+
"orchestrators": len(schema.orchestrators()),
|
|
74
|
+
"workflows": len(schema.workflows),
|
|
75
|
+
"rules": len(schema.rules),
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
_HTML_TEMPLATE = r"""<!DOCTYPE html>
|
|
81
|
+
<html lang="pl">
|
|
82
|
+
<head>
|
|
83
|
+
<meta charset="UTF-8">
|
|
84
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
85
|
+
<title>Code2Schema — CQRS Visualizer</title>
|
|
86
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
|
|
87
|
+
<style>
|
|
88
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
89
|
+
body{font-family:'Segoe UI',system-ui,sans-serif;background:#0f172a;color:#e2e8f0;height:100vh;display:flex;flex-direction:column}
|
|
90
|
+
header{padding:12px 20px;background:#1e293b;border-bottom:1px solid #334155;display:flex;align-items:center;gap:16px;flex-shrink:0}
|
|
91
|
+
header h1{font-size:18px;font-weight:700;color:#f8fafc}
|
|
92
|
+
header h1 span{color:#818cf8}
|
|
93
|
+
.stats{display:flex;gap:10px;margin-left:auto;flex-wrap:wrap}
|
|
94
|
+
.stat{background:#0f172a;border:1px solid #334155;border-radius:8px;padding:4px 12px;font-size:12px;display:flex;gap:6px;align-items:center}
|
|
95
|
+
.stat b{color:#f8fafc}
|
|
96
|
+
.main{display:flex;flex:1;overflow:hidden}
|
|
97
|
+
#graph{flex:1;overflow:hidden;cursor:grab}
|
|
98
|
+
#graph:active{cursor:grabbing}
|
|
99
|
+
#sidebar{width:280px;background:#1e293b;border-left:1px solid #334155;overflow-y:auto;flex-shrink:0;padding:16px;display:flex;flex-direction:column;gap:12px}
|
|
100
|
+
.legend h3,.panel h3{font-size:11px;text-transform:uppercase;letter-spacing:.08em;color:#94a3b8;margin-bottom:8px}
|
|
101
|
+
.legend-row{display:flex;align-items:center;gap:8px;margin-bottom:6px;cursor:pointer;padding:4px 6px;border-radius:6px;transition:background .15s}
|
|
102
|
+
.legend-row:hover{background:#0f172a}
|
|
103
|
+
.legend-row input{cursor:pointer}
|
|
104
|
+
.dot{width:14px;height:14px;border-radius:50%;flex-shrink:0}
|
|
105
|
+
.legend-row span{font-size:13px}
|
|
106
|
+
.panel{background:#0f172a;border-radius:8px;padding:12px}
|
|
107
|
+
.panel p{font-size:12px;color:#94a3b8;margin-bottom:8px}
|
|
108
|
+
#detail{min-height:120px}
|
|
109
|
+
#detail .fname{font-size:15px;font-weight:600;color:#f8fafc;margin-bottom:4px}
|
|
110
|
+
#detail .fmodule{font-size:11px;color:#64748b;margin-bottom:10px;word-break:break-all}
|
|
111
|
+
#detail .badge{display:inline-block;border-radius:9999px;font-size:11px;padding:2px 10px;font-weight:600;margin-bottom:8px}
|
|
112
|
+
.tag{display:inline-block;background:#1e293b;border:1px solid #334155;border-radius:4px;font-size:10px;padding:1px 6px;margin:2px}
|
|
113
|
+
.rule-item{font-size:11px;color:#fbbf24;margin:2px 0}
|
|
114
|
+
#search{width:100%;background:#0f172a;border:1px solid #334155;border-radius:6px;padding:6px 10px;color:#e2e8f0;font-size:13px;outline:none}
|
|
115
|
+
#search:focus{border-color:#818cf8}
|
|
116
|
+
#search::placeholder{color:#475569}
|
|
117
|
+
.node circle{transition:r .2s,opacity .15s}
|
|
118
|
+
.node text{pointer-events:none;user-select:none}
|
|
119
|
+
.link{stroke:#334155;stroke-opacity:.7}
|
|
120
|
+
.node.dimmed circle{opacity:.2}
|
|
121
|
+
.node.dimmed text{opacity:.2}
|
|
122
|
+
.link.dimmed{stroke-opacity:.08}
|
|
123
|
+
</style>
|
|
124
|
+
</head>
|
|
125
|
+
<body>
|
|
126
|
+
<header>
|
|
127
|
+
<h1>Code2<span>Schema</span> — CQRS Visualizer</h1>
|
|
128
|
+
<div class="stats" id="stats"></div>
|
|
129
|
+
</header>
|
|
130
|
+
<div class="main">
|
|
131
|
+
<svg id="graph"></svg>
|
|
132
|
+
<div id="sidebar">
|
|
133
|
+
<div>
|
|
134
|
+
<input id="search" type="text" placeholder="🔍 Szukaj funkcji...">
|
|
135
|
+
</div>
|
|
136
|
+
<div class="legend">
|
|
137
|
+
<h3>Role CQRS</h3>
|
|
138
|
+
<label class="legend-row"><input type="checkbox" checked data-role="query"><div class="dot" style="background:#4ade80"></div><span>🔍 Query</span></label>
|
|
139
|
+
<label class="legend-row"><input type="checkbox" checked data-role="command"><div class="dot" style="background:#fb923c"></div><span>✏️ Command</span></label>
|
|
140
|
+
<label class="legend-row"><input type="checkbox" checked data-role="orchestrator"><div class="dot" style="background:#a78bfa"></div><span>🔀 Orchestrator</span></label>
|
|
141
|
+
<label class="legend-row"><input type="checkbox" checked data-role="unknown"><div class="dot" style="background:#94a3b8"></div><span>❓ Unknown</span></label>
|
|
142
|
+
</div>
|
|
143
|
+
<div class="panel">
|
|
144
|
+
<h3>Szczegóły węzła</h3>
|
|
145
|
+
<div id="detail"><p>Kliknij węzeł aby zobaczyć szczegóły</p></div>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="panel">
|
|
148
|
+
<h3>Reguły jakości</h3>
|
|
149
|
+
<div id="rules-panel"><p>Brak naruszeń ✅</p></div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
<script>
|
|
154
|
+
const DATA = __GRAPH_DATA__;
|
|
155
|
+
|
|
156
|
+
// ── Stats bar ─────────────────────────────────────────────────────────────────
|
|
157
|
+
const s = DATA.stats;
|
|
158
|
+
document.getElementById('stats').innerHTML = [
|
|
159
|
+
['📦 Modules', s.modules],
|
|
160
|
+
['⚡ Functions', s.functions],
|
|
161
|
+
['🔍 Queries', s.queries],
|
|
162
|
+
['✏️ Commands', s.commands],
|
|
163
|
+
['🔀 Orchestrators', s.orchestrators],
|
|
164
|
+
['🔁 Workflows', s.workflows],
|
|
165
|
+
['⚠️ Rules', s.rules],
|
|
166
|
+
].map(([l,v])=>`<div class="stat">${l} <b>${v}</b></div>`).join('');
|
|
167
|
+
|
|
168
|
+
// ── Rules panel ───────────────────────────────────────────────────────────────
|
|
169
|
+
const rp = document.getElementById('rules-panel');
|
|
170
|
+
const rEntries = Object.entries(DATA.rules);
|
|
171
|
+
if(rEntries.length){
|
|
172
|
+
rp.innerHTML = rEntries.map(([fn,ids])=>
|
|
173
|
+
`<div class="rule-item">⚠️ <b>${fn}</b>: ${ids.join(', ')}</div>`
|
|
174
|
+
).join('');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── D3 Force Graph ────────────────────────────────────────────────────────────
|
|
178
|
+
const svg = d3.select('#graph');
|
|
179
|
+
const container = document.getElementById('graph');
|
|
180
|
+
|
|
181
|
+
let W = container.clientWidth, H = container.clientHeight;
|
|
182
|
+
svg.attr('width', W).attr('height', H);
|
|
183
|
+
|
|
184
|
+
const g = svg.append('g');
|
|
185
|
+
|
|
186
|
+
// Zoom
|
|
187
|
+
svg.call(d3.zoom().scaleExtent([0.1, 4]).on('zoom', e => g.attr('transform', e.transform)));
|
|
188
|
+
|
|
189
|
+
// Arrow marker
|
|
190
|
+
svg.append('defs').append('marker')
|
|
191
|
+
.attr('id','arrow').attr('viewBox','0 -4 8 8').attr('refX',16).attr('refY',0)
|
|
192
|
+
.attr('markerWidth',6).attr('markerHeight',6).attr('orient','auto')
|
|
193
|
+
.append('path').attr('d','M0,-4L8,0L0,4').attr('fill','#475569');
|
|
194
|
+
|
|
195
|
+
const simulation = d3.forceSimulation(DATA.nodes)
|
|
196
|
+
.force('link', d3.forceLink(DATA.links).id(d=>d.id).distance(80).strength(0.4))
|
|
197
|
+
.force('charge', d3.forceManyBody().strength(-220))
|
|
198
|
+
.force('center', d3.forceCenter(W/2, H/2))
|
|
199
|
+
.force('collision', d3.forceCollide(d => nodeRadius(d) + 6));
|
|
200
|
+
|
|
201
|
+
function nodeRadius(d){
|
|
202
|
+
if(d.role==='orchestrator') return 16 + Math.min(d.fan_out, 20);
|
|
203
|
+
if(d.role==='command') return 10;
|
|
204
|
+
return 8;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const link = g.append('g').selectAll('line')
|
|
208
|
+
.data(DATA.links).join('line')
|
|
209
|
+
.attr('class','link')
|
|
210
|
+
.attr('stroke-width', 1.2)
|
|
211
|
+
.attr('marker-end','url(#arrow)');
|
|
212
|
+
|
|
213
|
+
const node = g.append('g').selectAll('.node')
|
|
214
|
+
.data(DATA.nodes).join('g')
|
|
215
|
+
.attr('class','node')
|
|
216
|
+
.call(d3.drag()
|
|
217
|
+
.on('start',(e,d)=>{ if(!e.active) simulation.alphaTarget(0.3).restart(); d.fx=d.x; d.fy=d.y; })
|
|
218
|
+
.on('drag', (e,d)=>{ d.fx=e.x; d.fy=e.y; })
|
|
219
|
+
.on('end', (e,d)=>{ if(!e.active) simulation.alphaTarget(0); d.fx=null; d.fy=null; })
|
|
220
|
+
)
|
|
221
|
+
.on('click', onNodeClick)
|
|
222
|
+
.on('mouseover', onNodeHover)
|
|
223
|
+
.on('mouseout', onNodeOut);
|
|
224
|
+
|
|
225
|
+
node.append('circle')
|
|
226
|
+
.attr('r', nodeRadius)
|
|
227
|
+
.attr('fill', d=>d.color)
|
|
228
|
+
.attr('stroke', '#0f172a')
|
|
229
|
+
.attr('stroke-width', 2);
|
|
230
|
+
|
|
231
|
+
node.append('text')
|
|
232
|
+
.attr('dy', d => nodeRadius(d) + 12)
|
|
233
|
+
.attr('text-anchor','middle')
|
|
234
|
+
.attr('font-size', 10)
|
|
235
|
+
.attr('fill','#94a3b8')
|
|
236
|
+
.text(d => d.name.length > 18 ? d.name.slice(0,17)+'…' : d.name);
|
|
237
|
+
|
|
238
|
+
simulation.on('tick', ()=>{
|
|
239
|
+
link
|
|
240
|
+
.attr('x1',d=>d.source.x).attr('y1',d=>d.source.y)
|
|
241
|
+
.attr('x2',d=>d.target.x).attr('y2',d=>d.target.y);
|
|
242
|
+
node.attr('transform',d=>`translate(${d.x},${d.y})`);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// ── Hover ─────────────────────────────────────────────────────────────────────
|
|
246
|
+
function onNodeHover(e, d){
|
|
247
|
+
const connected = new Set([d.id]);
|
|
248
|
+
DATA.links.forEach(l=>{
|
|
249
|
+
if(l.source.id===d.id||l.target.id===d.id){
|
|
250
|
+
connected.add(l.source.id); connected.add(l.target.id);
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
node.classed('dimmed', n => !connected.has(n.id));
|
|
254
|
+
link.classed('dimmed', l => l.source.id!==d.id && l.target.id!==d.id);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function onNodeOut(){
|
|
258
|
+
node.classed('dimmed', false);
|
|
259
|
+
link.classed('dimmed', false);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── Click detail ──────────────────────────────────────────────────────────────
|
|
263
|
+
function onNodeClick(e, d){
|
|
264
|
+
const roleColors={'query':'#4ade80','command':'#fb923c','orchestrator':'#a78bfa','unknown':'#94a3b8'};
|
|
265
|
+
const rules = DATA.rules[d.name] || [];
|
|
266
|
+
const sideEff = d.side_effects.filter(s=>s!=='none');
|
|
267
|
+
document.getElementById('detail').innerHTML = `
|
|
268
|
+
<div class="fname">${d.emoji} ${d.name}</div>
|
|
269
|
+
<div class="fmodule">${d.module}</div>
|
|
270
|
+
<span class="badge" style="background:${roleColors[d.role]}20;color:${roleColors[d.role]};border:1px solid ${roleColors[d.role]}40">${d.role.toUpperCase()}</span>
|
|
271
|
+
${d.is_async ? '<span class="badge" style="background:#0ea5e920;color:#38bdf8;border:1px solid #0ea5e940">async</span>' : ''}
|
|
272
|
+
<div style="margin-top:8px;font-size:12px;color:#64748b">
|
|
273
|
+
<div>Fan-out: <b style="color:#e2e8f0">${d.fan_out}</b></div>
|
|
274
|
+
<div>Lines: <b style="color:#e2e8f0">${d.lines}</b></div>
|
|
275
|
+
${sideEff.length ? `<div style="margin-top:4px">Side effects:<br>${sideEff.map(s=>`<span class="tag">⚡${s}</span>`).join('')}</div>` : ''}
|
|
276
|
+
${rules.length ? `<div style="margin-top:6px;color:#fbbf24">${rules.map(r=>`⚠️ ${r}`).join('<br>')}</div>` : ''}
|
|
277
|
+
</div>
|
|
278
|
+
`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── Search ────────────────────────────────────────────────────────────────────
|
|
282
|
+
const hidden = new Set();
|
|
283
|
+
document.getElementById('search').addEventListener('input', e=>{
|
|
284
|
+
const q = e.target.value.trim().toLowerCase();
|
|
285
|
+
if(!q){ node.classed('dimmed',false); link.classed('dimmed',false); return; }
|
|
286
|
+
node.classed('dimmed', d => !d.name.toLowerCase().includes(q));
|
|
287
|
+
link.classed('dimmed', l => !l.source.name.toLowerCase().includes(q) && !l.target.name.toLowerCase().includes(q));
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// ── Legend filters ────────────────────────────────────────────────────────────
|
|
291
|
+
document.querySelectorAll('[data-role]').forEach(cb=>{
|
|
292
|
+
cb.addEventListener('change', ()=>{
|
|
293
|
+
const active = new Set([...document.querySelectorAll('[data-role]:checked')].map(c=>c.dataset.role));
|
|
294
|
+
node.style('display', d => active.has(d.role) ? null : 'none');
|
|
295
|
+
link.style('display', l => active.has(l.source.role) && active.has(l.target.role) ? null : 'none');
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// ── Resize ────────────────────────────────────────────────────────────────────
|
|
300
|
+
window.addEventListener('resize', ()=>{
|
|
301
|
+
W = container.clientWidth; H = container.clientHeight;
|
|
302
|
+
svg.attr('width',W).attr('height',H);
|
|
303
|
+
simulation.force('center', d3.forceCenter(W/2,H/2)).alpha(0.3).restart();
|
|
304
|
+
});
|
|
305
|
+
</script>
|
|
306
|
+
</body>
|
|
307
|
+
</html>"""
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def to_html(schema: SchemaIR) -> str:
|
|
311
|
+
"""Generuje interaktywny HTML z grafem CQRS."""
|
|
312
|
+
data = _build_graph_data(schema)
|
|
313
|
+
return _HTML_TEMPLATE.replace("__GRAPH_DATA__", json.dumps(data))
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def write_html(schema: SchemaIR, path: Path) -> None:
|
|
317
|
+
path.write_text(to_html(schema), encoding="utf-8")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# code2schema.core
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""
|
|
2
|
+
code2schema.core.extractor
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Ekstrakcja funkcji i importów z plików .py przy użyciu wbudowanego modułu `ast`.
|
|
5
|
+
Bez zewnętrznych zależności — czyste stdlib.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import List
|
|
13
|
+
|
|
14
|
+
from code2schema.core.models import FunctionIR, ModuleIR
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ── Wzorce side-effectów ─────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
_FILESYSTEM_CALLS: set[str] = {"open", "write", "read", "unlink", "mkdir", "rmdir", "rename"}
|
|
20
|
+
_NETWORK_CALLS: set[str] = {"get", "post", "put", "delete", "patch", "request", "fetch", "connect"}
|
|
21
|
+
_SYSTEM_CALLS: set[str] = {"system", "popen", "subprocess", "Popen", "run", "call", "check_output"}
|
|
22
|
+
_DB_CALLS: set[str] = {"execute", "commit", "rollback", "query", "insert", "update", "delete"}
|
|
23
|
+
|
|
24
|
+
_NETWORK_MODULES: set[str] = {"requests", "httpx", "aiohttp", "urllib", "http"}
|
|
25
|
+
_SYSTEM_MODULES: set[str] = {"os", "subprocess", "shutil", "sys"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _FunctionVisitor(ast.NodeVisitor):
|
|
29
|
+
"""Odwiedza węzły AST i buduje FunctionIR."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, module_name: str) -> None:
|
|
32
|
+
self.module_name = module_name
|
|
33
|
+
self.functions: list[FunctionIR] = []
|
|
34
|
+
self._imports: set[str] = set()
|
|
35
|
+
|
|
36
|
+
# ── imports ───────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
def visit_Import(self, node: ast.Import) -> None:
|
|
39
|
+
for alias in node.names:
|
|
40
|
+
self._imports.add(alias.name.split(".")[0])
|
|
41
|
+
self.generic_visit(node)
|
|
42
|
+
|
|
43
|
+
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
|
|
44
|
+
if node.module:
|
|
45
|
+
self._imports.add(node.module.split(".")[0])
|
|
46
|
+
self.generic_visit(node)
|
|
47
|
+
|
|
48
|
+
# ── functions ─────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
51
|
+
self._process_func(node, is_async=False)
|
|
52
|
+
self.generic_visit(node)
|
|
53
|
+
|
|
54
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
|
55
|
+
self._process_func(node, is_async=True)
|
|
56
|
+
self.generic_visit(node)
|
|
57
|
+
|
|
58
|
+
def _process_func(self, node: ast.FunctionDef | ast.AsyncFunctionDef, is_async: bool) -> None:
|
|
59
|
+
calls = self._collect_calls(node)
|
|
60
|
+
side_effects = self._detect_side_effects(node, calls)
|
|
61
|
+
docstring = ast.get_docstring(node)
|
|
62
|
+
|
|
63
|
+
func = FunctionIR(
|
|
64
|
+
name=node.name,
|
|
65
|
+
module=self.module_name,
|
|
66
|
+
calls=list(dict.fromkeys(calls)), # deduplicate, preserve order
|
|
67
|
+
fan_out=len(set(calls)),
|
|
68
|
+
side_effects=side_effects,
|
|
69
|
+
lines=node.end_lineno - node.lineno + 1 if hasattr(node, "end_lineno") else 0,
|
|
70
|
+
is_async=is_async,
|
|
71
|
+
docstring=docstring,
|
|
72
|
+
)
|
|
73
|
+
self.functions.append(func)
|
|
74
|
+
|
|
75
|
+
def _collect_calls(self, node: ast.AST) -> list[str]:
|
|
76
|
+
"""Zbiera nazwy wszystkich wywołań funkcji wewnątrz węzła."""
|
|
77
|
+
calls: list[str] = []
|
|
78
|
+
for child in ast.walk(node):
|
|
79
|
+
if isinstance(child, ast.Call):
|
|
80
|
+
name = self._resolve_call_name(child.func)
|
|
81
|
+
if name:
|
|
82
|
+
calls.append(name)
|
|
83
|
+
return calls
|
|
84
|
+
|
|
85
|
+
@staticmethod
|
|
86
|
+
def _resolve_call_name(func_node: ast.expr) -> str | None:
|
|
87
|
+
if isinstance(func_node, ast.Name):
|
|
88
|
+
return func_node.id
|
|
89
|
+
if isinstance(func_node, ast.Attribute):
|
|
90
|
+
return func_node.attr
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
def _detect_side_effects(
|
|
94
|
+
self, node: ast.AST, calls: list[str]
|
|
95
|
+
) -> list[str]:
|
|
96
|
+
from code2schema.core.models import SideEffect
|
|
97
|
+
|
|
98
|
+
effects: list[SideEffect] = []
|
|
99
|
+
call_set = set(calls)
|
|
100
|
+
|
|
101
|
+
if call_set & _FILESYSTEM_CALLS:
|
|
102
|
+
effects.append(SideEffect.FILESYSTEM)
|
|
103
|
+
if call_set & _NETWORK_CALLS:
|
|
104
|
+
effects.append(SideEffect.NETWORK)
|
|
105
|
+
if call_set & _SYSTEM_CALLS:
|
|
106
|
+
effects.append(SideEffect.SYSTEM)
|
|
107
|
+
if call_set & _DB_CALLS:
|
|
108
|
+
effects.append(SideEffect.DATABASE)
|
|
109
|
+
|
|
110
|
+
return effects or [SideEffect.NONE]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ── Public API ────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
def extract_module(path: Path) -> ModuleIR | None:
|
|
116
|
+
"""Parsuje jeden plik .py i zwraca ModuleIR."""
|
|
117
|
+
try:
|
|
118
|
+
source = path.read_text(encoding="utf-8", errors="replace")
|
|
119
|
+
tree = ast.parse(source, filename=str(path))
|
|
120
|
+
except SyntaxError:
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
module_name = _path_to_module(path)
|
|
124
|
+
visitor = _FunctionVisitor(module_name)
|
|
125
|
+
visitor.visit(tree)
|
|
126
|
+
|
|
127
|
+
imports = [
|
|
128
|
+
alias.name
|
|
129
|
+
for node in ast.walk(tree)
|
|
130
|
+
for alias in (node.names if isinstance(node, (ast.Import, ast.ImportFrom)) else [])
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
return ModuleIR(
|
|
134
|
+
name=module_name,
|
|
135
|
+
path=str(path),
|
|
136
|
+
functions=visitor.functions,
|
|
137
|
+
imports=list(dict.fromkeys(imports)),
|
|
138
|
+
lines=len(source.splitlines()),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def extract_project(root: Path, exclude: list[str] | None = None) -> list[ModuleIR]:
|
|
143
|
+
"""Rekurencyjnie przetwarza katalog i zwraca listę ModuleIR."""
|
|
144
|
+
exclude = set(exclude or ["__pycache__", ".venv", "venv", "node_modules", ".git"])
|
|
145
|
+
modules: list[ModuleIR] = []
|
|
146
|
+
|
|
147
|
+
for py_file in root.rglob("*.py"):
|
|
148
|
+
if any(part in exclude for part in py_file.parts):
|
|
149
|
+
continue
|
|
150
|
+
mod = extract_module(py_file)
|
|
151
|
+
if mod is not None:
|
|
152
|
+
modules.append(mod)
|
|
153
|
+
|
|
154
|
+
return modules
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _path_to_module(path: Path) -> str:
|
|
158
|
+
"""Konwertuje ścieżkę pliku na notację modułu (dots)."""
|
|
159
|
+
parts = list(path.with_suffix("").parts)
|
|
160
|
+
return ".".join(parts)
|