code2schema 0.1.8__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.
Files changed (25) hide show
  1. {code2schema-0.1.8 → code2schema-0.1.9}/PKG-INFO +11 -7
  2. {code2schema-0.1.8 → code2schema-0.1.9}/README.md +8 -3
  3. {code2schema-0.1.8 → code2schema-0.1.9}/code2schema/__init__.py +1 -1
  4. {code2schema-0.1.8 → code2schema-0.1.9}/code2schema/analyzer/events.py +5 -3
  5. {code2schema-0.1.8 → code2schema-0.1.9}/code2schema/analyzer/graph.py +11 -2
  6. code2schema-0.1.9/code2schema/mcp_server.py +305 -0
  7. {code2schema-0.1.8 → code2schema-0.1.9}/code2schema.egg-info/PKG-INFO +11 -7
  8. {code2schema-0.1.8 → code2schema-0.1.9}/code2schema.egg-info/SOURCES.txt +3 -1
  9. {code2schema-0.1.8 → code2schema-0.1.9}/code2schema.egg-info/requires.txt +2 -1
  10. {code2schema-0.1.8 → code2schema-0.1.9}/pyproject.toml +14 -7
  11. code2schema-0.1.9/tests/test_event_handlers.py +30 -0
  12. {code2schema-0.1.8 → code2schema-0.1.9}/LICENSE +0 -0
  13. {code2schema-0.1.8 → code2schema-0.1.9}/code2schema/analyzer/__init__.py +0 -0
  14. {code2schema-0.1.8 → code2schema-0.1.9}/code2schema/analyzer/cqrs.py +0 -0
  15. {code2schema-0.1.8 → code2schema-0.1.9}/code2schema/cli.py +0 -0
  16. {code2schema-0.1.8 → code2schema-0.1.9}/code2schema/codegen/__init__.py +0 -0
  17. {code2schema-0.1.8 → code2schema-0.1.9}/code2schema/codegen/visualizer.py +0 -0
  18. {code2schema-0.1.8 → code2schema-0.1.9}/code2schema/core/__init__.py +0 -0
  19. {code2schema-0.1.8 → code2schema-0.1.9}/code2schema/core/extractor.py +0 -0
  20. {code2schema-0.1.8 → code2schema-0.1.9}/code2schema/core/models.py +0 -0
  21. {code2schema-0.1.8 → code2schema-0.1.9}/code2schema.egg-info/dependency_links.txt +0 -0
  22. {code2schema-0.1.8 → code2schema-0.1.9}/code2schema.egg-info/entry_points.txt +0 -0
  23. {code2schema-0.1.8 → code2schema-0.1.9}/code2schema.egg-info/top_level.txt +0 -0
  24. {code2schema-0.1.8 → code2schema-0.1.9}/setup.cfg +0 -0
  25. {code2schema-0.1.8 → 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.8
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,11 +33,13 @@ Dynamic: license-file
34
33
 
35
34
  ## AI Cost Tracking
36
35
 
37
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$1.05-green) ![AI Model](https://img.shields.io/badge/AI%20Model-openrouter%2Fqwen%2Fqwen3-coder-next-lightgrey)
36
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.9-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
37
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$6.77-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-17.4h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
38
38
 
39
- This project uses AI-generated code. Total cost: **$1.0500** with **7** AI commits.
39
+ - 🤖 **LLM usage:** $6.7712 (15 commits)
40
+ - 👤 **Human dev:** ~$1736 (17.4h @ $100/h, 30min dedup)
40
41
 
41
- Generated on 2026-06-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/models/openrouter/qwen/qwen3-coder-next)
42
+ Generated on 2026-09-13 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
42
43
 
43
44
  ---
44
45
 
@@ -268,3 +269,6 @@ MIT
268
269
  ## License
269
270
 
270
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,11 +3,13 @@
3
3
 
4
4
  ## AI Cost Tracking
5
5
 
6
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$1.05-green) ![AI Model](https://img.shields.io/badge/AI%20Model-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.9-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-$6.77-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-17.4h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
7
8
 
8
- This project uses AI-generated code. Total cost: **$1.0500** with **7** AI commits.
9
+ - 🤖 **LLM usage:** $6.7712 (15 commits)
10
+ - 👤 **Human dev:** ~$1736 (17.4h @ $100/h, 30min dedup)
9
11
 
10
- Generated on 2026-06-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/models/openrouter/qwen/qwen3-coder-next)
12
+ Generated on 2026-09-13 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
11
13
 
12
14
  ---
13
15
 
@@ -237,3 +239,6 @@ MIT
237
239
  ## License
238
240
 
239
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.
@@ -13,4 +13,4 @@ __all__ = [
13
13
  "extract_module",
14
14
  "analyze",
15
15
  ]
16
- __version__ = "0.1.8"
16
+ __version__ = "0.1.9"
@@ -89,12 +89,14 @@ def _find_emitters(modules: List[ModuleIR]) -> List[DomainEvent]:
89
89
 
90
90
  def _find_handlers(events: List[DomainEvent], modules: List[ModuleIR]) -> None:
91
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]
92
94
  for mod in modules:
93
95
  for f in mod.functions:
94
96
  if _EVENT_HANDLER_PATTERNS.search(f.name):
95
- for ev in events:
96
- keyword = ev.name.lower().replace("event", "").strip()
97
- if keyword and keyword in f.name.lower():
97
+ handler_name = f.name.lower()
98
+ for ev, keyword in keywords:
99
+ if keyword and keyword in handler_name:
98
100
  ev.handled_by.append(f.qualified_name)
99
101
 
100
102
 
@@ -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
- pr = nx.pagerank(G, alpha=0.85)
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
 
@@ -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.8
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,11 +33,13 @@ Dynamic: license-file
34
33
 
35
34
  ## AI Cost Tracking
36
35
 
37
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$1.05-green) ![AI Model](https://img.shields.io/badge/AI%20Model-openrouter%2Fqwen%2Fqwen3-coder-next-lightgrey)
36
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.1.9-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
37
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$6.77-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-17.4h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
38
38
 
39
- This project uses AI-generated code. Total cost: **$1.0500** with **7** AI commits.
39
+ - 🤖 **LLM usage:** $6.7712 (15 commits)
40
+ - 👤 **Human dev:** ~$1736 (17.4h @ $100/h, 30min dedup)
40
41
 
41
- Generated on 2026-06-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/models/openrouter/qwen/qwen3-coder-next)
42
+ Generated on 2026-09-13 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
42
43
 
43
44
  ---
44
45
 
@@ -268,3 +269,6 @@ MIT
268
269
  ## License
269
270
 
270
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
@@ -7,8 +7,9 @@ pytest>=8
7
7
  pytest-cov
8
8
  black
9
9
  ruff
10
- goal>=2.1.0
11
10
  costs>=0.1.20
11
+
12
+ [dev:python_version >= "3.10"]
12
13
  pfix>=0.1.60
13
14
 
14
15
  [neo4j]
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "code2schema"
7
- version = "0.1.8"
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 = ["pytest>=8", "pytest-cov", "black", "ruff",
31
- "goal>=2.1.0",
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