code2schema 0.1.7__tar.gz → 0.1.9__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.7 → code2schema-0.1.9}/PKG-INFO +11 -9
- {code2schema-0.1.7 → code2schema-0.1.9}/README.md +8 -5
- {code2schema-0.1.7 → code2schema-0.1.9}/code2schema/__init__.py +1 -1
- {code2schema-0.1.7 → code2schema-0.1.9}/code2schema/analyzer/cqrs.py +1 -3
- {code2schema-0.1.7 → code2schema-0.1.9}/code2schema/analyzer/events.py +7 -11
- {code2schema-0.1.7 → code2schema-0.1.9}/code2schema/analyzer/graph.py +11 -2
- {code2schema-0.1.7 → code2schema-0.1.9}/code2schema/cli.py +3 -6
- {code2schema-0.1.7 → code2schema-0.1.9}/code2schema/core/extractor.py +3 -9
- {code2schema-0.1.7 → code2schema-0.1.9}/code2schema/core/models.py +1 -3
- code2schema-0.1.9/code2schema/mcp_server.py +305 -0
- {code2schema-0.1.7 → code2schema-0.1.9}/code2schema.egg-info/PKG-INFO +11 -9
- {code2schema-0.1.7 → code2schema-0.1.9}/code2schema.egg-info/SOURCES.txt +3 -1
- {code2schema-0.1.7 → code2schema-0.1.9}/code2schema.egg-info/requires.txt +2 -1
- {code2schema-0.1.7 → code2schema-0.1.9}/pyproject.toml +14 -7
- code2schema-0.1.9/tests/test_event_handlers.py +30 -0
- {code2schema-0.1.7 → code2schema-0.1.9}/LICENSE +0 -0
- {code2schema-0.1.7 → code2schema-0.1.9}/code2schema/analyzer/__init__.py +0 -0
- {code2schema-0.1.7 → code2schema-0.1.9}/code2schema/codegen/__init__.py +0 -0
- {code2schema-0.1.7 → code2schema-0.1.9}/code2schema/codegen/visualizer.py +0 -0
- {code2schema-0.1.7 → code2schema-0.1.9}/code2schema/core/__init__.py +0 -0
- {code2schema-0.1.7 → code2schema-0.1.9}/code2schema.egg-info/dependency_links.txt +0 -0
- {code2schema-0.1.7 → code2schema-0.1.9}/code2schema.egg-info/entry_points.txt +0 -0
- {code2schema-0.1.7 → code2schema-0.1.9}/code2schema.egg-info/top_level.txt +0 -0
- {code2schema-0.1.7 → code2schema-0.1.9}/setup.cfg +0 -0
- {code2schema-0.1.7 → code2schema-0.1.9}/tests/test_code2schema.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code2schema
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: Semantic compiler: Code → AST → CQRS Model → Workflow DAG → Proto/Schema
|
|
5
|
-
License: Apache-2.0
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
6
|
Keywords: ast,cqrs,static-analysis,code-quality,schema
|
|
7
7
|
Classifier: Development Status :: 3 - Alpha
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -24,9 +24,8 @@ Requires-Dist: pytest>=8; extra == "dev"
|
|
|
24
24
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
25
25
|
Requires-Dist: black; extra == "dev"
|
|
26
26
|
Requires-Dist: ruff; extra == "dev"
|
|
27
|
-
Requires-Dist: goal>=2.1.0; extra == "dev"
|
|
28
27
|
Requires-Dist: costs>=0.1.20; extra == "dev"
|
|
29
|
-
Requires-Dist: pfix>=0.1.60; extra == "dev"
|
|
28
|
+
Requires-Dist: pfix>=0.1.60; python_version >= "3.10" and extra == "dev"
|
|
30
29
|
Dynamic: license-file
|
|
31
30
|
|
|
32
31
|
# code2schema
|
|
@@ -34,13 +33,13 @@ Dynamic: license-file
|
|
|
34
33
|
|
|
35
34
|
## AI Cost Tracking
|
|
36
35
|
|
|
37
|
-
    
|
|
37
|
+
  
|
|
39
38
|
|
|
40
|
-
- 🤖 **LLM usage:** $
|
|
41
|
-
- 👤 **Human dev:** ~$
|
|
39
|
+
- 🤖 **LLM usage:** $6.7712 (15 commits)
|
|
40
|
+
- 👤 **Human dev:** ~$1736 (17.4h @ $100/h, 30min dedup)
|
|
42
41
|
|
|
43
|
-
Generated on 2026-
|
|
42
|
+
Generated on 2026-09-13 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
44
43
|
|
|
45
44
|
---
|
|
46
45
|
|
|
@@ -270,3 +269,6 @@ MIT
|
|
|
270
269
|
## License
|
|
271
270
|
|
|
272
271
|
Licensed under Apache-2.0.
|
|
272
|
+
## Dependency maintenance
|
|
273
|
+
|
|
274
|
+
See [dependency updates and Python tool groups](docs/dependencies.md) for locked tests, daily updates and freshness checks.
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
|
|
4
4
|
## AI Cost Tracking
|
|
5
5
|
|
|
6
|
-
    
|
|
7
|
+
  
|
|
8
8
|
|
|
9
|
-
- 🤖 **LLM usage:** $
|
|
10
|
-
- 👤 **Human dev:** ~$
|
|
9
|
+
- 🤖 **LLM usage:** $6.7712 (15 commits)
|
|
10
|
+
- 👤 **Human dev:** ~$1736 (17.4h @ $100/h, 30min dedup)
|
|
11
11
|
|
|
12
|
-
Generated on 2026-
|
|
12
|
+
Generated on 2026-09-13 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
@@ -239,3 +239,6 @@ MIT
|
|
|
239
239
|
## License
|
|
240
240
|
|
|
241
241
|
Licensed under Apache-2.0.
|
|
242
|
+
## Dependency maintenance
|
|
243
|
+
|
|
244
|
+
See [dependency updates and Python tool groups](docs/dependencies.md) for locked tests, daily updates and freshness checks.
|
|
@@ -92,9 +92,7 @@ def build_workflows(modules: List[ModuleIR]) -> List[WorkflowIR]:
|
|
|
92
92
|
workflow = WorkflowIR(
|
|
93
93
|
name=f"workflow_{f.name}",
|
|
94
94
|
entry=f.qualified_name,
|
|
95
|
-
steps=[
|
|
96
|
-
WorkflowStep(callee=c, is_async=f.is_async) for c in f.calls
|
|
97
|
-
],
|
|
95
|
+
steps=[WorkflowStep(callee=c, is_async=f.is_async) for c in f.calls],
|
|
98
96
|
)
|
|
99
97
|
workflows.append(workflow)
|
|
100
98
|
return workflows
|
|
@@ -62,9 +62,7 @@ class EventModel:
|
|
|
62
62
|
lines.append("\nDomain Events:")
|
|
63
63
|
for ev in self.events[:15]:
|
|
64
64
|
handlers = ", ".join(ev.handled_by[:3]) or "—"
|
|
65
|
-
lines.append(
|
|
66
|
-
f" {ev.name:30s} ← {ev.emitted_by.split('.')[-1]:25s} → [{handlers}]"
|
|
67
|
-
)
|
|
65
|
+
lines.append(f" {ev.name:30s} ← {ev.emitted_by.split('.')[-1]:25s} → [{handlers}]")
|
|
68
66
|
if self.commands:
|
|
69
67
|
lines.append("\nCommand Handlers (top 10):")
|
|
70
68
|
for cmd in self.commands[:10]:
|
|
@@ -91,12 +89,14 @@ def _find_emitters(modules: List[ModuleIR]) -> List[DomainEvent]:
|
|
|
91
89
|
|
|
92
90
|
def _find_handlers(events: List[DomainEvent], modules: List[ModuleIR]) -> None:
|
|
93
91
|
"""Krok 2: dopasuj handlery do zdarzeń (mutuje events in-place)."""
|
|
92
|
+
# Event keywords are invariant for this pass; normalize each only once.
|
|
93
|
+
keywords = [(ev, ev.name.lower().replace("event", "").strip()) for ev in events]
|
|
94
94
|
for mod in modules:
|
|
95
95
|
for f in mod.functions:
|
|
96
96
|
if _EVENT_HANDLER_PATTERNS.search(f.name):
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if keyword and keyword in
|
|
97
|
+
handler_name = f.name.lower()
|
|
98
|
+
for ev, keyword in keywords:
|
|
99
|
+
if keyword and keyword in handler_name:
|
|
100
100
|
ev.handled_by.append(f.qualified_name)
|
|
101
101
|
|
|
102
102
|
|
|
@@ -106,11 +106,7 @@ def _find_command_handlers(modules: List[ModuleIR]) -> List[CommandHandler]:
|
|
|
106
106
|
for mod in modules:
|
|
107
107
|
for f in mod.functions:
|
|
108
108
|
if f.role == CQRSRole.COMMAND or _AGGREGATE_PATTERNS.search(f.name):
|
|
109
|
-
emits = [
|
|
110
|
-
_derive_event_name(c)
|
|
111
|
-
for c in f.calls
|
|
112
|
-
if _EVENT_EMIT_PATTERNS.search(c)
|
|
113
|
-
]
|
|
109
|
+
emits = [_derive_event_name(c) for c in f.calls if _EVENT_EMIT_PATTERNS.search(c)]
|
|
114
110
|
if emits or f.role == CQRSRole.COMMAND:
|
|
115
111
|
commands.append(
|
|
116
112
|
CommandHandler(
|
|
@@ -54,10 +54,19 @@ def build_rich_graph(schema: SchemaIR) -> nx.DiGraph:
|
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
def centrality_report(G: nx.DiGraph, top_n: int = 10) -> List[Tuple[str, float]]:
|
|
57
|
-
"""PageRank — funkcje najważniejsze architektonicznie.
|
|
57
|
+
"""PageRank — funkcje najważniejsze architektonicznie.
|
|
58
|
+
|
|
59
|
+
Falls back to in-degree ranking when scipy is unavailable
|
|
60
|
+
(networkx pagerank delegates to scipy on some versions).
|
|
61
|
+
"""
|
|
58
62
|
if G.number_of_edges() == 0:
|
|
59
63
|
return []
|
|
60
|
-
|
|
64
|
+
try:
|
|
65
|
+
pr = nx.pagerank(G, alpha=0.85)
|
|
66
|
+
except ModuleNotFoundError:
|
|
67
|
+
pr = dict(G.in_degree())
|
|
68
|
+
total = sum(pr.values()) or 1
|
|
69
|
+
pr = {node: deg / total for node, deg in pr.items()}
|
|
61
70
|
return sorted(pr.items(), key=lambda x: x[1], reverse=True)[:top_n]
|
|
62
71
|
|
|
63
72
|
|
|
@@ -91,9 +91,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
91
91
|
parser.add_argument("--no-rules", action="store_true")
|
|
92
92
|
parser.add_argument("--exclude", nargs="*", default=[])
|
|
93
93
|
parser.add_argument("-q", "--quiet", action="store_true")
|
|
94
|
-
parser.add_argument(
|
|
95
|
-
"-V", "--version", action="version", version=f"%(prog)s {__version__}"
|
|
96
|
-
)
|
|
94
|
+
parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {__version__}")
|
|
97
95
|
return parser
|
|
98
96
|
|
|
99
97
|
|
|
@@ -209,7 +207,7 @@ def _print_summary(modules, schema, G, t0, out_path, proto_path, md_path, html_p
|
|
|
209
207
|
if html_path:
|
|
210
208
|
outputs.append(f" → {html_path.name}")
|
|
211
209
|
print(
|
|
212
|
-
f"\n✅ Gotowe ({time.perf_counter()-t0:.2f}s)\n"
|
|
210
|
+
f"\n✅ Gotowe ({time.perf_counter() - t0:.2f}s)\n"
|
|
213
211
|
f" Modules : {len(modules)}\n"
|
|
214
212
|
f" Functions: {len(funcs)}\n"
|
|
215
213
|
f" Queries : {len(schema.queries())}\n"
|
|
@@ -217,8 +215,7 @@ def _print_summary(modules, schema, G, t0, out_path, proto_path, md_path, html_p
|
|
|
217
215
|
f" Orchest. : {len(schema.orchestrators())}\n"
|
|
218
216
|
f" Workflows: {len(schema.workflows)}\n"
|
|
219
217
|
f" Rules : {len(schema.rules)}\n"
|
|
220
|
-
f" Graph : {G.number_of_nodes()}N / {G.number_of_edges()}E\n"
|
|
221
|
-
+ "\n".join(outputs)
|
|
218
|
+
f" Graph : {G.number_of_nodes()}N / {G.number_of_edges()}E\n" + "\n".join(outputs)
|
|
222
219
|
)
|
|
223
220
|
|
|
224
221
|
|
|
@@ -87,9 +87,7 @@ class _FunctionVisitor(ast.NodeVisitor):
|
|
|
87
87
|
self._process_func(node, is_async=True)
|
|
88
88
|
self.generic_visit(node)
|
|
89
89
|
|
|
90
|
-
def _process_func(
|
|
91
|
-
self, node: ast.FunctionDef | ast.AsyncFunctionDef, is_async: bool
|
|
92
|
-
) -> None:
|
|
90
|
+
def _process_func(self, node: ast.FunctionDef | ast.AsyncFunctionDef, is_async: bool) -> None:
|
|
93
91
|
calls = self._collect_calls(node)
|
|
94
92
|
side_effects = self._detect_side_effects(node, calls)
|
|
95
93
|
docstring = ast.get_docstring(node)
|
|
@@ -100,9 +98,7 @@ class _FunctionVisitor(ast.NodeVisitor):
|
|
|
100
98
|
calls=list(dict.fromkeys(calls)), # deduplicate, preserve order
|
|
101
99
|
fan_out=len(set(calls)),
|
|
102
100
|
side_effects=side_effects,
|
|
103
|
-
lines=(
|
|
104
|
-
node.end_lineno - node.lineno + 1 if hasattr(node, "end_lineno") else 0
|
|
105
|
-
),
|
|
101
|
+
lines=(node.end_lineno - node.lineno + 1 if hasattr(node, "end_lineno") else 0),
|
|
106
102
|
is_async=is_async,
|
|
107
103
|
docstring=docstring,
|
|
108
104
|
)
|
|
@@ -162,9 +158,7 @@ def extract_module(path: Path) -> ModuleIR | None:
|
|
|
162
158
|
imports = [
|
|
163
159
|
alias.name
|
|
164
160
|
for node in ast.walk(tree)
|
|
165
|
-
for alias in (
|
|
166
|
-
node.names if isinstance(node, (ast.Import, ast.ImportFrom)) else []
|
|
167
|
-
)
|
|
161
|
+
for alias in (node.names if isinstance(node, (ast.Import, ast.ImportFrom)) else [])
|
|
168
162
|
]
|
|
169
163
|
|
|
170
164
|
return ModuleIR(
|
|
@@ -81,9 +81,7 @@ class RuleIR(BaseModel):
|
|
|
81
81
|
class SchemaIR(BaseModel):
|
|
82
82
|
"""Korzeń modelu semantycznego całego projektu."""
|
|
83
83
|
|
|
84
|
-
system: dict = Field(
|
|
85
|
-
default_factory=lambda: {"type": "code2schema", "version": "0.1"}
|
|
86
|
-
)
|
|
84
|
+
system: dict = Field(default_factory=lambda: {"type": "code2schema", "version": "0.1"})
|
|
87
85
|
modules: List[ModuleIR] = Field(default_factory=list)
|
|
88
86
|
workflows: List[WorkflowIR] = Field(default_factory=list)
|
|
89
87
|
rules: List[RuleIR] = Field(default_factory=list)
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""code2schema MCP server — dependency-free stdio JSON-RPC.
|
|
2
|
+
|
|
3
|
+
Adopts wellmanifest/poa (typed tools, closed inputSchema, fail-closed
|
|
4
|
+
dispatch) and wellmanifest/logs (append-only hash-chained JSONL event
|
|
5
|
+
stream under $XDG_STATE_HOME/code2schema/mcp-events.jsonl, overridable
|
|
6
|
+
with CODE2SCHEMA_MCP_EVENT_LOG).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
import uuid
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Callable
|
|
19
|
+
|
|
20
|
+
_PROTOCOL_VERSION = "2024-11-05"
|
|
21
|
+
_NOTIFICATIONS = frozenset({"notifications/initialized", "notifications/cancelled"})
|
|
22
|
+
|
|
23
|
+
SERVER_NAME = "code2schema"
|
|
24
|
+
try:
|
|
25
|
+
from importlib.metadata import version as _pkg_version
|
|
26
|
+
|
|
27
|
+
SERVER_VERSION = _pkg_version("code2schema")
|
|
28
|
+
except Exception:
|
|
29
|
+
SERVER_VERSION = "0.0.0"
|
|
30
|
+
|
|
31
|
+
_ZERO_HASH = "0" * 64
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _event_log_path() -> Path:
|
|
35
|
+
override = os.environ.get("CODE2SCHEMA_MCP_EVENT_LOG")
|
|
36
|
+
if override:
|
|
37
|
+
return Path(override)
|
|
38
|
+
state_home = Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local" / "state"))
|
|
39
|
+
return state_home / "code2schema" / "mcp-events.jsonl"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _emit_event(tool: str, status: str, duration_ms: int, detail: str = "") -> None:
|
|
43
|
+
"""Append one hash-chained event; logging failure never breaks a tool call."""
|
|
44
|
+
try:
|
|
45
|
+
path = _event_log_path()
|
|
46
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
prev = _ZERO_HASH
|
|
48
|
+
if path.exists() and path.stat().st_size:
|
|
49
|
+
with path.open("rb") as fh:
|
|
50
|
+
fh.seek(0, os.SEEK_END)
|
|
51
|
+
tail = fh.read(65536).decode("utf-8", errors="replace")
|
|
52
|
+
last = tail.rstrip().rsplit("\n", 1)[-1]
|
|
53
|
+
prev = json.loads(last).get("event_hash", _ZERO_HASH)
|
|
54
|
+
event = {
|
|
55
|
+
"schema": "code2schema.mcp/event/v1",
|
|
56
|
+
"event_id": f"event:{uuid.uuid4().hex[:24]}",
|
|
57
|
+
"ts": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
58
|
+
"actor": "agent:mcp",
|
|
59
|
+
"tool": tool,
|
|
60
|
+
"status": status,
|
|
61
|
+
"duration_ms": duration_ms,
|
|
62
|
+
"detail": detail[:200],
|
|
63
|
+
"prev_hash": prev,
|
|
64
|
+
}
|
|
65
|
+
body = json.dumps(event, sort_keys=True)
|
|
66
|
+
event["event_hash"] = hashlib.sha256(body.encode()).hexdigest()
|
|
67
|
+
with path.open("a", encoding="utf-8") as fh:
|
|
68
|
+
fh.write(json.dumps(event) + "\n")
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _load_graph(path: str, exclude: list[str] | None):
|
|
74
|
+
from code2schema.analyzer.cqrs import analyze
|
|
75
|
+
from code2schema.analyzer.graph import build_rich_graph
|
|
76
|
+
from code2schema.core.extractor import extract_project
|
|
77
|
+
|
|
78
|
+
root = Path(path)
|
|
79
|
+
if not root.exists():
|
|
80
|
+
raise FileNotFoundError(f"Path does not exist: {path}")
|
|
81
|
+
modules = extract_project(root, exclude=exclude or None)
|
|
82
|
+
if not modules:
|
|
83
|
+
raise ValueError(f"No .py files found under: {path}")
|
|
84
|
+
schema = analyze(modules)
|
|
85
|
+
return modules, schema, build_rich_graph(schema)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _tool_analyze(args: dict[str, Any]) -> str:
|
|
89
|
+
from code2schema.codegen import to_json
|
|
90
|
+
|
|
91
|
+
_, schema, _ = _load_graph(args["path"], args.get("exclude"))
|
|
92
|
+
return to_json(schema)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _tool_summary(args: dict[str, Any]) -> str:
|
|
96
|
+
from code2schema.analyzer.events import infer_event_model
|
|
97
|
+
from code2schema.analyzer.graph import detect_cycles, graph_summary
|
|
98
|
+
|
|
99
|
+
modules, schema, G = _load_graph(args["path"], args.get("exclude"))
|
|
100
|
+
cycles = detect_cycles(G)
|
|
101
|
+
result = {
|
|
102
|
+
"modules": len(modules),
|
|
103
|
+
"functions": len(schema.all_functions()),
|
|
104
|
+
"queries": len(schema.queries()),
|
|
105
|
+
"commands": len(schema.commands()),
|
|
106
|
+
"orchestrators": len(schema.orchestrators()),
|
|
107
|
+
"workflows": len(schema.workflows),
|
|
108
|
+
"rules": len(schema.rules),
|
|
109
|
+
"graph_nodes": G.number_of_nodes(),
|
|
110
|
+
"graph_edges": G.number_of_edges(),
|
|
111
|
+
"cycles": len(cycles),
|
|
112
|
+
"graph_summary": graph_summary(G, schema),
|
|
113
|
+
"event_model": infer_event_model(modules).summary(),
|
|
114
|
+
}
|
|
115
|
+
return json.dumps(result, indent=2, default=str)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _tool_cycles(args: dict[str, Any]) -> str:
|
|
119
|
+
from code2schema.analyzer.graph import detect_cycles
|
|
120
|
+
|
|
121
|
+
_, _, G = _load_graph(args["path"], args.get("exclude"))
|
|
122
|
+
return json.dumps({"cycles": detect_cycles(G)}, indent=2)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _tool_proto(args: dict[str, Any]) -> str:
|
|
126
|
+
from code2schema.codegen import to_proto
|
|
127
|
+
|
|
128
|
+
_, schema, _ = _load_graph(args["path"], args.get("exclude"))
|
|
129
|
+
return to_proto(schema)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
_PATH_PROPERTIES = {
|
|
133
|
+
"path": {"type": "string", "description": "Project directory to analyze"},
|
|
134
|
+
"exclude": {
|
|
135
|
+
"type": "array",
|
|
136
|
+
"items": {"type": "string"},
|
|
137
|
+
"description": "Directory/file names to exclude",
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
TOOL_SCHEMAS: dict[str, dict[str, Any]] = {
|
|
142
|
+
"code2schema_analyze": {
|
|
143
|
+
"name": "code2schema_analyze",
|
|
144
|
+
"description": "Compile a Python project to the full CQRS SchemaIR JSON (commands, queries, workflows, rules).",
|
|
145
|
+
"inputSchema": {
|
|
146
|
+
"type": "object",
|
|
147
|
+
"properties": _PATH_PROPERTIES,
|
|
148
|
+
"required": ["path"],
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
"code2schema_summary": {
|
|
152
|
+
"name": "code2schema_summary",
|
|
153
|
+
"description": "Compact analysis summary: module/function/command counts, graph size, cycles, event model.",
|
|
154
|
+
"inputSchema": {
|
|
155
|
+
"type": "object",
|
|
156
|
+
"properties": _PATH_PROPERTIES,
|
|
157
|
+
"required": ["path"],
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
"code2schema_cycles": {
|
|
161
|
+
"name": "code2schema_cycles",
|
|
162
|
+
"description": "Detect dependency cycles in the CQRS graph.",
|
|
163
|
+
"inputSchema": {
|
|
164
|
+
"type": "object",
|
|
165
|
+
"properties": _PATH_PROPERTIES,
|
|
166
|
+
"required": ["path"],
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
"code2schema_proto": {
|
|
170
|
+
"name": "code2schema_proto",
|
|
171
|
+
"description": "Generate proto3 definition from the CQRS schema.",
|
|
172
|
+
"inputSchema": {
|
|
173
|
+
"type": "object",
|
|
174
|
+
"properties": _PATH_PROPERTIES,
|
|
175
|
+
"required": ["path"],
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
TOOL_HANDLERS: dict[str, Callable[[dict[str, Any]], str]] = {
|
|
181
|
+
"code2schema_analyze": _tool_analyze,
|
|
182
|
+
"code2schema_summary": _tool_summary,
|
|
183
|
+
"code2schema_cycles": _tool_cycles,
|
|
184
|
+
"code2schema_proto": _tool_proto,
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _handle_initialize(request_id: Any, params: dict[str, Any] | None) -> dict[str, Any]:
|
|
189
|
+
client_version = (params or {}).get("protocolVersion", _PROTOCOL_VERSION)
|
|
190
|
+
return {
|
|
191
|
+
"jsonrpc": "2.0",
|
|
192
|
+
"id": request_id,
|
|
193
|
+
"result": {
|
|
194
|
+
"protocolVersion": client_version,
|
|
195
|
+
"serverInfo": {"name": SERVER_NAME, "version": SERVER_VERSION},
|
|
196
|
+
"capabilities": {"tools": {}},
|
|
197
|
+
},
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _handle_tools_list(request_id: Any) -> dict[str, Any]:
|
|
202
|
+
tools = [
|
|
203
|
+
{
|
|
204
|
+
"name": schema["name"],
|
|
205
|
+
"description": schema["description"],
|
|
206
|
+
"inputSchema": schema["inputSchema"],
|
|
207
|
+
}
|
|
208
|
+
for schema in TOOL_SCHEMAS.values()
|
|
209
|
+
]
|
|
210
|
+
return {"jsonrpc": "2.0", "id": request_id, "result": {"tools": tools}}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _handle_tools_call(request_id: Any, params: dict[str, Any]) -> dict[str, Any]:
|
|
214
|
+
tool_name = params.get("name")
|
|
215
|
+
arguments = params.get("arguments", {}) or {}
|
|
216
|
+
|
|
217
|
+
if tool_name not in TOOL_HANDLERS:
|
|
218
|
+
return {
|
|
219
|
+
"jsonrpc": "2.0",
|
|
220
|
+
"id": request_id,
|
|
221
|
+
"error": {"code": -32601, "message": f"Tool '{tool_name}' not found"},
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
start = time.monotonic()
|
|
225
|
+
try:
|
|
226
|
+
result = TOOL_HANDLERS[tool_name](arguments)
|
|
227
|
+
_emit_event(tool_name, "ok", int((time.monotonic() - start) * 1000))
|
|
228
|
+
return {
|
|
229
|
+
"jsonrpc": "2.0",
|
|
230
|
+
"id": request_id,
|
|
231
|
+
"result": {"content": [{"type": "text", "text": result}]},
|
|
232
|
+
}
|
|
233
|
+
except Exception as exc:
|
|
234
|
+
_emit_event(
|
|
235
|
+
tool_name, "error", int((time.monotonic() - start) * 1000), str(exc)
|
|
236
|
+
)
|
|
237
|
+
return {
|
|
238
|
+
"jsonrpc": "2.0",
|
|
239
|
+
"id": request_id,
|
|
240
|
+
"error": {"code": -32603, "message": f"Tool execution failed: {exc}"},
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def handle_request(request: dict[str, Any]) -> dict[str, Any]:
|
|
245
|
+
method = request.get("method", "")
|
|
246
|
+
params = request.get("params", {}) or {}
|
|
247
|
+
request_id = request.get("id")
|
|
248
|
+
|
|
249
|
+
if method in _NOTIFICATIONS:
|
|
250
|
+
return {}
|
|
251
|
+
if method == "initialize":
|
|
252
|
+
return _handle_initialize(request_id, params)
|
|
253
|
+
if method == "tools/list":
|
|
254
|
+
return _handle_tools_list(request_id)
|
|
255
|
+
if method == "tools/call":
|
|
256
|
+
return _handle_tools_call(request_id, params)
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
"jsonrpc": "2.0",
|
|
260
|
+
"id": request_id,
|
|
261
|
+
"error": {"code": -32601, "message": f"Method '{method}' not found"},
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def run_server() -> None:
|
|
266
|
+
print("code2schema MCP Server started", file=sys.stderr)
|
|
267
|
+
print(f"Available tools: {', '.join(sorted(TOOL_SCHEMAS))}", file=sys.stderr)
|
|
268
|
+
|
|
269
|
+
for line in sys.stdin:
|
|
270
|
+
line = line.strip()
|
|
271
|
+
if not line:
|
|
272
|
+
continue
|
|
273
|
+
try:
|
|
274
|
+
request = json.loads(line)
|
|
275
|
+
response = handle_request(request)
|
|
276
|
+
if response:
|
|
277
|
+
print(json.dumps(response), flush=True)
|
|
278
|
+
except json.JSONDecodeError as exc:
|
|
279
|
+
print(
|
|
280
|
+
json.dumps(
|
|
281
|
+
{
|
|
282
|
+
"jsonrpc": "2.0",
|
|
283
|
+
"error": {"code": -32700, "message": f"Parse error: {exc}"},
|
|
284
|
+
}
|
|
285
|
+
),
|
|
286
|
+
flush=True,
|
|
287
|
+
)
|
|
288
|
+
except Exception as exc:
|
|
289
|
+
print(
|
|
290
|
+
json.dumps(
|
|
291
|
+
{
|
|
292
|
+
"jsonrpc": "2.0",
|
|
293
|
+
"error": {"code": -32603, "message": f"Internal error: {exc}"},
|
|
294
|
+
}
|
|
295
|
+
),
|
|
296
|
+
flush=True,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def main() -> None:
|
|
301
|
+
run_server()
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
if __name__ == "__main__":
|
|
305
|
+
main()
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code2schema
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: Semantic compiler: Code → AST → CQRS Model → Workflow DAG → Proto/Schema
|
|
5
|
-
License: Apache-2.0
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
6
|
Keywords: ast,cqrs,static-analysis,code-quality,schema
|
|
7
7
|
Classifier: Development Status :: 3 - Alpha
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -24,9 +24,8 @@ Requires-Dist: pytest>=8; extra == "dev"
|
|
|
24
24
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
25
25
|
Requires-Dist: black; extra == "dev"
|
|
26
26
|
Requires-Dist: ruff; extra == "dev"
|
|
27
|
-
Requires-Dist: goal>=2.1.0; extra == "dev"
|
|
28
27
|
Requires-Dist: costs>=0.1.20; extra == "dev"
|
|
29
|
-
Requires-Dist: pfix>=0.1.60; extra == "dev"
|
|
28
|
+
Requires-Dist: pfix>=0.1.60; python_version >= "3.10" and extra == "dev"
|
|
30
29
|
Dynamic: license-file
|
|
31
30
|
|
|
32
31
|
# code2schema
|
|
@@ -34,13 +33,13 @@ Dynamic: license-file
|
|
|
34
33
|
|
|
35
34
|
## AI Cost Tracking
|
|
36
35
|
|
|
37
|
-
    
|
|
37
|
+
  
|
|
39
38
|
|
|
40
|
-
- 🤖 **LLM usage:** $
|
|
41
|
-
- 👤 **Human dev:** ~$
|
|
39
|
+
- 🤖 **LLM usage:** $6.7712 (15 commits)
|
|
40
|
+
- 👤 **Human dev:** ~$1736 (17.4h @ $100/h, 30min dedup)
|
|
42
41
|
|
|
43
|
-
Generated on 2026-
|
|
42
|
+
Generated on 2026-09-13 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
|
|
44
43
|
|
|
45
44
|
---
|
|
46
45
|
|
|
@@ -270,3 +269,6 @@ MIT
|
|
|
270
269
|
## License
|
|
271
270
|
|
|
272
271
|
Licensed under Apache-2.0.
|
|
272
|
+
## Dependency maintenance
|
|
273
|
+
|
|
274
|
+
See [dependency updates and Python tool groups](docs/dependencies.md) for locked tests, daily updates and freshness checks.
|
|
@@ -3,6 +3,7 @@ README.md
|
|
|
3
3
|
pyproject.toml
|
|
4
4
|
code2schema/__init__.py
|
|
5
5
|
code2schema/cli.py
|
|
6
|
+
code2schema/mcp_server.py
|
|
6
7
|
code2schema.egg-info/PKG-INFO
|
|
7
8
|
code2schema.egg-info/SOURCES.txt
|
|
8
9
|
code2schema.egg-info/dependency_links.txt
|
|
@@ -18,4 +19,5 @@ code2schema/codegen/visualizer.py
|
|
|
18
19
|
code2schema/core/__init__.py
|
|
19
20
|
code2schema/core/extractor.py
|
|
20
21
|
code2schema/core/models.py
|
|
21
|
-
tests/test_code2schema.py
|
|
22
|
+
tests/test_code2schema.py
|
|
23
|
+
tests/test_event_handlers.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "code2schema"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.9"
|
|
8
8
|
description = "Semantic compiler: Code → AST → CQRS Model → Workflow DAG → Proto/Schema"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -19,18 +19,19 @@ dependencies = [
|
|
|
19
19
|
"networkx>=3.2",
|
|
20
20
|
"pydantic>=2.5",
|
|
21
21
|
]
|
|
22
|
-
|
|
23
|
-
[project.license]
|
|
24
|
-
text = "Apache-2.0"
|
|
22
|
+
license = "Apache-2.0"
|
|
25
23
|
|
|
26
24
|
[project.optional-dependencies]
|
|
27
25
|
proto = ["grpcio-tools>=1.60"]
|
|
28
26
|
neo4j = ["neo4j>=5.15"]
|
|
29
27
|
viz = ["pyvis>=0.3"]
|
|
30
|
-
dev = [
|
|
31
|
-
"
|
|
28
|
+
dev = [
|
|
29
|
+
"pytest>=8",
|
|
30
|
+
"pytest-cov",
|
|
31
|
+
"black",
|
|
32
|
+
"ruff",
|
|
32
33
|
"costs>=0.1.20",
|
|
33
|
-
"pfix>=0.1.60",
|
|
34
|
+
"pfix>=0.1.60; python_version >= '3.10'",
|
|
34
35
|
]
|
|
35
36
|
|
|
36
37
|
[project.scripts]
|
|
@@ -75,3 +76,9 @@ max_commits = 500
|
|
|
75
76
|
|
|
76
77
|
# Cost thresholds for badge colors (USD)
|
|
77
78
|
badge_color_thresholds = { low = 1.0, medium = 5.0, high = 10.0, critical = 50.0 }
|
|
79
|
+
|
|
80
|
+
[tool.uv.dependency-groups]
|
|
81
|
+
automation = {requires-python = ">=3.12"}
|
|
82
|
+
|
|
83
|
+
[dependency-groups]
|
|
84
|
+
automation = ["goal>=2.2.0"]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
from code2schema.analyzer.events import DomainEvent, _find_handlers
|
|
5
|
+
from code2schema.core.models import FunctionIR, ModuleIR
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_handler_matching_preserves_order_duplicates_case_and_empty_keywords():
|
|
9
|
+
names = ["UserEvent", "UserEvent", "Event", "Entity1Event", "Entity10Event", "ŻółćEvent"]
|
|
10
|
+
events = [DomainEvent(name=n, emitted_by=str(i), handled_by=["existing"]) for i,n in enumerate(names)]
|
|
11
|
+
modules = [ModuleIR(name=m, path=m+".py", functions=[FunctionIR(name=n,module=m) for n in [
|
|
12
|
+
"handle_user", "ON_USER", "handle_entity10", "on_żółć", "plain_user", "consumer_user"]]) for m in ["a","b"]]
|
|
13
|
+
expected = copy.deepcopy(events)
|
|
14
|
+
for _ in range(2):
|
|
15
|
+
for mod in modules:
|
|
16
|
+
for f in mod.functions:
|
|
17
|
+
if re.search(r"(on_|handle_|listener|subscriber|consumer|process_|receive_)", f.name, re.I):
|
|
18
|
+
for ev in expected:
|
|
19
|
+
keyword = ev.name.lower().replace("event", "").strip()
|
|
20
|
+
if keyword and keyword in f.name.lower():
|
|
21
|
+
ev.handled_by.append(f.qualified_name)
|
|
22
|
+
_find_handlers(events, modules)
|
|
23
|
+
assert events == expected
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_empty_events_and_modules_are_noops():
|
|
27
|
+
events = [DomainEvent(name="UserEvent", emitted_by="emit")]
|
|
28
|
+
_find_handlers(events, [])
|
|
29
|
+
_find_handlers([], [])
|
|
30
|
+
assert events[0].handled_by == []
|
|
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
|