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.
Files changed (23) hide show
  1. {code2schema-0.1.4 → code2schema-0.1.6}/PKG-INFO +5 -5
  2. {code2schema-0.1.4 → code2schema-0.1.6}/README.md +4 -4
  3. {code2schema-0.1.4 → code2schema-0.1.6}/code2schema/__init__.py +9 -3
  4. {code2schema-0.1.4 → code2schema-0.1.6}/code2schema/analyzer/cqrs.py +38 -28
  5. {code2schema-0.1.4 → code2schema-0.1.6}/code2schema/analyzer/events.py +21 -14
  6. {code2schema-0.1.4 → code2schema-0.1.6}/code2schema/analyzer/graph.py +7 -3
  7. {code2schema-0.1.4 → code2schema-0.1.6}/code2schema/cli.py +62 -17
  8. {code2schema-0.1.4 → code2schema-0.1.6}/code2schema/codegen/__init__.py +9 -7
  9. {code2schema-0.1.4 → code2schema-0.1.6}/code2schema/codegen/visualizer.py +59 -36
  10. {code2schema-0.1.4 → code2schema-0.1.6}/code2schema/core/extractor.py +49 -12
  11. {code2schema-0.1.4 → code2schema-0.1.6}/code2schema/core/models.py +11 -3
  12. {code2schema-0.1.4 → code2schema-0.1.6}/code2schema.egg-info/PKG-INFO +5 -5
  13. {code2schema-0.1.4 → code2schema-0.1.6}/pyproject.toml +1 -1
  14. {code2schema-0.1.4 → code2schema-0.1.6}/LICENSE +0 -0
  15. {code2schema-0.1.4 → code2schema-0.1.6}/code2schema/analyzer/__init__.py +0 -0
  16. {code2schema-0.1.4 → code2schema-0.1.6}/code2schema/core/__init__.py +0 -0
  17. {code2schema-0.1.4 → code2schema-0.1.6}/code2schema.egg-info/SOURCES.txt +0 -0
  18. {code2schema-0.1.4 → code2schema-0.1.6}/code2schema.egg-info/dependency_links.txt +0 -0
  19. {code2schema-0.1.4 → code2schema-0.1.6}/code2schema.egg-info/entry_points.txt +0 -0
  20. {code2schema-0.1.4 → code2schema-0.1.6}/code2schema.egg-info/requires.txt +0 -0
  21. {code2schema-0.1.4 → code2schema-0.1.6}/code2schema.egg-info/top_level.txt +0 -0
  22. {code2schema-0.1.4 → code2schema-0.1.6}/setup.cfg +0 -0
  23. {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.4
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
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.4-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.45-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-3.5h-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.4500 (3 commits)
41
- - 👤 **Human dev:** ~$350 (3.5h @ $100/h, 30min dedup)
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
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.4-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.45-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-3.5h-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.4500 (3 commits)
10
- - 👤 **Human dev:** ~$350 (3.5h @ $100/h, 30min dedup)
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", "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.4"
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,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", "update": "Updated", "delete": "Deleted",
164
- "save": "Saved", "send": "Sent", "emit": "Emitted",
165
- "publish": "Published", "register": "Registered",
166
- "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",
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 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 = {
@@ -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, detect_cycles, graph_summary, write_dot, write_graphml,
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
- parser = argparse.ArgumentParser(prog="code2schema",
46
- description="Semantic Compiler: Code → CQRS → Schema / Proto / Graph")
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("-o", "--out", metavar="FILE", help="Output JSON schema (default: <project>_schema.json)")
49
- parser.add_argument("--proto", metavar="FILE", nargs="?", const=True, default=None,
50
- help="Output .proto file (default: <project>_api.proto if flag present)")
51
- parser.add_argument("--md", metavar="FILE", nargs="?", const=True, default=None,
52
- help="Output Markdown report (default: <project>_report.md if flag present)")
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("--html", metavar="FILE", nargs="?", const=True, default=None,
56
- help="Interactive HTML visualization (default: <project>_viz.html if flag present)")
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("-V", "--version", action="version",
64
- version=f"%(prog)s {__version__}")
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 _resolve_paths(
93
- args: argparse.Namespace,
94
- ) -> tuple[Path, Path, Path | None, Path | None, Path | None]:
95
- """Resolve root, project name and output file paths from CLI args."""
96
- root = Path(args.path)
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
- f"// Generated by code2schema v0.1",
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
- f"| Role | Count |",
85
- f"|------|-------|",
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: "#4ade80", # zielony
18
- CQRSRole.COMMAND: "#fb923c", # pomarańczowy
19
- CQRSRole.ORCHESTRATOR: "#a78bfa", # fioletowy
20
- CQRSRole.UNKNOWN: "#94a3b8", # szary
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 _build_graph_data(schema: SchemaIR) -> dict[str, Any]:
32
- """Buduje nodes/links dla D3 force graph."""
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
- "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
-
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 src is not None and dst is not None and src != dst:
65
+ if dst is not None and src != dst:
58
66
  links.append({"source": src, "target": dst})
67
+ return links
68
+
59
69
 
60
- rules_by_target = {}
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": rules_by_target,
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] = {"open", "write", "read", "unlink", "mkdir", "rmdir", "rename"}
20
- _NETWORK_CALLS: set[str] = {"get", "post", "put", "delete", "patch", "request", "fetch", "connect"}
21
- _SYSTEM_CALLS: set[str] = {"system", "popen", "subprocess", "Popen", "run", "call", "check_output"}
22
- _DB_CALLS: set[str] = {"execute", "commit", "rollback", "query", "insert", "update", "delete"}
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(self, node: ast.FunctionDef | ast.AsyncFunctionDef, is_async: bool) -> None:
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=node.end_lineno - node.lineno + 1 if hasattr(node, "end_lineno") else 0,
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 (node.names if isinstance(node, (ast.Import, ast.ImportFrom)) else [])
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, Set
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
- name: str # np. "backend.services.analyzer"
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
- system: dict = Field(default_factory=lambda: {"type": "code2schema", "version": "0.1"})
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.4
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
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.4-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.45-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-3.5h-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.4500 (3 commits)
41
- - 👤 **Human dev:** ~$350 (3.5h @ $100/h, 30min dedup)
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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "code2schema"
7
- version = "0.1.4"
7
+ version = "0.1.6"
8
8
  description = "Semantic compiler: Code → AST → CQRS Model → Workflow DAG → Proto/Schema"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
File without changes
File without changes