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.
Files changed (24) hide show
  1. {code2schema-0.1.2 → code2schema-0.1.6}/PKG-INFO +6 -6
  2. {code2schema-0.1.2 → code2schema-0.1.6}/README.md +5 -5
  3. {code2schema-0.1.2 → code2schema-0.1.6}/code2schema/__init__.py +9 -3
  4. {code2schema-0.1.2 → code2schema-0.1.6}/code2schema/analyzer/cqrs.py +38 -28
  5. {code2schema-0.1.2 → code2schema-0.1.6}/code2schema/analyzer/events.py +49 -36
  6. {code2schema-0.1.2 → code2schema-0.1.6}/code2schema/analyzer/graph.py +7 -3
  7. code2schema-0.1.6/code2schema/cli.py +252 -0
  8. {code2schema-0.1.2 → code2schema-0.1.6}/code2schema/codegen/__init__.py +9 -7
  9. {code2schema-0.1.2 → code2schema-0.1.6}/code2schema/codegen/visualizer.py +59 -36
  10. {code2schema-0.1.2 → code2schema-0.1.6}/code2schema/core/extractor.py +49 -12
  11. {code2schema-0.1.2 → code2schema-0.1.6}/code2schema/core/models.py +11 -3
  12. {code2schema-0.1.2 → code2schema-0.1.6}/code2schema.egg-info/PKG-INFO +6 -6
  13. {code2schema-0.1.2 → code2schema-0.1.6}/pyproject.toml +1 -1
  14. {code2schema-0.1.2 → code2schema-0.1.6}/tests/test_code2schema.py +47 -0
  15. code2schema-0.1.2/code2schema/cli.py +0 -147
  16. {code2schema-0.1.2 → code2schema-0.1.6}/LICENSE +0 -0
  17. {code2schema-0.1.2 → code2schema-0.1.6}/code2schema/analyzer/__init__.py +0 -0
  18. {code2schema-0.1.2 → code2schema-0.1.6}/code2schema/core/__init__.py +0 -0
  19. {code2schema-0.1.2 → code2schema-0.1.6}/code2schema.egg-info/SOURCES.txt +0 -0
  20. {code2schema-0.1.2 → code2schema-0.1.6}/code2schema.egg-info/dependency_links.txt +0 -0
  21. {code2schema-0.1.2 → code2schema-0.1.6}/code2schema.egg-info/entry_points.txt +0 -0
  22. {code2schema-0.1.2 → code2schema-0.1.6}/code2schema.egg-info/requires.txt +0 -0
  23. {code2schema-0.1.2 → code2schema-0.1.6}/code2schema.egg-info/top_level.txt +0 -0
  24. {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.2
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
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.2-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
38
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.15-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-2.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
37
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.6-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
38
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.60-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-3.6h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
39
39
 
40
- - 🤖 **LLM usage:** $0.1500 (1 commits)
41
- - 👤 **Human dev:** ~$200 (2.0h @ $100/h, 30min dedup)
40
+ - 🤖 **LLM usage:** $0.6000 (4 commits)
41
+ - 👤 **Human dev:** ~$361 (3.6h @ $100/h, 30min dedup)
42
42
 
43
- Generated on 2026-05-04 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
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
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.2-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.15-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-2.0h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.6-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$0.60-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-3.6h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $0.1500 (1 commits)
10
- - 👤 **Human dev:** ~$200 (2.0h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $0.6000 (4 commits)
10
+ - 👤 **Human dev:** ~$361 (3.6h @ $100/h, 30min dedup)
11
11
 
12
- Generated on 2026-05-04 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
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", "CQRSRole", "FunctionIR", "ModuleIR",
8
- "extract_project", "extract_module", "analyze",
8
+ "SchemaIR",
9
+ "CQRSRole",
10
+ "FunctionIR",
11
+ "ModuleIR",
12
+ "extract_project",
13
+ "extract_module",
14
+ "analyze",
9
15
  ]
10
- __version__ = "0.1.2"
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 # >= N wywołań → orchestrator
26
- FAN_OUT_HIGH = 10 # alert reguły
27
- CC_LIMIT = 15 # (placeholder, liczony zewnętrznie)
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(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
- ))
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(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
- ))
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, FunctionIR, ModuleIR
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 # np. "UserCreated"
37
- emitted_by: str # qualified_name funkcji emitującej
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 # nazwa wywołania, które traktujemy jako "command"
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(f" {ev.name:30s} ← {ev.emitted_by.split('.')[-1]:25s} → [{handlers}]")
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
- # Krok 1: znajdź emiterów
86
- emitters: dict[str, DomainEvent] = {}
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
- ev = DomainEvent(name=event_name, emitted_by=f.qualified_name)
94
- emitters[f.name] = ev
95
- model.events.append(ev)
88
+ events.append(DomainEvent(name=event_name, emitted_by=f.qualified_name))
89
+ return events
96
90
 
97
- # Krok 2: znajdź handlerów
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
- # sprawdź, czy handler pasuje do jakiegoś zdarzenia
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
- # Krok 3: command handlers
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
- model.commands.append(
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
- # Krok 4: agregaty moduły z operacjami CRUD
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
- model.aggregates.append(mod.name)
131
+ aggregates.append(mod.name)
132
+ return aggregates
133
133
 
134
- return model
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", "update": "Updated", "delete": "Deleted",
158
- "save": "Saved", "send": "Sent", "emit": "Emitted",
159
- "publish": "Published", "register": "Registered",
160
- "process": "Processed", "notify": "Notified",
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 Dict, List, Tuple
14
+ from typing import List, Tuple
14
15
 
15
16
  import networkx as nx
16
17
 
17
- from code2schema.core.models import CQRSRole, ModuleIR, SchemaIR
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 {", ' rankdir=LR;', ' node [shape=box];', ""]
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())