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
code2schema/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""code2schema — Semantic Compiler for Software Systems."""
|
|
2
|
+
from code2schema.core.models import SchemaIR, CQRSRole, FunctionIR, ModuleIR
|
|
3
|
+
from code2schema.core.extractor import extract_project, extract_module
|
|
4
|
+
from code2schema.analyzer.cqrs import analyze
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"SchemaIR", "CQRSRole", "FunctionIR", "ModuleIR",
|
|
8
|
+
"extract_project", "extract_module", "analyze",
|
|
9
|
+
]
|
|
10
|
+
__version__ = "0.1.1"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# code2schema.analyzer
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
code2schema.analyzer.cqrs
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
CQRS inference + call graph (NetworkX) + reguły jakości.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import List
|
|
9
|
+
|
|
10
|
+
import networkx as nx
|
|
11
|
+
|
|
12
|
+
from code2schema.core.models import (
|
|
13
|
+
CQRSRole,
|
|
14
|
+
FunctionIR,
|
|
15
|
+
ModuleIR,
|
|
16
|
+
RuleIR,
|
|
17
|
+
SchemaIR,
|
|
18
|
+
SideEffect,
|
|
19
|
+
WorkflowIR,
|
|
20
|
+
WorkflowStep,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# ── Progi heurystyczne ───────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
FAN_OUT_ORCHESTRATOR = 5 # >= N wywołań → orchestrator
|
|
26
|
+
FAN_OUT_HIGH = 10 # alert reguły
|
|
27
|
+
CC_LIMIT = 15 # (placeholder, liczony zewnętrznie)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ── CQRS Inference ───────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
def _infer_role(func: FunctionIR) -> CQRSRole:
|
|
33
|
+
"""Klasyfikuje funkcję na podstawie side-effectów i fan-outu."""
|
|
34
|
+
has_side_effects = SideEffect.NONE not in func.side_effects
|
|
35
|
+
|
|
36
|
+
if func.fan_out >= FAN_OUT_ORCHESTRATOR:
|
|
37
|
+
return CQRSRole.ORCHESTRATOR
|
|
38
|
+
|
|
39
|
+
if has_side_effects:
|
|
40
|
+
return CQRSRole.COMMAND
|
|
41
|
+
|
|
42
|
+
if func.fan_out == 0:
|
|
43
|
+
return CQRSRole.QUERY
|
|
44
|
+
|
|
45
|
+
# Małe funkcje bez side-effectów, ale z wywołaniami → query
|
|
46
|
+
return CQRSRole.QUERY
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ── Call Graph ───────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
def build_call_graph(modules: List[ModuleIR]) -> nx.DiGraph:
|
|
52
|
+
"""Buduje skierowany graf wywołań między funkcjami."""
|
|
53
|
+
G = nx.DiGraph()
|
|
54
|
+
|
|
55
|
+
all_names: set[str] = set()
|
|
56
|
+
for mod in modules:
|
|
57
|
+
for f in mod.functions:
|
|
58
|
+
all_names.add(f.name)
|
|
59
|
+
G.add_node(f.qualified_name, role=f.role, module=mod.name)
|
|
60
|
+
|
|
61
|
+
for mod in modules:
|
|
62
|
+
for f in mod.functions:
|
|
63
|
+
for callee in f.calls:
|
|
64
|
+
if callee in all_names:
|
|
65
|
+
G.add_edge(f.qualified_name, callee)
|
|
66
|
+
|
|
67
|
+
return G
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def detect_cycles(G: nx.DiGraph) -> List[List[str]]:
|
|
71
|
+
"""Zwraca listę cykli w grafie wywołań."""
|
|
72
|
+
return list(nx.simple_cycles(G))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def centrality(G: nx.DiGraph) -> dict[str, float]:
|
|
76
|
+
"""PageRank — im wyższy, tym bardziej 'centralny' w architekturze."""
|
|
77
|
+
return nx.pagerank(G) if G.number_of_edges() > 0 else {}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ── Workflow DAG ──────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
def build_workflows(modules: List[ModuleIR]) -> List[WorkflowIR]:
|
|
83
|
+
"""Buduje DAG wykonania dla każdego orkiestratora."""
|
|
84
|
+
workflows: list[WorkflowIR] = []
|
|
85
|
+
for mod in modules:
|
|
86
|
+
for f in mod.functions:
|
|
87
|
+
if f.role == CQRSRole.ORCHESTRATOR and f.calls:
|
|
88
|
+
workflow = WorkflowIR(
|
|
89
|
+
name=f"workflow_{f.name}",
|
|
90
|
+
entry=f.qualified_name,
|
|
91
|
+
steps=[
|
|
92
|
+
WorkflowStep(callee=c, is_async=f.is_async)
|
|
93
|
+
for c in f.calls
|
|
94
|
+
],
|
|
95
|
+
)
|
|
96
|
+
workflows.append(workflow)
|
|
97
|
+
return workflows
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ── Rules ────────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
def generate_rules(modules: List[ModuleIR]) -> List[RuleIR]:
|
|
103
|
+
"""Generuje heurystyczne reguły jakości na podstawie IR."""
|
|
104
|
+
rules: list[RuleIR] = []
|
|
105
|
+
|
|
106
|
+
for mod in modules:
|
|
107
|
+
for f in mod.functions:
|
|
108
|
+
if f.fan_out >= FAN_OUT_HIGH:
|
|
109
|
+
rules.append(RuleIR(
|
|
110
|
+
id="HIGH_FAN_OUT",
|
|
111
|
+
target=f.qualified_name,
|
|
112
|
+
condition=f"fan_out={f.fan_out} >= {FAN_OUT_HIGH}",
|
|
113
|
+
action="refactor_to_service",
|
|
114
|
+
severity="error",
|
|
115
|
+
))
|
|
116
|
+
if f.lines > 100:
|
|
117
|
+
rules.append(RuleIR(
|
|
118
|
+
id="LONG_FUNCTION",
|
|
119
|
+
target=f.qualified_name,
|
|
120
|
+
condition=f"lines={f.lines} > 100",
|
|
121
|
+
action="split_function",
|
|
122
|
+
severity="warning",
|
|
123
|
+
))
|
|
124
|
+
if (f.role == CQRSRole.QUERY
|
|
125
|
+
and SideEffect.NONE not in f.side_effects):
|
|
126
|
+
rules.append(RuleIR(
|
|
127
|
+
id="QUERY_WITH_SIDE_EFFECTS",
|
|
128
|
+
target=f.qualified_name,
|
|
129
|
+
condition="role=query but has side effects",
|
|
130
|
+
action="separate_command_from_query",
|
|
131
|
+
severity="warning",
|
|
132
|
+
))
|
|
133
|
+
|
|
134
|
+
return rules
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# ── Main entry ────────────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
def analyze(modules: List[ModuleIR]) -> SchemaIR:
|
|
140
|
+
"""Pełna analiza: CQRS + graf + workflow + reguły → SchemaIR."""
|
|
141
|
+
|
|
142
|
+
# 1. Ustaw rolę CQRS dla każdej funkcji
|
|
143
|
+
for mod in modules:
|
|
144
|
+
for f in mod.functions:
|
|
145
|
+
f.role = _infer_role(f)
|
|
146
|
+
|
|
147
|
+
# 2. Graf wywołań
|
|
148
|
+
G = build_call_graph(modules)
|
|
149
|
+
|
|
150
|
+
# 3. Wzbogać fan-out danymi z grafu
|
|
151
|
+
out_degrees = dict(G.out_degree())
|
|
152
|
+
for mod in modules:
|
|
153
|
+
for f in mod.functions:
|
|
154
|
+
qn = f.qualified_name
|
|
155
|
+
if qn in out_degrees:
|
|
156
|
+
f.fan_out = max(f.fan_out, out_degrees[qn])
|
|
157
|
+
# Ponów rolę po uaktualnieniu fan-out
|
|
158
|
+
f.role = _infer_role(f)
|
|
159
|
+
|
|
160
|
+
# 4. Workflow DAG
|
|
161
|
+
workflows = build_workflows(modules)
|
|
162
|
+
|
|
163
|
+
# 5. Reguły
|
|
164
|
+
rules = generate_rules(modules)
|
|
165
|
+
|
|
166
|
+
return SchemaIR(modules=modules, workflows=workflows, rules=rules)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""
|
|
2
|
+
code2schema.analyzer.events
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Inferencja modelu zdarzeń (DDD / Event Sourcing):
|
|
5
|
+
- wykrywa funkcje, które "emitują" zdarzenia
|
|
6
|
+
- klasyfikuje: Command Handler → Event → Event Handler
|
|
7
|
+
- buduje Event Flow Map
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import List
|
|
14
|
+
|
|
15
|
+
from code2schema.core.models import CQRSRole, FunctionIR, ModuleIR
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ── Heurystyki nazw ───────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
_EVENT_EMIT_PATTERNS = re.compile(
|
|
21
|
+
r"(emit|publish|dispatch|fire|send|enqueue|produce|notify|trigger|broadcast)",
|
|
22
|
+
re.I,
|
|
23
|
+
)
|
|
24
|
+
_EVENT_HANDLER_PATTERNS = re.compile(
|
|
25
|
+
r"(on_|handle_|listener|subscriber|consumer|process_|receive_)",
|
|
26
|
+
re.I,
|
|
27
|
+
)
|
|
28
|
+
_AGGREGATE_PATTERNS = re.compile(
|
|
29
|
+
r"(create|update|delete|save|store|persist|commit|apply)",
|
|
30
|
+
re.I,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class DomainEvent:
|
|
36
|
+
name: str # np. "UserCreated"
|
|
37
|
+
emitted_by: str # qualified_name funkcji emitującej
|
|
38
|
+
handled_by: List[str] = field(default_factory=list)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class CommandHandler:
|
|
43
|
+
name: str
|
|
44
|
+
command: str # nazwa wywołania, które traktujemy jako "command"
|
|
45
|
+
emits: List[str] = field(default_factory=list)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class EventModel:
|
|
50
|
+
commands: List[CommandHandler] = field(default_factory=list)
|
|
51
|
+
events: List[DomainEvent] = field(default_factory=list)
|
|
52
|
+
aggregates: List[str] = field(default_factory=list)
|
|
53
|
+
|
|
54
|
+
def summary(self) -> str:
|
|
55
|
+
lines = [
|
|
56
|
+
f"Commands : {len(self.commands)}",
|
|
57
|
+
f"Domain Events : {len(self.events)}",
|
|
58
|
+
f"Aggregates : {len(self.aggregates)}",
|
|
59
|
+
]
|
|
60
|
+
if self.events:
|
|
61
|
+
lines.append("\nDomain Events:")
|
|
62
|
+
for ev in self.events[:15]:
|
|
63
|
+
handlers = ", ".join(ev.handled_by[:3]) or "—"
|
|
64
|
+
lines.append(f" {ev.name:30s} ← {ev.emitted_by.split('.')[-1]:25s} → [{handlers}]")
|
|
65
|
+
if self.commands:
|
|
66
|
+
lines.append("\nCommand Handlers (top 10):")
|
|
67
|
+
for cmd in self.commands[:10]:
|
|
68
|
+
emits = ", ".join(cmd.emits[:3]) or "—"
|
|
69
|
+
lines.append(f" {cmd.name:30s} emits=[{emits}]")
|
|
70
|
+
return "\n".join(lines)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ── Inference ─────────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
def infer_event_model(modules: List[ModuleIR]) -> EventModel:
|
|
76
|
+
"""Przechodzi po IR i buduje model zdarzeń."""
|
|
77
|
+
model = EventModel()
|
|
78
|
+
|
|
79
|
+
all_funcs: dict[str, FunctionIR] = {}
|
|
80
|
+
for mod in modules:
|
|
81
|
+
for f in mod.functions:
|
|
82
|
+
all_funcs[f.qualified_name] = f
|
|
83
|
+
all_funcs[f.name] = f # short alias
|
|
84
|
+
|
|
85
|
+
# Krok 1: znajdź emiterów
|
|
86
|
+
emitters: dict[str, DomainEvent] = {}
|
|
87
|
+
for mod in modules:
|
|
88
|
+
for f in mod.functions:
|
|
89
|
+
if _EVENT_EMIT_PATTERNS.search(f.name) or any(
|
|
90
|
+
_EVENT_EMIT_PATTERNS.search(c) for c in f.calls
|
|
91
|
+
):
|
|
92
|
+
event_name = _derive_event_name(f.name)
|
|
93
|
+
ev = DomainEvent(name=event_name, emitted_by=f.qualified_name)
|
|
94
|
+
emitters[f.name] = ev
|
|
95
|
+
model.events.append(ev)
|
|
96
|
+
|
|
97
|
+
# Krok 2: znajdź handlerów
|
|
98
|
+
for mod in modules:
|
|
99
|
+
for f in mod.functions:
|
|
100
|
+
if _EVENT_HANDLER_PATTERNS.search(f.name):
|
|
101
|
+
# sprawdź, czy handler pasuje do jakiegoś zdarzenia
|
|
102
|
+
for ev in model.events:
|
|
103
|
+
keyword = ev.name.lower().replace("event", "").strip()
|
|
104
|
+
if keyword and keyword in f.name.lower():
|
|
105
|
+
ev.handled_by.append(f.qualified_name)
|
|
106
|
+
|
|
107
|
+
# Krok 3: command handlers
|
|
108
|
+
for mod in modules:
|
|
109
|
+
for f in mod.functions:
|
|
110
|
+
if f.role == CQRSRole.COMMAND or _AGGREGATE_PATTERNS.search(f.name):
|
|
111
|
+
emits = [
|
|
112
|
+
_derive_event_name(c)
|
|
113
|
+
for c in f.calls
|
|
114
|
+
if _EVENT_EMIT_PATTERNS.search(c)
|
|
115
|
+
]
|
|
116
|
+
if emits or f.role == CQRSRole.COMMAND:
|
|
117
|
+
model.commands.append(
|
|
118
|
+
CommandHandler(
|
|
119
|
+
name=f.qualified_name,
|
|
120
|
+
command=f.name,
|
|
121
|
+
emits=emits,
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Krok 4: agregaty — moduły z operacjami CRUD
|
|
126
|
+
for mod in modules:
|
|
127
|
+
crud_score = sum(
|
|
128
|
+
1 for f in mod.functions
|
|
129
|
+
if _AGGREGATE_PATTERNS.search(f.name)
|
|
130
|
+
)
|
|
131
|
+
if crud_score >= 2:
|
|
132
|
+
model.aggregates.append(mod.name)
|
|
133
|
+
|
|
134
|
+
return model
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _derive_event_name(func_name: str) -> str:
|
|
138
|
+
"""save_user → UserSaved, create_order → OrderCreated."""
|
|
139
|
+
# Usuń prefiks czasownikowy
|
|
140
|
+
for prefix in ("on_", "handle_", "emit_", "publish_", "dispatch_", "fire_"):
|
|
141
|
+
if func_name.startswith(prefix):
|
|
142
|
+
func_name = func_name[len(prefix):]
|
|
143
|
+
break
|
|
144
|
+
|
|
145
|
+
parts = func_name.split("_")
|
|
146
|
+
if len(parts) >= 2:
|
|
147
|
+
verb = parts[-1] # ostatni segment = czasownik → past tense
|
|
148
|
+
subject = "".join(p.capitalize() for p in parts[:-1])
|
|
149
|
+
past = _past_tense(verb)
|
|
150
|
+
return f"{subject}{past}"
|
|
151
|
+
|
|
152
|
+
return func_name.capitalize() + "Event"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _past_tense(verb: str) -> str:
|
|
156
|
+
_map = {
|
|
157
|
+
"create": "Created", "update": "Updated", "delete": "Deleted",
|
|
158
|
+
"save": "Saved", "send": "Sent", "emit": "Emitted",
|
|
159
|
+
"publish": "Published", "register": "Registered",
|
|
160
|
+
"process": "Processed", "notify": "Notified",
|
|
161
|
+
}
|
|
162
|
+
return _map.get(verb.lower(), verb.capitalize() + "d")
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""
|
|
2
|
+
code2schema.analyzer.graph
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Zaawansowana analiza grafu wywołań:
|
|
5
|
+
- eksport GraphML (do Gephi / yEd / Neo4j)
|
|
6
|
+
- PageRank centrality (kto jest "centrum" architektury)
|
|
7
|
+
- detekcja hubów i cykli
|
|
8
|
+
- metryki warstw (layered architecture check)
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Dict, List, Tuple
|
|
14
|
+
|
|
15
|
+
import networkx as nx
|
|
16
|
+
|
|
17
|
+
from code2schema.core.models import CQRSRole, ModuleIR, SchemaIR
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ── Builder ───────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
def build_rich_graph(schema: SchemaIR) -> nx.DiGraph:
|
|
23
|
+
"""Buduje pełny graf z atrybutami węzłów (rola, moduł, fan-out)."""
|
|
24
|
+
G = nx.DiGraph()
|
|
25
|
+
|
|
26
|
+
func_map: dict[str, str] = {} # short_name → qualified_name (last wins)
|
|
27
|
+
for mod in schema.modules:
|
|
28
|
+
for f in mod.functions:
|
|
29
|
+
G.add_node(
|
|
30
|
+
f.qualified_name,
|
|
31
|
+
label=f.name,
|
|
32
|
+
role=f.role.value,
|
|
33
|
+
module=mod.name,
|
|
34
|
+
fan_out=f.fan_out,
|
|
35
|
+
lines=f.lines,
|
|
36
|
+
is_async=int(f.is_async),
|
|
37
|
+
)
|
|
38
|
+
func_map[f.name] = f.qualified_name
|
|
39
|
+
|
|
40
|
+
for mod in schema.modules:
|
|
41
|
+
for f in mod.functions:
|
|
42
|
+
src = f.qualified_name
|
|
43
|
+
for callee in f.calls:
|
|
44
|
+
dst = func_map.get(callee)
|
|
45
|
+
if dst and dst != src:
|
|
46
|
+
G.add_edge(src, dst)
|
|
47
|
+
|
|
48
|
+
return G
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ── Metrics ───────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
def centrality_report(G: nx.DiGraph, top_n: int = 10) -> List[Tuple[str, float]]:
|
|
54
|
+
"""PageRank — funkcje najważniejsze architektonicznie."""
|
|
55
|
+
if G.number_of_edges() == 0:
|
|
56
|
+
return []
|
|
57
|
+
pr = nx.pagerank(G, alpha=0.85)
|
|
58
|
+
return sorted(pr.items(), key=lambda x: x[1], reverse=True)[:top_n]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def hub_nodes(G: nx.DiGraph, threshold: int = 5) -> List[str]:
|
|
62
|
+
"""Węzły z fan-in >= threshold — potencjalne hub-moduły."""
|
|
63
|
+
return [n for n, d in G.in_degree() if d >= threshold]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def detect_cycles(G: nx.DiGraph) -> List[List[str]]:
|
|
67
|
+
return list(nx.simple_cycles(G))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def layer_violations(schema: SchemaIR) -> List[str]:
|
|
71
|
+
"""
|
|
72
|
+
Prosta heurystyka: query nie powinno wołać command.
|
|
73
|
+
Zwraca listę naruszeń.
|
|
74
|
+
"""
|
|
75
|
+
func_roles: dict[str, CQRSRole] = {}
|
|
76
|
+
for mod in schema.modules:
|
|
77
|
+
for f in mod.functions:
|
|
78
|
+
func_roles[f.name] = f.role
|
|
79
|
+
|
|
80
|
+
violations: list[str] = []
|
|
81
|
+
for mod in schema.modules:
|
|
82
|
+
for f in mod.functions:
|
|
83
|
+
if f.role == CQRSRole.QUERY:
|
|
84
|
+
for callee in f.calls:
|
|
85
|
+
if func_roles.get(callee) == CQRSRole.COMMAND:
|
|
86
|
+
violations.append(
|
|
87
|
+
f"LAYER_VIOLATION: {f.qualified_name} (query) → {callee} (command)"
|
|
88
|
+
)
|
|
89
|
+
return violations
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ── Export ────────────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
def write_graphml(G: nx.DiGraph, path: Path) -> None:
|
|
95
|
+
"""Eksport do GraphML — kompatybilny z Gephi / yEd / Neo4j importer."""
|
|
96
|
+
nx.write_graphml(G, str(path))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def write_dot(G: nx.DiGraph, path: Path) -> None:
|
|
100
|
+
"""Eksport do DOT — renderowany przez Graphviz."""
|
|
101
|
+
lines = ["digraph CallGraph {", ' rankdir=LR;', ' node [shape=box];', ""]
|
|
102
|
+
for n, data in G.nodes(data=True):
|
|
103
|
+
role = data.get("role", "unknown")
|
|
104
|
+
color = {
|
|
105
|
+
"query": "lightblue",
|
|
106
|
+
"command": "lightsalmon",
|
|
107
|
+
"orchestrator": "lightgreen",
|
|
108
|
+
}.get(role, "white")
|
|
109
|
+
label = data.get("label", n.split(".")[-1])
|
|
110
|
+
lines.append(f' "{n}" [label="{label}" style=filled fillcolor="{color}"];')
|
|
111
|
+
lines.append("")
|
|
112
|
+
for src, dst in G.edges():
|
|
113
|
+
lines.append(f' "{src}" -> "{dst}";')
|
|
114
|
+
lines += ["}", ""]
|
|
115
|
+
path.write_text("\n".join(lines), encoding="utf-8")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def graph_summary(G: nx.DiGraph, schema: SchemaIR) -> str:
|
|
119
|
+
"""Tekstowe podsumowanie grafu."""
|
|
120
|
+
cycles = detect_cycles(G)
|
|
121
|
+
hubs = hub_nodes(G)
|
|
122
|
+
top = centrality_report(G, top_n=5)
|
|
123
|
+
violations = layer_violations(schema)
|
|
124
|
+
|
|
125
|
+
lines = [
|
|
126
|
+
f"Nodes : {G.number_of_nodes()}",
|
|
127
|
+
f"Edges : {G.number_of_edges()}",
|
|
128
|
+
f"Cycles : {len(cycles)}",
|
|
129
|
+
f"Hubs : {len(hubs)}",
|
|
130
|
+
f"Violations: {len(violations)}",
|
|
131
|
+
"",
|
|
132
|
+
]
|
|
133
|
+
if top:
|
|
134
|
+
lines.append("Top-5 PageRank (architektoniczne centra):")
|
|
135
|
+
for name, score in top:
|
|
136
|
+
short = name.split(".")[-1]
|
|
137
|
+
lines.append(f" {score:.4f} {short}")
|
|
138
|
+
if violations:
|
|
139
|
+
lines.append("\nLayer violations:")
|
|
140
|
+
for v in violations[:10]:
|
|
141
|
+
lines.append(f" {v}")
|
|
142
|
+
if cycles:
|
|
143
|
+
lines.append(f"\nCycles (pierwsze {min(5, len(cycles))}):")
|
|
144
|
+
for c in cycles[:5]:
|
|
145
|
+
lines.append(f" {' → '.join(c[:4])}{'...' if len(c) > 4 else ''}")
|
|
146
|
+
return "\n".join(lines)
|
code2schema/cli.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
code2schema.cli
|
|
3
|
+
~~~~~~~~~~~~~~~
|
|
4
|
+
CLI v3 — pełny pipeline.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import sys
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from code2schema.analyzer.cqrs import analyze
|
|
14
|
+
from code2schema.analyzer.events import infer_event_model
|
|
15
|
+
from code2schema.analyzer.graph import (
|
|
16
|
+
build_rich_graph, detect_cycles, graph_summary, write_dot, write_graphml,
|
|
17
|
+
)
|
|
18
|
+
from code2schema.codegen import write_json, write_markdown, write_proto
|
|
19
|
+
from code2schema.core.extractor import extract_project
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main(argv=None):
|
|
23
|
+
parser = argparse.ArgumentParser(prog="code2schema",
|
|
24
|
+
description="Semantic Compiler: Code → CQRS → Schema / Proto / Graph")
|
|
25
|
+
parser.add_argument("path")
|
|
26
|
+
parser.add_argument("-o", "--out", default="schema.json")
|
|
27
|
+
parser.add_argument("--proto", metavar="FILE")
|
|
28
|
+
parser.add_argument("--md", metavar="FILE")
|
|
29
|
+
parser.add_argument("--graphml", metavar="FILE")
|
|
30
|
+
parser.add_argument("--dot", metavar="FILE")
|
|
31
|
+
parser.add_argument("--events", action="store_true")
|
|
32
|
+
parser.add_argument("--cycles", action="store_true")
|
|
33
|
+
parser.add_argument("--graph-summary", action="store_true")
|
|
34
|
+
parser.add_argument("--no-rules", action="store_true")
|
|
35
|
+
parser.add_argument("--exclude", nargs="*", default=[])
|
|
36
|
+
parser.add_argument("-q", "--quiet", action="store_true")
|
|
37
|
+
args = parser.parse_args(argv)
|
|
38
|
+
|
|
39
|
+
root = Path(args.path)
|
|
40
|
+
if not root.exists():
|
|
41
|
+
print(f"ERROR: {root}", file=sys.stderr)
|
|
42
|
+
return 1
|
|
43
|
+
|
|
44
|
+
t0 = time.perf_counter()
|
|
45
|
+
if not args.quiet:
|
|
46
|
+
print(f"⏳ Parsing: {root}")
|
|
47
|
+
|
|
48
|
+
modules = extract_project(root, exclude=args.exclude or None)
|
|
49
|
+
if not modules:
|
|
50
|
+
print("ERROR: Brak plików .py.", file=sys.stderr)
|
|
51
|
+
return 1
|
|
52
|
+
|
|
53
|
+
schema = analyze(modules)
|
|
54
|
+
if args.no_rules:
|
|
55
|
+
schema.rules = []
|
|
56
|
+
|
|
57
|
+
G = build_rich_graph(schema)
|
|
58
|
+
|
|
59
|
+
if args.cycles:
|
|
60
|
+
cycles = detect_cycles(G)
|
|
61
|
+
if cycles:
|
|
62
|
+
print(f"\n⚠️ Cykle ({len(cycles)}):")
|
|
63
|
+
for c in cycles[:5]:
|
|
64
|
+
print(f" {' → '.join(c[:5])}")
|
|
65
|
+
else:
|
|
66
|
+
print("✅ Brak cykli.")
|
|
67
|
+
|
|
68
|
+
if args.graph_summary:
|
|
69
|
+
print("\n── Graph Summary ──────────────────────")
|
|
70
|
+
print(graph_summary(G, schema))
|
|
71
|
+
|
|
72
|
+
if args.events:
|
|
73
|
+
em = infer_event_model(modules)
|
|
74
|
+
print("\n── Event Model ────────────────────────")
|
|
75
|
+
print(em.summary())
|
|
76
|
+
|
|
77
|
+
write_json(schema, Path(args.out))
|
|
78
|
+
if args.proto:
|
|
79
|
+
write_proto(schema, Path(args.proto))
|
|
80
|
+
if args.md:
|
|
81
|
+
write_markdown(schema, Path(args.md))
|
|
82
|
+
if args.graphml:
|
|
83
|
+
write_graphml(G, Path(args.graphml))
|
|
84
|
+
if args.dot:
|
|
85
|
+
write_dot(G, Path(args.dot))
|
|
86
|
+
|
|
87
|
+
if not args.quiet:
|
|
88
|
+
funcs = schema.all_functions()
|
|
89
|
+
print(
|
|
90
|
+
f"\n✅ Gotowe ({time.perf_counter()-t0:.2f}s)\n"
|
|
91
|
+
f" Modules : {len(modules)}\n"
|
|
92
|
+
f" Functions: {len(funcs)}\n"
|
|
93
|
+
f" Queries : {len(schema.queries())}\n"
|
|
94
|
+
f" Commands : {len(schema.commands())}\n"
|
|
95
|
+
f" Orchest. : {len(schema.orchestrators())}\n"
|
|
96
|
+
f" Workflows: {len(schema.workflows)}\n"
|
|
97
|
+
f" Rules : {len(schema.rules)}\n"
|
|
98
|
+
f" Graph : {G.number_of_nodes()}N / {G.number_of_edges()}E\n"
|
|
99
|
+
f" → {args.out}"
|
|
100
|
+
)
|
|
101
|
+
return 0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
raise SystemExit(main())
|