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.
@@ -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())