code2schema 0.1.4__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.4 → code2schema-0.1.6}/PKG-INFO +5 -5
- {code2schema-0.1.4 → code2schema-0.1.6}/README.md +4 -4
- {code2schema-0.1.4 → code2schema-0.1.6}/code2schema/__init__.py +9 -3
- {code2schema-0.1.4 → code2schema-0.1.6}/code2schema/analyzer/cqrs.py +38 -28
- {code2schema-0.1.4 → code2schema-0.1.6}/code2schema/analyzer/events.py +21 -14
- {code2schema-0.1.4 → code2schema-0.1.6}/code2schema/analyzer/graph.py +7 -3
- {code2schema-0.1.4 → code2schema-0.1.6}/code2schema/cli.py +62 -17
- {code2schema-0.1.4 → code2schema-0.1.6}/code2schema/codegen/__init__.py +9 -7
- {code2schema-0.1.4 → code2schema-0.1.6}/code2schema/codegen/visualizer.py +59 -36
- {code2schema-0.1.4 → code2schema-0.1.6}/code2schema/core/extractor.py +49 -12
- {code2schema-0.1.4 → code2schema-0.1.6}/code2schema/core/models.py +11 -3
- {code2schema-0.1.4 → code2schema-0.1.6}/code2schema.egg-info/PKG-INFO +5 -5
- {code2schema-0.1.4 → code2schema-0.1.6}/pyproject.toml +1 -1
- {code2schema-0.1.4 → code2schema-0.1.6}/LICENSE +0 -0
- {code2schema-0.1.4 → code2schema-0.1.6}/code2schema/analyzer/__init__.py +0 -0
- {code2schema-0.1.4 → code2schema-0.1.6}/code2schema/core/__init__.py +0 -0
- {code2schema-0.1.4 → code2schema-0.1.6}/code2schema.egg-info/SOURCES.txt +0 -0
- {code2schema-0.1.4 → code2schema-0.1.6}/code2schema.egg-info/dependency_links.txt +0 -0
- {code2schema-0.1.4 → code2schema-0.1.6}/code2schema.egg-info/entry_points.txt +0 -0
- {code2schema-0.1.4 → code2schema-0.1.6}/code2schema.egg-info/requires.txt +0 -0
- {code2schema-0.1.4 → code2schema-0.1.6}/code2schema.egg-info/top_level.txt +0 -0
- {code2schema-0.1.4 → code2schema-0.1.6}/setup.cfg +0 -0
- {code2schema-0.1.4 → code2schema-0.1.6}/tests/test_code2schema.py +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,11 +34,11 @@ 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
43
|
Generated on 2026-05-07 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
44
44
|
|
|
@@ -3,11 +3,11 @@
|
|
|
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
12
|
Generated on 2026-05-07 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
13
13
|
|
|
@@ -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,6 +75,7 @@ class EventModel:
|
|
|
72
75
|
|
|
73
76
|
# ── Inference ─────────────────────────────────────────────────────────────────
|
|
74
77
|
|
|
78
|
+
|
|
75
79
|
def _find_emitters(modules: List[ModuleIR]) -> List[DomainEvent]:
|
|
76
80
|
"""Krok 1: znajdź funkcje emitujące zdarzenia."""
|
|
77
81
|
events: List[DomainEvent] = []
|
|
@@ -122,10 +126,7 @@ def _find_aggregates(modules: List[ModuleIR]) -> List[str]:
|
|
|
122
126
|
"""Krok 4: znajdź moduły będące agregatami (CRUD score >= 2)."""
|
|
123
127
|
aggregates: List[str] = []
|
|
124
128
|
for mod in modules:
|
|
125
|
-
crud_score = sum(
|
|
126
|
-
1 for f in mod.functions
|
|
127
|
-
if _AGGREGATE_PATTERNS.search(f.name)
|
|
128
|
-
)
|
|
129
|
+
crud_score = sum(1 for f in mod.functions if _AGGREGATE_PATTERNS.search(f.name))
|
|
129
130
|
if crud_score >= 2:
|
|
130
131
|
aggregates.append(mod.name)
|
|
131
132
|
return aggregates
|
|
@@ -145,7 +146,7 @@ def _derive_event_name(func_name: str) -> str:
|
|
|
145
146
|
# Usuń prefiks czasownikowy
|
|
146
147
|
for prefix in ("on_", "handle_", "emit_", "publish_", "dispatch_", "fire_"):
|
|
147
148
|
if func_name.startswith(prefix):
|
|
148
|
-
func_name = func_name[len(prefix):]
|
|
149
|
+
func_name = func_name[len(prefix) :]
|
|
149
150
|
break
|
|
150
151
|
|
|
151
152
|
parts = func_name.split("_")
|
|
@@ -160,9 +161,15 @@ def _derive_event_name(func_name: str) -> str:
|
|
|
160
161
|
|
|
161
162
|
def _past_tense(verb: str) -> str:
|
|
162
163
|
_map = {
|
|
163
|
-
"create": "Created",
|
|
164
|
-
"
|
|
165
|
-
"
|
|
166
|
-
"
|
|
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",
|
|
167
174
|
}
|
|
168
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 = {
|
|
@@ -3,6 +3,7 @@ code2schema.cli
|
|
|
3
3
|
~~~~~~~~~~~~~~~
|
|
4
4
|
CLI v3 — pełny pipeline.
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
from __future__ import annotations
|
|
7
8
|
|
|
8
9
|
import argparse
|
|
@@ -13,7 +14,11 @@ from pathlib import Path
|
|
|
13
14
|
from code2schema.analyzer.cqrs import analyze
|
|
14
15
|
from code2schema.analyzer.events import infer_event_model
|
|
15
16
|
from code2schema.analyzer.graph import (
|
|
16
|
-
build_rich_graph,
|
|
17
|
+
build_rich_graph,
|
|
18
|
+
detect_cycles,
|
|
19
|
+
graph_summary,
|
|
20
|
+
write_dot,
|
|
21
|
+
write_graphml,
|
|
17
22
|
)
|
|
18
23
|
from code2schema.codegen import write_json, write_markdown, write_proto
|
|
19
24
|
from code2schema.codegen.visualizer import write_html
|
|
@@ -42,26 +47,53 @@ def _project_name_from_path(path: Path) -> str:
|
|
|
42
47
|
def _build_parser() -> argparse.ArgumentParser:
|
|
43
48
|
"""Build CLI argument parser."""
|
|
44
49
|
from code2schema import __version__
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
|
|
51
|
+
parser = argparse.ArgumentParser(
|
|
52
|
+
prog="code2schema",
|
|
53
|
+
description="Semantic Compiler: Code → CQRS → Schema / Proto / Graph",
|
|
54
|
+
)
|
|
47
55
|
parser.add_argument("path")
|
|
48
|
-
parser.add_argument(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
+
)
|
|
53
78
|
parser.add_argument("--graphml", metavar="FILE")
|
|
54
79
|
parser.add_argument("--dot", metavar="FILE")
|
|
55
|
-
parser.add_argument(
|
|
56
|
-
|
|
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
|
+
)
|
|
57
88
|
parser.add_argument("--events", action="store_true")
|
|
58
89
|
parser.add_argument("--cycles", action="store_true")
|
|
59
90
|
parser.add_argument("--graph-summary", action="store_true")
|
|
60
91
|
parser.add_argument("--no-rules", action="store_true")
|
|
61
92
|
parser.add_argument("--exclude", nargs="*", default=[])
|
|
62
93
|
parser.add_argument("-q", "--quiet", action="store_true")
|
|
63
|
-
parser.add_argument(
|
|
64
|
-
|
|
94
|
+
parser.add_argument(
|
|
95
|
+
"-V", "--version", action="version", version=f"%(prog)s {__version__}"
|
|
96
|
+
)
|
|
65
97
|
return parser
|
|
66
98
|
|
|
67
99
|
|
|
@@ -89,11 +121,15 @@ def _resolve_optional_path(
|
|
|
89
121
|
return None
|
|
90
122
|
|
|
91
123
|
|
|
92
|
-
def
|
|
93
|
-
args
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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."""
|
|
97
133
|
proj_name = _project_name_from_path(root)
|
|
98
134
|
out_dir = _resolve_output_dir(root)
|
|
99
135
|
|
|
@@ -101,6 +137,15 @@ def _resolve_paths(
|
|
|
101
137
|
proto_path = _resolve_optional_path(out_dir, args.proto, f"{proj_name}_api.proto")
|
|
102
138
|
md_path = _resolve_optional_path(out_dir, args.md, f"{proj_name}_report.md")
|
|
103
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)
|
|
104
149
|
return root, out_path, proto_path, md_path, html_path
|
|
105
150
|
|
|
106
151
|
|
|
@@ -6,17 +6,18 @@ Generatory wyjściowe:
|
|
|
6
6
|
- Proto (gRPC .proto)
|
|
7
7
|
- Markdown summary
|
|
8
8
|
"""
|
|
9
|
+
|
|
9
10
|
from __future__ import annotations
|
|
10
11
|
|
|
11
12
|
import json
|
|
12
13
|
from pathlib import Path
|
|
13
|
-
from typing import Literal
|
|
14
14
|
|
|
15
15
|
from code2schema.core.models import CQRSRole, SchemaIR
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
# ── JSON ──────────────────────────────────────────────────────────────────────
|
|
19
19
|
|
|
20
|
+
|
|
20
21
|
def to_json(schema: SchemaIR, indent: int = 2) -> str:
|
|
21
22
|
"""Serializuje SchemaIR do JSON."""
|
|
22
23
|
return json.dumps(schema.model_dump(), indent=indent, default=str)
|
|
@@ -28,12 +29,13 @@ def write_json(schema: SchemaIR, path: Path) -> None:
|
|
|
28
29
|
|
|
29
30
|
# ── Proto ─────────────────────────────────────────────────────────────────────
|
|
30
31
|
|
|
32
|
+
|
|
31
33
|
def to_proto(schema: SchemaIR) -> str:
|
|
32
34
|
"""Generuje .proto z modelu CQRS."""
|
|
33
35
|
lines: list[str] = [
|
|
34
36
|
'syntax = "proto3";',
|
|
35
37
|
"",
|
|
36
|
-
|
|
38
|
+
"// Generated by code2schema v0.1",
|
|
37
39
|
f"// Modules: {len(schema.modules)}, "
|
|
38
40
|
f"Commands: {len(schema.commands())}, "
|
|
39
41
|
f"Queries: {len(schema.queries())}",
|
|
@@ -50,9 +52,7 @@ def to_proto(schema: SchemaIR) -> str:
|
|
|
50
52
|
if func.role in (CQRSRole.QUERY, CQRSRole.COMMAND, CQRSRole.ORCHESTRATOR):
|
|
51
53
|
suffix = func.role.value.capitalize()
|
|
52
54
|
safe_name = _safe_proto_name(func.name)
|
|
53
|
-
lines.append(
|
|
54
|
-
f" rpc {safe_name}{suffix} (Request) returns (Response);"
|
|
55
|
-
)
|
|
55
|
+
lines.append(f" rpc {safe_name}{suffix} (Request) returns (Response);")
|
|
56
56
|
|
|
57
57
|
lines += ["}", ""]
|
|
58
58
|
return "\n".join(lines)
|
|
@@ -64,6 +64,7 @@ def write_proto(schema: SchemaIR, path: Path) -> None:
|
|
|
64
64
|
|
|
65
65
|
# ── Markdown summary ──────────────────────────────────────────────────────────
|
|
66
66
|
|
|
67
|
+
|
|
67
68
|
def to_markdown(schema: SchemaIR) -> str:
|
|
68
69
|
"""Zwraca czytelne Markdown podsumowanie."""
|
|
69
70
|
funcs = schema.all_functions()
|
|
@@ -81,8 +82,8 @@ def to_markdown(schema: SchemaIR) -> str:
|
|
|
81
82
|
"",
|
|
82
83
|
"## CQRS Distribution",
|
|
83
84
|
"",
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
"| Role | Count |",
|
|
86
|
+
"|------|-------|",
|
|
86
87
|
f"| Query | {queries} |",
|
|
87
88
|
f"| Command | {commands} |",
|
|
88
89
|
f"| Orchestrator | {orchestrators} |",
|
|
@@ -112,6 +113,7 @@ def write_markdown(schema: SchemaIR, path: Path) -> None:
|
|
|
112
113
|
|
|
113
114
|
# ── helpers ───────────────────────────────────────────────────────────────────
|
|
114
115
|
|
|
116
|
+
|
|
115
117
|
def _safe_proto_name(name: str) -> str:
|
|
116
118
|
"""CamelCase dla nazw proto RPC."""
|
|
117
119
|
return "".join(part.capitalize() for part in name.split("_"))
|
|
@@ -4,6 +4,7 @@ code2schema.codegen.visualizer
|
|
|
4
4
|
Generuje interaktywny HTML z grafem CQRS (D3.js force layout).
|
|
5
5
|
Bez zewnętrznych zależności poza stdlib — D3 ładowany z CDN.
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
from __future__ import annotations
|
|
8
9
|
|
|
9
10
|
import json
|
|
@@ -14,66 +15,88 @@ from code2schema.core.models import CQRSRole, SchemaIR
|
|
|
14
15
|
|
|
15
16
|
# ── Kolory ról ────────────────────────────────────────────────────────────────
|
|
16
17
|
ROLE_COLOR: dict[str, str] = {
|
|
17
|
-
CQRSRole.QUERY:
|
|
18
|
-
CQRSRole.COMMAND:
|
|
19
|
-
CQRSRole.ORCHESTRATOR: "#a78bfa",
|
|
20
|
-
CQRSRole.UNKNOWN:
|
|
18
|
+
CQRSRole.QUERY: "#4ade80", # zielony
|
|
19
|
+
CQRSRole.COMMAND: "#fb923c", # pomarańczowy
|
|
20
|
+
CQRSRole.ORCHESTRATOR: "#a78bfa", # fioletowy
|
|
21
|
+
CQRSRole.UNKNOWN: "#94a3b8", # szary
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
ROLE_EMOJI: dict[str, str] = {
|
|
24
|
-
CQRSRole.QUERY:
|
|
25
|
-
CQRSRole.COMMAND:
|
|
25
|
+
CQRSRole.QUERY: "🔍",
|
|
26
|
+
CQRSRole.COMMAND: "✏️",
|
|
26
27
|
CQRSRole.ORCHESTRATOR: "🔀",
|
|
27
|
-
CQRSRole.UNKNOWN:
|
|
28
|
+
CQRSRole.UNKNOWN: "❓",
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
|
|
31
|
-
def
|
|
32
|
-
"""Buduje
|
|
32
|
+
def _build_nodes(schema: SchemaIR) -> tuple[list[dict], dict[str, int]]:
|
|
33
|
+
"""Buduje listę node'ów i mapę nazwa → index dla D3."""
|
|
33
34
|
nodes: list[dict] = []
|
|
34
|
-
links: list[dict] = []
|
|
35
35
|
node_ids: dict[str, int] = {}
|
|
36
|
-
|
|
37
36
|
for func in schema.all_functions():
|
|
38
37
|
idx = len(nodes)
|
|
39
38
|
node_ids[func.name] = idx
|
|
40
|
-
nodes.append(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
39
|
+
nodes.append(
|
|
40
|
+
{
|
|
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
|
+
return nodes, node_ids
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _build_links(schema: SchemaIR, node_ids: dict[str, int]) -> list[dict]:
|
|
57
|
+
"""Buduje listę krawędzi (caller → callee) dla D3."""
|
|
58
|
+
links: list[dict] = []
|
|
53
59
|
for func in schema.all_functions():
|
|
54
60
|
src = node_ids.get(func.name)
|
|
61
|
+
if src is None:
|
|
62
|
+
continue
|
|
55
63
|
for callee in func.calls:
|
|
56
64
|
dst = node_ids.get(callee)
|
|
57
|
-
if
|
|
65
|
+
if dst is not None and src != dst:
|
|
58
66
|
links.append({"source": src, "target": dst})
|
|
67
|
+
return links
|
|
68
|
+
|
|
59
69
|
|
|
60
|
-
|
|
70
|
+
def _group_rules_by_target(schema: SchemaIR) -> dict[str, list[str]]:
|
|
71
|
+
"""Grupuje rule.id po krótkiej nazwie targetu."""
|
|
72
|
+
rules_by_target: dict[str, list[str]] = {}
|
|
61
73
|
for r in schema.rules:
|
|
62
74
|
rules_by_target.setdefault(r.target.split(".")[-1], []).append(r.id)
|
|
75
|
+
return rules_by_target
|
|
63
76
|
|
|
77
|
+
|
|
78
|
+
def _build_stats(schema: SchemaIR, function_count: int) -> dict[str, int]:
|
|
79
|
+
"""Buduje sekcję 'stats' wyświetlaną w pasku górnym."""
|
|
80
|
+
return {
|
|
81
|
+
"modules": len(schema.modules),
|
|
82
|
+
"functions": function_count,
|
|
83
|
+
"commands": len(schema.commands()),
|
|
84
|
+
"queries": len(schema.queries()),
|
|
85
|
+
"orchestrators": len(schema.orchestrators()),
|
|
86
|
+
"workflows": len(schema.workflows),
|
|
87
|
+
"rules": len(schema.rules),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _build_graph_data(schema: SchemaIR) -> dict[str, Any]:
|
|
92
|
+
"""Buduje nodes/links/rules/stats dla D3 force graph."""
|
|
93
|
+
nodes, node_ids = _build_nodes(schema)
|
|
94
|
+
links = _build_links(schema, node_ids)
|
|
64
95
|
return {
|
|
65
96
|
"nodes": nodes,
|
|
66
97
|
"links": links,
|
|
67
|
-
"rules":
|
|
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
|
-
},
|
|
98
|
+
"rules": _group_rules_by_target(schema),
|
|
99
|
+
"stats": _build_stats(schema, len(nodes)),
|
|
77
100
|
}
|
|
78
101
|
|
|
79
102
|
|
|
@@ -4,22 +4,54 @@ code2schema.core.extractor
|
|
|
4
4
|
Ekstrakcja funkcji i importów z plików .py przy użyciu wbudowanego modułu `ast`.
|
|
5
5
|
Bez zewnętrznych zależności — czyste stdlib.
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
from __future__ import annotations
|
|
8
9
|
|
|
9
10
|
import ast
|
|
10
|
-
import os
|
|
11
11
|
from pathlib import Path
|
|
12
|
-
from typing import List
|
|
13
12
|
|
|
14
13
|
from code2schema.core.models import FunctionIR, ModuleIR
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
# ── Wzorce side-effectów ─────────────────────────────────────────────────────
|
|
18
17
|
|
|
19
|
-
_FILESYSTEM_CALLS: set[str] = {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
_FILESYSTEM_CALLS: set[str] = {
|
|
19
|
+
"open",
|
|
20
|
+
"write",
|
|
21
|
+
"read",
|
|
22
|
+
"unlink",
|
|
23
|
+
"mkdir",
|
|
24
|
+
"rmdir",
|
|
25
|
+
"rename",
|
|
26
|
+
}
|
|
27
|
+
_NETWORK_CALLS: set[str] = {
|
|
28
|
+
"get",
|
|
29
|
+
"post",
|
|
30
|
+
"put",
|
|
31
|
+
"delete",
|
|
32
|
+
"patch",
|
|
33
|
+
"request",
|
|
34
|
+
"fetch",
|
|
35
|
+
"connect",
|
|
36
|
+
}
|
|
37
|
+
_SYSTEM_CALLS: set[str] = {
|
|
38
|
+
"system",
|
|
39
|
+
"popen",
|
|
40
|
+
"subprocess",
|
|
41
|
+
"Popen",
|
|
42
|
+
"run",
|
|
43
|
+
"call",
|
|
44
|
+
"check_output",
|
|
45
|
+
}
|
|
46
|
+
_DB_CALLS: set[str] = {
|
|
47
|
+
"execute",
|
|
48
|
+
"commit",
|
|
49
|
+
"rollback",
|
|
50
|
+
"query",
|
|
51
|
+
"insert",
|
|
52
|
+
"update",
|
|
53
|
+
"delete",
|
|
54
|
+
}
|
|
23
55
|
|
|
24
56
|
_NETWORK_MODULES: set[str] = {"requests", "httpx", "aiohttp", "urllib", "http"}
|
|
25
57
|
_SYSTEM_MODULES: set[str] = {"os", "subprocess", "shutil", "sys"}
|
|
@@ -55,7 +87,9 @@ class _FunctionVisitor(ast.NodeVisitor):
|
|
|
55
87
|
self._process_func(node, is_async=True)
|
|
56
88
|
self.generic_visit(node)
|
|
57
89
|
|
|
58
|
-
def _process_func(
|
|
90
|
+
def _process_func(
|
|
91
|
+
self, node: ast.FunctionDef | ast.AsyncFunctionDef, is_async: bool
|
|
92
|
+
) -> None:
|
|
59
93
|
calls = self._collect_calls(node)
|
|
60
94
|
side_effects = self._detect_side_effects(node, calls)
|
|
61
95
|
docstring = ast.get_docstring(node)
|
|
@@ -66,7 +100,9 @@ class _FunctionVisitor(ast.NodeVisitor):
|
|
|
66
100
|
calls=list(dict.fromkeys(calls)), # deduplicate, preserve order
|
|
67
101
|
fan_out=len(set(calls)),
|
|
68
102
|
side_effects=side_effects,
|
|
69
|
-
lines=
|
|
103
|
+
lines=(
|
|
104
|
+
node.end_lineno - node.lineno + 1 if hasattr(node, "end_lineno") else 0
|
|
105
|
+
),
|
|
70
106
|
is_async=is_async,
|
|
71
107
|
docstring=docstring,
|
|
72
108
|
)
|
|
@@ -90,9 +126,7 @@ class _FunctionVisitor(ast.NodeVisitor):
|
|
|
90
126
|
return func_node.attr
|
|
91
127
|
return None
|
|
92
128
|
|
|
93
|
-
def _detect_side_effects(
|
|
94
|
-
self, node: ast.AST, calls: list[str]
|
|
95
|
-
) -> list[str]:
|
|
129
|
+
def _detect_side_effects(self, node: ast.AST, calls: list[str]) -> list[str]:
|
|
96
130
|
from code2schema.core.models import SideEffect
|
|
97
131
|
|
|
98
132
|
effects: list[SideEffect] = []
|
|
@@ -112,6 +146,7 @@ class _FunctionVisitor(ast.NodeVisitor):
|
|
|
112
146
|
|
|
113
147
|
# ── Public API ────────────────────────────────────────────────────────────────
|
|
114
148
|
|
|
149
|
+
|
|
115
150
|
def extract_module(path: Path) -> ModuleIR | None:
|
|
116
151
|
"""Parsuje jeden plik .py i zwraca ModuleIR."""
|
|
117
152
|
try:
|
|
@@ -127,7 +162,9 @@ def extract_module(path: Path) -> ModuleIR | None:
|
|
|
127
162
|
imports = [
|
|
128
163
|
alias.name
|
|
129
164
|
for node in ast.walk(tree)
|
|
130
|
-
for alias in (
|
|
165
|
+
for alias in (
|
|
166
|
+
node.names if isinstance(node, (ast.Import, ast.ImportFrom)) else []
|
|
167
|
+
)
|
|
131
168
|
]
|
|
132
169
|
|
|
133
170
|
return ModuleIR(
|
|
@@ -3,10 +3,11 @@ code2schema.core.models
|
|
|
3
3
|
~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
4
|
Intermediate Representation (IR) — język-most między kodem a schematem.
|
|
5
5
|
"""
|
|
6
|
+
|
|
6
7
|
from __future__ import annotations
|
|
7
8
|
|
|
8
9
|
from enum import Enum
|
|
9
|
-
from typing import List, Optional
|
|
10
|
+
from typing import List, Optional
|
|
10
11
|
|
|
11
12
|
from pydantic import BaseModel, Field
|
|
12
13
|
|
|
@@ -28,6 +29,7 @@ class SideEffect(str, Enum):
|
|
|
28
29
|
|
|
29
30
|
class FunctionIR(BaseModel):
|
|
30
31
|
"""Pojedyncza funkcja w modelu semantycznym."""
|
|
32
|
+
|
|
31
33
|
name: str
|
|
32
34
|
module: str
|
|
33
35
|
calls: List[str] = Field(default_factory=list)
|
|
@@ -45,7 +47,8 @@ class FunctionIR(BaseModel):
|
|
|
45
47
|
|
|
46
48
|
class ModuleIR(BaseModel):
|
|
47
49
|
"""Moduł (plik .py) z wyekstrahowanymi funkcjami."""
|
|
48
|
-
|
|
50
|
+
|
|
51
|
+
name: str # np. "backend.services.analyzer"
|
|
49
52
|
path: str
|
|
50
53
|
functions: List[FunctionIR] = Field(default_factory=list)
|
|
51
54
|
imports: List[str] = Field(default_factory=list)
|
|
@@ -59,6 +62,7 @@ class WorkflowStep(BaseModel):
|
|
|
59
62
|
|
|
60
63
|
class WorkflowIR(BaseModel):
|
|
61
64
|
"""Graf wykonania (DAG) dla jednej funkcji-orkiestratora."""
|
|
65
|
+
|
|
62
66
|
name: str
|
|
63
67
|
entry: str
|
|
64
68
|
steps: List[WorkflowStep] = Field(default_factory=list)
|
|
@@ -66,6 +70,7 @@ class WorkflowIR(BaseModel):
|
|
|
66
70
|
|
|
67
71
|
class RuleIR(BaseModel):
|
|
68
72
|
"""Heurystyczna reguła jakości wygenerowana z analizy."""
|
|
73
|
+
|
|
69
74
|
id: str
|
|
70
75
|
target: str
|
|
71
76
|
condition: str
|
|
@@ -75,7 +80,10 @@ class RuleIR(BaseModel):
|
|
|
75
80
|
|
|
76
81
|
class SchemaIR(BaseModel):
|
|
77
82
|
"""Korzeń modelu semantycznego całego projektu."""
|
|
78
|
-
|
|
83
|
+
|
|
84
|
+
system: dict = Field(
|
|
85
|
+
default_factory=lambda: {"type": "code2schema", "version": "0.1"}
|
|
86
|
+
)
|
|
79
87
|
modules: List[ModuleIR] = Field(default_factory=list)
|
|
80
88
|
workflows: List[WorkflowIR] = Field(default_factory=list)
|
|
81
89
|
rules: List[RuleIR] = Field(default_factory=list)
|
|
@@ -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,11 +34,11 @@ 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
43
|
Generated on 2026-05-07 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
44
44
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|