code2schema 0.1.2__tar.gz → 0.1.6__tar.gz
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-0.1.2 → code2schema-0.1.6}/PKG-INFO +6 -6
- {code2schema-0.1.2 → code2schema-0.1.6}/README.md +5 -5
- {code2schema-0.1.2 → code2schema-0.1.6}/code2schema/__init__.py +9 -3
- {code2schema-0.1.2 → code2schema-0.1.6}/code2schema/analyzer/cqrs.py +38 -28
- {code2schema-0.1.2 → code2schema-0.1.6}/code2schema/analyzer/events.py +49 -36
- {code2schema-0.1.2 → code2schema-0.1.6}/code2schema/analyzer/graph.py +7 -3
- code2schema-0.1.6/code2schema/cli.py +252 -0
- {code2schema-0.1.2 → code2schema-0.1.6}/code2schema/codegen/__init__.py +9 -7
- {code2schema-0.1.2 → code2schema-0.1.6}/code2schema/codegen/visualizer.py +59 -36
- {code2schema-0.1.2 → code2schema-0.1.6}/code2schema/core/extractor.py +49 -12
- {code2schema-0.1.2 → code2schema-0.1.6}/code2schema/core/models.py +11 -3
- {code2schema-0.1.2 → code2schema-0.1.6}/code2schema.egg-info/PKG-INFO +6 -6
- {code2schema-0.1.2 → code2schema-0.1.6}/pyproject.toml +1 -1
- {code2schema-0.1.2 → code2schema-0.1.6}/tests/test_code2schema.py +47 -0
- code2schema-0.1.2/code2schema/cli.py +0 -147
- {code2schema-0.1.2 → code2schema-0.1.6}/LICENSE +0 -0
- {code2schema-0.1.2 → code2schema-0.1.6}/code2schema/analyzer/__init__.py +0 -0
- {code2schema-0.1.2 → code2schema-0.1.6}/code2schema/core/__init__.py +0 -0
- {code2schema-0.1.2 → code2schema-0.1.6}/code2schema.egg-info/SOURCES.txt +0 -0
- {code2schema-0.1.2 → code2schema-0.1.6}/code2schema.egg-info/dependency_links.txt +0 -0
- {code2schema-0.1.2 → code2schema-0.1.6}/code2schema.egg-info/entry_points.txt +0 -0
- {code2schema-0.1.2 → code2schema-0.1.6}/code2schema.egg-info/requires.txt +0 -0
- {code2schema-0.1.2 → code2schema-0.1.6}/code2schema.egg-info/top_level.txt +0 -0
- {code2schema-0.1.2 → code2schema-0.1.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code2schema
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: Semantic compiler: Code → AST → CQRS Model → Workflow DAG → Proto/Schema
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
Keywords: ast,cqrs,static-analysis,code-quality,schema
|
|
@@ -34,13 +34,13 @@ Dynamic: license-file
|
|
|
34
34
|
|
|
35
35
|
## AI Cost Tracking
|
|
36
36
|
|
|
37
|
-
    
|
|
38
|
+
  
|
|
39
39
|
|
|
40
|
-
- 🤖 **LLM usage:** $0.
|
|
41
|
-
- 👤 **Human dev:** ~$
|
|
40
|
+
- 🤖 **LLM usage:** $0.6000 (4 commits)
|
|
41
|
+
- 👤 **Human dev:** ~$361 (3.6h @ $100/h, 30min dedup)
|
|
42
42
|
|
|
43
|
-
Generated on 2026-05-
|
|
43
|
+
Generated on 2026-05-07 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
44
44
|
|
|
45
45
|
---
|
|
46
46
|
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
    
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $0.
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $0.6000 (4 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$361 (3.6h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
|
-
Generated on 2026-05-
|
|
12
|
+
Generated on 2026-05-07 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
"""code2schema — Semantic Compiler for Software Systems."""
|
|
2
|
+
|
|
2
3
|
from code2schema.core.models import SchemaIR, CQRSRole, FunctionIR, ModuleIR
|
|
3
4
|
from code2schema.core.extractor import extract_project, extract_module
|
|
4
5
|
from code2schema.analyzer.cqrs import analyze
|
|
5
6
|
|
|
6
7
|
__all__ = [
|
|
7
|
-
"SchemaIR",
|
|
8
|
-
"
|
|
8
|
+
"SchemaIR",
|
|
9
|
+
"CQRSRole",
|
|
10
|
+
"FunctionIR",
|
|
11
|
+
"ModuleIR",
|
|
12
|
+
"extract_project",
|
|
13
|
+
"extract_module",
|
|
14
|
+
"analyze",
|
|
9
15
|
]
|
|
10
|
-
__version__ = "0.1.
|
|
16
|
+
__version__ = "0.1.6"
|
|
@@ -3,6 +3,7 @@ code2schema.analyzer.cqrs
|
|
|
3
3
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
4
|
CQRS inference + call graph (NetworkX) + reguły jakości.
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
from __future__ import annotations
|
|
7
8
|
|
|
8
9
|
from typing import List
|
|
@@ -22,13 +23,14 @@ from code2schema.core.models import (
|
|
|
22
23
|
|
|
23
24
|
# ── Progi heurystyczne ───────────────────────────────────────────────────────
|
|
24
25
|
|
|
25
|
-
FAN_OUT_ORCHESTRATOR = 5
|
|
26
|
-
FAN_OUT_HIGH = 10
|
|
27
|
-
CC_LIMIT = 15
|
|
26
|
+
FAN_OUT_ORCHESTRATOR = 5 # >= N wywołań → orchestrator
|
|
27
|
+
FAN_OUT_HIGH = 10 # alert reguły
|
|
28
|
+
CC_LIMIT = 15 # (placeholder, liczony zewnętrznie)
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
# ── CQRS Inference ───────────────────────────────────────────────────────────
|
|
31
32
|
|
|
33
|
+
|
|
32
34
|
def _infer_role(func: FunctionIR) -> CQRSRole:
|
|
33
35
|
"""Klasyfikuje funkcję na podstawie side-effectów i fan-outu."""
|
|
34
36
|
has_side_effects = SideEffect.NONE not in func.side_effects
|
|
@@ -48,6 +50,7 @@ def _infer_role(func: FunctionIR) -> CQRSRole:
|
|
|
48
50
|
|
|
49
51
|
# ── Call Graph ───────────────────────────────────────────────────────────────
|
|
50
52
|
|
|
53
|
+
|
|
51
54
|
def build_call_graph(modules: List[ModuleIR]) -> nx.DiGraph:
|
|
52
55
|
"""Buduje skierowany graf wywołań między funkcjami."""
|
|
53
56
|
G = nx.DiGraph()
|
|
@@ -79,6 +82,7 @@ def centrality(G: nx.DiGraph) -> dict[str, float]:
|
|
|
79
82
|
|
|
80
83
|
# ── Workflow DAG ──────────────────────────────────────────────────────────────
|
|
81
84
|
|
|
85
|
+
|
|
82
86
|
def build_workflows(modules: List[ModuleIR]) -> List[WorkflowIR]:
|
|
83
87
|
"""Buduje DAG wykonania dla każdego orkiestratora."""
|
|
84
88
|
workflows: list[WorkflowIR] = []
|
|
@@ -89,8 +93,7 @@ def build_workflows(modules: List[ModuleIR]) -> List[WorkflowIR]:
|
|
|
89
93
|
name=f"workflow_{f.name}",
|
|
90
94
|
entry=f.qualified_name,
|
|
91
95
|
steps=[
|
|
92
|
-
WorkflowStep(callee=c, is_async=f.is_async)
|
|
93
|
-
for c in f.calls
|
|
96
|
+
WorkflowStep(callee=c, is_async=f.is_async) for c in f.calls
|
|
94
97
|
],
|
|
95
98
|
)
|
|
96
99
|
workflows.append(workflow)
|
|
@@ -99,6 +102,7 @@ def build_workflows(modules: List[ModuleIR]) -> List[WorkflowIR]:
|
|
|
99
102
|
|
|
100
103
|
# ── Rules ────────────────────────────────────────────────────────────────────
|
|
101
104
|
|
|
105
|
+
|
|
102
106
|
def generate_rules(modules: List[ModuleIR]) -> List[RuleIR]:
|
|
103
107
|
"""Generuje heurystyczne reguły jakości na podstawie IR."""
|
|
104
108
|
rules: list[RuleIR] = []
|
|
@@ -106,36 +110,42 @@ def generate_rules(modules: List[ModuleIR]) -> List[RuleIR]:
|
|
|
106
110
|
for mod in modules:
|
|
107
111
|
for f in mod.functions:
|
|
108
112
|
if f.fan_out >= FAN_OUT_HIGH:
|
|
109
|
-
rules.append(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
rules.append(
|
|
114
|
+
RuleIR(
|
|
115
|
+
id="HIGH_FAN_OUT",
|
|
116
|
+
target=f.qualified_name,
|
|
117
|
+
condition=f"fan_out={f.fan_out} >= {FAN_OUT_HIGH}",
|
|
118
|
+
action="refactor_to_service",
|
|
119
|
+
severity="error",
|
|
120
|
+
)
|
|
121
|
+
)
|
|
116
122
|
if f.lines > 100:
|
|
117
|
-
rules.append(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
123
|
+
rules.append(
|
|
124
|
+
RuleIR(
|
|
125
|
+
id="LONG_FUNCTION",
|
|
126
|
+
target=f.qualified_name,
|
|
127
|
+
condition=f"lines={f.lines} > 100",
|
|
128
|
+
action="split_function",
|
|
129
|
+
severity="warning",
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
if f.role == CQRSRole.QUERY and SideEffect.NONE not in f.side_effects:
|
|
133
|
+
rules.append(
|
|
134
|
+
RuleIR(
|
|
135
|
+
id="QUERY_WITH_SIDE_EFFECTS",
|
|
136
|
+
target=f.qualified_name,
|
|
137
|
+
condition="role=query but has side effects",
|
|
138
|
+
action="separate_command_from_query",
|
|
139
|
+
severity="warning",
|
|
140
|
+
)
|
|
141
|
+
)
|
|
133
142
|
|
|
134
143
|
return rules
|
|
135
144
|
|
|
136
145
|
|
|
137
146
|
# ── Main entry ────────────────────────────────────────────────────────────────
|
|
138
147
|
|
|
148
|
+
|
|
139
149
|
def analyze(modules: List[ModuleIR]) -> SchemaIR:
|
|
140
150
|
"""Pełna analiza: CQRS + graf + workflow + reguły → SchemaIR."""
|
|
141
151
|
|
|
@@ -6,13 +6,14 @@ Inferencja modelu zdarzeń (DDD / Event Sourcing):
|
|
|
6
6
|
- klasyfikuje: Command Handler → Event → Event Handler
|
|
7
7
|
- buduje Event Flow Map
|
|
8
8
|
"""
|
|
9
|
+
|
|
9
10
|
from __future__ import annotations
|
|
10
11
|
|
|
11
12
|
import re
|
|
12
13
|
from dataclasses import dataclass, field
|
|
13
14
|
from typing import List
|
|
14
15
|
|
|
15
|
-
from code2schema.core.models import CQRSRole,
|
|
16
|
+
from code2schema.core.models import CQRSRole, ModuleIR
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
# ── Heurystyki nazw ───────────────────────────────────────────────────────────
|
|
@@ -33,15 +34,15 @@ _AGGREGATE_PATTERNS = re.compile(
|
|
|
33
34
|
|
|
34
35
|
@dataclass
|
|
35
36
|
class DomainEvent:
|
|
36
|
-
name: str
|
|
37
|
-
emitted_by: str
|
|
37
|
+
name: str # np. "UserCreated"
|
|
38
|
+
emitted_by: str # qualified_name funkcji emitującej
|
|
38
39
|
handled_by: List[str] = field(default_factory=list)
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
@dataclass
|
|
42
43
|
class CommandHandler:
|
|
43
44
|
name: str
|
|
44
|
-
command: str
|
|
45
|
+
command: str # nazwa wywołania, które traktujemy jako "command"
|
|
45
46
|
emits: List[str] = field(default_factory=list)
|
|
46
47
|
|
|
47
48
|
|
|
@@ -61,7 +62,9 @@ class EventModel:
|
|
|
61
62
|
lines.append("\nDomain Events:")
|
|
62
63
|
for ev in self.events[:15]:
|
|
63
64
|
handlers = ", ".join(ev.handled_by[:3]) or "—"
|
|
64
|
-
lines.append(
|
|
65
|
+
lines.append(
|
|
66
|
+
f" {ev.name:30s} ← {ev.emitted_by.split('.')[-1]:25s} → [{handlers}]"
|
|
67
|
+
)
|
|
65
68
|
if self.commands:
|
|
66
69
|
lines.append("\nCommand Handlers (top 10):")
|
|
67
70
|
for cmd in self.commands[:10]:
|
|
@@ -72,39 +75,34 @@ class EventModel:
|
|
|
72
75
|
|
|
73
76
|
# ── Inference ─────────────────────────────────────────────────────────────────
|
|
74
77
|
|
|
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
78
|
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
def _find_emitters(modules: List[ModuleIR]) -> List[DomainEvent]:
|
|
80
|
+
"""Krok 1: znajdź funkcje emitujące zdarzenia."""
|
|
81
|
+
events: List[DomainEvent] = []
|
|
87
82
|
for mod in modules:
|
|
88
83
|
for f in mod.functions:
|
|
89
84
|
if _EVENT_EMIT_PATTERNS.search(f.name) or any(
|
|
90
85
|
_EVENT_EMIT_PATTERNS.search(c) for c in f.calls
|
|
91
86
|
):
|
|
92
87
|
event_name = _derive_event_name(f.name)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
model.events.append(ev)
|
|
88
|
+
events.append(DomainEvent(name=event_name, emitted_by=f.qualified_name))
|
|
89
|
+
return events
|
|
96
90
|
|
|
97
|
-
|
|
91
|
+
|
|
92
|
+
def _find_handlers(events: List[DomainEvent], modules: List[ModuleIR]) -> None:
|
|
93
|
+
"""Krok 2: dopasuj handlery do zdarzeń (mutuje events in-place)."""
|
|
98
94
|
for mod in modules:
|
|
99
95
|
for f in mod.functions:
|
|
100
96
|
if _EVENT_HANDLER_PATTERNS.search(f.name):
|
|
101
|
-
|
|
102
|
-
for ev in model.events:
|
|
97
|
+
for ev in events:
|
|
103
98
|
keyword = ev.name.lower().replace("event", "").strip()
|
|
104
99
|
if keyword and keyword in f.name.lower():
|
|
105
100
|
ev.handled_by.append(f.qualified_name)
|
|
106
101
|
|
|
107
|
-
|
|
102
|
+
|
|
103
|
+
def _find_command_handlers(modules: List[ModuleIR]) -> List[CommandHandler]:
|
|
104
|
+
"""Krok 3: znajdź command handlery i ich emitowane zdarzenia."""
|
|
105
|
+
commands: List[CommandHandler] = []
|
|
108
106
|
for mod in modules:
|
|
109
107
|
for f in mod.functions:
|
|
110
108
|
if f.role == CQRSRole.COMMAND or _AGGREGATE_PATTERNS.search(f.name):
|
|
@@ -114,24 +112,33 @@ def infer_event_model(modules: List[ModuleIR]) -> EventModel:
|
|
|
114
112
|
if _EVENT_EMIT_PATTERNS.search(c)
|
|
115
113
|
]
|
|
116
114
|
if emits or f.role == CQRSRole.COMMAND:
|
|
117
|
-
|
|
115
|
+
commands.append(
|
|
118
116
|
CommandHandler(
|
|
119
117
|
name=f.qualified_name,
|
|
120
118
|
command=f.name,
|
|
121
119
|
emits=emits,
|
|
122
120
|
)
|
|
123
121
|
)
|
|
122
|
+
return commands
|
|
123
|
+
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
def _find_aggregates(modules: List[ModuleIR]) -> List[str]:
|
|
126
|
+
"""Krok 4: znajdź moduły będące agregatami (CRUD score >= 2)."""
|
|
127
|
+
aggregates: List[str] = []
|
|
126
128
|
for mod in modules:
|
|
127
|
-
crud_score = sum(
|
|
128
|
-
1 for f in mod.functions
|
|
129
|
-
if _AGGREGATE_PATTERNS.search(f.name)
|
|
130
|
-
)
|
|
129
|
+
crud_score = sum(1 for f in mod.functions if _AGGREGATE_PATTERNS.search(f.name))
|
|
131
130
|
if crud_score >= 2:
|
|
132
|
-
|
|
131
|
+
aggregates.append(mod.name)
|
|
132
|
+
return aggregates
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
|
|
135
|
+
def infer_event_model(modules: List[ModuleIR]) -> EventModel:
|
|
136
|
+
"""Przechodzi po IR i buduje model zdarzeń."""
|
|
137
|
+
events = _find_emitters(modules)
|
|
138
|
+
_find_handlers(events, modules)
|
|
139
|
+
commands = _find_command_handlers(modules)
|
|
140
|
+
aggregates = _find_aggregates(modules)
|
|
141
|
+
return EventModel(commands=commands, events=events, aggregates=aggregates)
|
|
135
142
|
|
|
136
143
|
|
|
137
144
|
def _derive_event_name(func_name: str) -> str:
|
|
@@ -139,7 +146,7 @@ def _derive_event_name(func_name: str) -> str:
|
|
|
139
146
|
# Usuń prefiks czasownikowy
|
|
140
147
|
for prefix in ("on_", "handle_", "emit_", "publish_", "dispatch_", "fire_"):
|
|
141
148
|
if func_name.startswith(prefix):
|
|
142
|
-
func_name = func_name[len(prefix):]
|
|
149
|
+
func_name = func_name[len(prefix) :]
|
|
143
150
|
break
|
|
144
151
|
|
|
145
152
|
parts = func_name.split("_")
|
|
@@ -154,9 +161,15 @@ def _derive_event_name(func_name: str) -> str:
|
|
|
154
161
|
|
|
155
162
|
def _past_tense(verb: str) -> str:
|
|
156
163
|
_map = {
|
|
157
|
-
"create": "Created",
|
|
158
|
-
"
|
|
159
|
-
"
|
|
160
|
-
"
|
|
164
|
+
"create": "Created",
|
|
165
|
+
"update": "Updated",
|
|
166
|
+
"delete": "Deleted",
|
|
167
|
+
"save": "Saved",
|
|
168
|
+
"send": "Sent",
|
|
169
|
+
"emit": "Emitted",
|
|
170
|
+
"publish": "Published",
|
|
171
|
+
"register": "Registered",
|
|
172
|
+
"process": "Processed",
|
|
173
|
+
"notify": "Notified",
|
|
161
174
|
}
|
|
162
175
|
return _map.get(verb.lower(), verb.capitalize() + "d")
|
|
@@ -7,18 +7,20 @@ Zaawansowana analiza grafu wywołań:
|
|
|
7
7
|
- detekcja hubów i cykli
|
|
8
8
|
- metryki warstw (layered architecture check)
|
|
9
9
|
"""
|
|
10
|
+
|
|
10
11
|
from __future__ import annotations
|
|
11
12
|
|
|
12
13
|
from pathlib import Path
|
|
13
|
-
from typing import
|
|
14
|
+
from typing import List, Tuple
|
|
14
15
|
|
|
15
16
|
import networkx as nx
|
|
16
17
|
|
|
17
|
-
from code2schema.core.models import CQRSRole,
|
|
18
|
+
from code2schema.core.models import CQRSRole, SchemaIR
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
# ── Builder ───────────────────────────────────────────────────────────────────
|
|
21
22
|
|
|
23
|
+
|
|
22
24
|
def build_rich_graph(schema: SchemaIR) -> nx.DiGraph:
|
|
23
25
|
"""Buduje pełny graf z atrybutami węzłów (rola, moduł, fan-out)."""
|
|
24
26
|
G = nx.DiGraph()
|
|
@@ -50,6 +52,7 @@ def build_rich_graph(schema: SchemaIR) -> nx.DiGraph:
|
|
|
50
52
|
|
|
51
53
|
# ── Metrics ───────────────────────────────────────────────────────────────────
|
|
52
54
|
|
|
55
|
+
|
|
53
56
|
def centrality_report(G: nx.DiGraph, top_n: int = 10) -> List[Tuple[str, float]]:
|
|
54
57
|
"""PageRank — funkcje najważniejsze architektonicznie."""
|
|
55
58
|
if G.number_of_edges() == 0:
|
|
@@ -91,6 +94,7 @@ def layer_violations(schema: SchemaIR) -> List[str]:
|
|
|
91
94
|
|
|
92
95
|
# ── Export ────────────────────────────────────────────────────────────────────
|
|
93
96
|
|
|
97
|
+
|
|
94
98
|
def write_graphml(G: nx.DiGraph, path: Path) -> None:
|
|
95
99
|
"""Eksport do GraphML — kompatybilny z Gephi / yEd / Neo4j importer."""
|
|
96
100
|
nx.write_graphml(G, str(path))
|
|
@@ -98,7 +102,7 @@ def write_graphml(G: nx.DiGraph, path: Path) -> None:
|
|
|
98
102
|
|
|
99
103
|
def write_dot(G: nx.DiGraph, path: Path) -> None:
|
|
100
104
|
"""Eksport do DOT — renderowany przez Graphviz."""
|
|
101
|
-
lines = ["digraph CallGraph {",
|
|
105
|
+
lines = ["digraph CallGraph {", " rankdir=LR;", " node [shape=box];", ""]
|
|
102
106
|
for n, data in G.nodes(data=True):
|
|
103
107
|
role = data.get("role", "unknown")
|
|
104
108
|
color = {
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""
|
|
2
|
+
code2schema.cli
|
|
3
|
+
~~~~~~~~~~~~~~~
|
|
4
|
+
CLI v3 — pełny pipeline.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from code2schema.analyzer.cqrs import analyze
|
|
15
|
+
from code2schema.analyzer.events import infer_event_model
|
|
16
|
+
from code2schema.analyzer.graph import (
|
|
17
|
+
build_rich_graph,
|
|
18
|
+
detect_cycles,
|
|
19
|
+
graph_summary,
|
|
20
|
+
write_dot,
|
|
21
|
+
write_graphml,
|
|
22
|
+
)
|
|
23
|
+
from code2schema.codegen import write_json, write_markdown, write_proto
|
|
24
|
+
from code2schema.codegen.visualizer import write_html
|
|
25
|
+
from code2schema.core.extractor import extract_project
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_SPECIAL_PROJECT_DIRS = {"backend", "frontend", "src", "app", "api"}
|
|
29
|
+
_GENERIC_PATH_PARTS = {"home", "github", "workspace", "projects", "src"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _project_name_from_path(path: Path) -> str:
|
|
33
|
+
"""Extract project name from path (e.g., /path/to/c2004/backend -> c2004)."""
|
|
34
|
+
# Try to find a meaningful name from the path
|
|
35
|
+
parts = path.parts
|
|
36
|
+
for i, part in enumerate(reversed(parts)):
|
|
37
|
+
if part in _SPECIAL_PROJECT_DIRS:
|
|
38
|
+
# Use parent directory name
|
|
39
|
+
if i < len(parts) - 1:
|
|
40
|
+
return parts[-(i + 2)]
|
|
41
|
+
# Skip common generic names
|
|
42
|
+
if part not in _GENERIC_PATH_PARTS:
|
|
43
|
+
return part
|
|
44
|
+
return path.name or "project"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
48
|
+
"""Build CLI argument parser."""
|
|
49
|
+
from code2schema import __version__
|
|
50
|
+
|
|
51
|
+
parser = argparse.ArgumentParser(
|
|
52
|
+
prog="code2schema",
|
|
53
|
+
description="Semantic Compiler: Code → CQRS → Schema / Proto / Graph",
|
|
54
|
+
)
|
|
55
|
+
parser.add_argument("path")
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"-o",
|
|
58
|
+
"--out",
|
|
59
|
+
metavar="FILE",
|
|
60
|
+
help="Output JSON schema (default: <project>_schema.json)",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--proto",
|
|
64
|
+
metavar="FILE",
|
|
65
|
+
nargs="?",
|
|
66
|
+
const=True,
|
|
67
|
+
default=None,
|
|
68
|
+
help="Output .proto file (default: <project>_api.proto if flag present)",
|
|
69
|
+
)
|
|
70
|
+
parser.add_argument(
|
|
71
|
+
"--md",
|
|
72
|
+
metavar="FILE",
|
|
73
|
+
nargs="?",
|
|
74
|
+
const=True,
|
|
75
|
+
default=None,
|
|
76
|
+
help="Output Markdown report (default: <project>_report.md if flag present)",
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument("--graphml", metavar="FILE")
|
|
79
|
+
parser.add_argument("--dot", metavar="FILE")
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"--html",
|
|
82
|
+
metavar="FILE",
|
|
83
|
+
nargs="?",
|
|
84
|
+
const=True,
|
|
85
|
+
default=None,
|
|
86
|
+
help="Interactive HTML visualization (default: <project>_viz.html if flag present)",
|
|
87
|
+
)
|
|
88
|
+
parser.add_argument("--events", action="store_true")
|
|
89
|
+
parser.add_argument("--cycles", action="store_true")
|
|
90
|
+
parser.add_argument("--graph-summary", action="store_true")
|
|
91
|
+
parser.add_argument("--no-rules", action="store_true")
|
|
92
|
+
parser.add_argument("--exclude", nargs="*", default=[])
|
|
93
|
+
parser.add_argument("-q", "--quiet", action="store_true")
|
|
94
|
+
parser.add_argument(
|
|
95
|
+
"-V", "--version", action="version", version=f"%(prog)s {__version__}"
|
|
96
|
+
)
|
|
97
|
+
return parser
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _resolve_output_dir(root: Path) -> Path:
|
|
101
|
+
if not root.is_dir():
|
|
102
|
+
return root.parent
|
|
103
|
+
if root.name in _SPECIAL_PROJECT_DIRS:
|
|
104
|
+
return root.parent
|
|
105
|
+
return root
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _resolve_required_path(out_dir: Path, value: str | None, default_name: str) -> Path:
|
|
109
|
+
return out_dir / (value or default_name)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _resolve_optional_path(
|
|
113
|
+
out_dir: Path,
|
|
114
|
+
value: str | bool | None,
|
|
115
|
+
default_name: str,
|
|
116
|
+
) -> Path | None:
|
|
117
|
+
if isinstance(value, str):
|
|
118
|
+
return out_dir / value
|
|
119
|
+
if value is True:
|
|
120
|
+
return out_dir / default_name
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _validate_root(args: argparse.Namespace) -> Path:
|
|
125
|
+
"""Return ``Path`` for the analysis root from CLI args."""
|
|
126
|
+
return Path(args.path)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _build_output_paths(
|
|
130
|
+
root: Path, args: argparse.Namespace
|
|
131
|
+
) -> tuple[Path, Path | None, Path | None, Path | None]:
|
|
132
|
+
"""Build output file paths (JSON / proto / md / html) for the resolved root."""
|
|
133
|
+
proj_name = _project_name_from_path(root)
|
|
134
|
+
out_dir = _resolve_output_dir(root)
|
|
135
|
+
|
|
136
|
+
out_path = _resolve_required_path(out_dir, args.out, f"{proj_name}_schema.json")
|
|
137
|
+
proto_path = _resolve_optional_path(out_dir, args.proto, f"{proj_name}_api.proto")
|
|
138
|
+
md_path = _resolve_optional_path(out_dir, args.md, f"{proj_name}_report.md")
|
|
139
|
+
html_path = _resolve_optional_path(out_dir, args.html, f"{proj_name}_viz.html")
|
|
140
|
+
return out_path, proto_path, md_path, html_path
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _resolve_paths(
|
|
144
|
+
args: argparse.Namespace,
|
|
145
|
+
) -> tuple[Path, Path, Path | None, Path | None, Path | None]:
|
|
146
|
+
"""Resolve root and all output file paths from CLI args."""
|
|
147
|
+
root = _validate_root(args)
|
|
148
|
+
out_path, proto_path, md_path, html_path = _build_output_paths(root, args)
|
|
149
|
+
return root, out_path, proto_path, md_path, html_path
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _run_extraction(args, root):
|
|
153
|
+
"""Parse project and run CQRS analysis. Returns (modules, schema, G) or None on failure."""
|
|
154
|
+
modules = extract_project(root, exclude=args.exclude or None)
|
|
155
|
+
if not modules:
|
|
156
|
+
print("ERROR: Brak plików .py.", file=sys.stderr)
|
|
157
|
+
return None
|
|
158
|
+
schema = analyze(modules)
|
|
159
|
+
if args.no_rules:
|
|
160
|
+
schema.rules = []
|
|
161
|
+
G = build_rich_graph(schema)
|
|
162
|
+
return modules, schema, G
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _run_reports(args, modules, schema, G):
|
|
166
|
+
"""Print optional reports: cycles, graph summary, events."""
|
|
167
|
+
if args.cycles:
|
|
168
|
+
cycles = detect_cycles(G)
|
|
169
|
+
if cycles:
|
|
170
|
+
print(f"\n⚠️ Cykle ({len(cycles)}):")
|
|
171
|
+
for c in cycles[:5]:
|
|
172
|
+
print(f" {' → '.join(c[:5])}")
|
|
173
|
+
else:
|
|
174
|
+
print("✅ Brak cykli.")
|
|
175
|
+
|
|
176
|
+
if args.graph_summary:
|
|
177
|
+
print("\n── Graph Summary ──────────────────────")
|
|
178
|
+
print(graph_summary(G, schema))
|
|
179
|
+
|
|
180
|
+
if args.events:
|
|
181
|
+
em = infer_event_model(modules)
|
|
182
|
+
print("\n── Event Model ────────────────────────")
|
|
183
|
+
print(em.summary())
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _write_outputs(args, schema, G, out_path, proto_path, md_path, html_path):
|
|
187
|
+
"""Write all output files (JSON, proto, md, html, graphml, dot)."""
|
|
188
|
+
write_json(schema, out_path)
|
|
189
|
+
if proto_path:
|
|
190
|
+
write_proto(schema, proto_path)
|
|
191
|
+
if md_path:
|
|
192
|
+
write_markdown(schema, md_path)
|
|
193
|
+
if html_path:
|
|
194
|
+
write_html(schema, html_path)
|
|
195
|
+
if args.graphml:
|
|
196
|
+
write_graphml(G, Path(args.graphml))
|
|
197
|
+
if args.dot:
|
|
198
|
+
write_dot(G, Path(args.dot))
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _print_summary(modules, schema, G, t0, out_path, proto_path, md_path, html_path):
|
|
202
|
+
"""Print final summary with metrics and output file names."""
|
|
203
|
+
funcs = schema.all_functions()
|
|
204
|
+
outputs = [f" → {out_path.name}"]
|
|
205
|
+
if proto_path:
|
|
206
|
+
outputs.append(f" → {proto_path.name}")
|
|
207
|
+
if md_path:
|
|
208
|
+
outputs.append(f" → {md_path.name}")
|
|
209
|
+
if html_path:
|
|
210
|
+
outputs.append(f" → {html_path.name}")
|
|
211
|
+
print(
|
|
212
|
+
f"\n✅ Gotowe ({time.perf_counter()-t0:.2f}s)\n"
|
|
213
|
+
f" Modules : {len(modules)}\n"
|
|
214
|
+
f" Functions: {len(funcs)}\n"
|
|
215
|
+
f" Queries : {len(schema.queries())}\n"
|
|
216
|
+
f" Commands : {len(schema.commands())}\n"
|
|
217
|
+
f" Orchest. : {len(schema.orchestrators())}\n"
|
|
218
|
+
f" Workflows: {len(schema.workflows)}\n"
|
|
219
|
+
f" Rules : {len(schema.rules)}\n"
|
|
220
|
+
f" Graph : {G.number_of_nodes()}N / {G.number_of_edges()}E\n"
|
|
221
|
+
+ "\n".join(outputs)
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def main(argv=None):
|
|
226
|
+
parser = _build_parser()
|
|
227
|
+
args = parser.parse_args(argv)
|
|
228
|
+
|
|
229
|
+
root, out_path, proto_path, md_path, html_path = _resolve_paths(args)
|
|
230
|
+
if not root.exists():
|
|
231
|
+
print(f"ERROR: {root}", file=sys.stderr)
|
|
232
|
+
return 1
|
|
233
|
+
|
|
234
|
+
t0 = time.perf_counter()
|
|
235
|
+
if not args.quiet:
|
|
236
|
+
print(f"⏳ Parsing: {root}")
|
|
237
|
+
|
|
238
|
+
result = _run_extraction(args, root)
|
|
239
|
+
if result is None:
|
|
240
|
+
return 1
|
|
241
|
+
modules, schema, G = result
|
|
242
|
+
|
|
243
|
+
_run_reports(args, modules, schema, G)
|
|
244
|
+
_write_outputs(args, schema, G, out_path, proto_path, md_path, html_path)
|
|
245
|
+
|
|
246
|
+
if not args.quiet:
|
|
247
|
+
_print_summary(modules, schema, G, t0, out_path, proto_path, md_path, html_path)
|
|
248
|
+
return 0
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
if __name__ == "__main__":
|
|
252
|
+
raise SystemExit(main())
|