codegraph-engine 2.3.0__py3-none-any.whl → 3.0.1__py3-none-any.whl

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 (118) hide show
  1. codegraph/__init__.py +1 -1
  2. codegraph/__main__.py +7 -0
  3. codegraph/agent_brain.py +253 -2
  4. codegraph/agent_capabilities.py +1348 -67
  5. codegraph/agent_rules.py +2 -1
  6. codegraph/api_drift.py +455 -0
  7. codegraph/architecture_rules.py +123 -0
  8. codegraph/async_queue/__init__.py +50 -0
  9. codegraph/async_queue/detectors/__init__.py +48 -0
  10. codegraph/async_queue/detectors/celery.py +160 -0
  11. codegraph/async_queue/detectors/kafka.py +168 -0
  12. codegraph/async_queue/detectors/rabbitmq.py +116 -0
  13. codegraph/async_queue/detectors/redis.py +130 -0
  14. codegraph/async_queue/detectors/sqs.py +135 -0
  15. codegraph/async_queue/models.py +161 -0
  16. codegraph/async_queue/otel_models.py +74 -0
  17. codegraph/async_queue/otel_parser.py +337 -0
  18. codegraph/async_queue/reconciler.py +427 -0
  19. codegraph/async_queue/scanner.py +291 -0
  20. codegraph/async_queue/traversal.py +447 -0
  21. codegraph/change_impact.py +320 -110
  22. codegraph/changelog.py +170 -0
  23. codegraph/cli.py +1605 -62
  24. codegraph/cli_output.py +17 -2
  25. codegraph/context.py +135 -24
  26. codegraph/context_freshness.py +2 -0
  27. codegraph/database/__init__.py +14 -0
  28. codegraph/database/extractor.py +712 -17
  29. codegraph/database/interrogation.py +388 -2
  30. codegraph/database/schema_drift.py +862 -0
  31. codegraph/dataflow_propagation.py +118 -0
  32. codegraph/dead_code.py +387 -0
  33. codegraph/epistemic.py +15 -0
  34. codegraph/evidence_contract.py +34 -3
  35. codegraph/federation.py +245 -0
  36. codegraph/frameworks.py +234 -12
  37. codegraph/frameworks_nestjs.py +502 -0
  38. codegraph/frameworks_nextjs.py +337 -0
  39. codegraph/frameworks_react.py +457 -0
  40. codegraph/freshness.py +2 -0
  41. codegraph/frontend_html.py +109 -0
  42. codegraph/git.py +2 -0
  43. codegraph/git_state.py +2 -0
  44. codegraph/graph/traversal.py +150 -7
  45. codegraph/graph_csr.py +218 -0
  46. codegraph/incremental.py +150 -0
  47. codegraph/indexing/__init__.py +7 -1
  48. codegraph/indexing/classifier.py +5 -0
  49. codegraph/indexing/indexer.py +292 -38
  50. codegraph/indexing/languages/__init__.py +1 -0
  51. codegraph/indexing/languages/cpp.py +457 -0
  52. codegraph/indexing/languages/go.py +459 -0
  53. codegraph/indexing/languages/jvm.py +583 -0
  54. codegraph/indexing/languages/rust.py +507 -0
  55. codegraph/indexing/models.py +15 -1
  56. codegraph/indexing/parser.py +385 -9
  57. codegraph/indexing/pool.py +136 -0
  58. codegraph/indexing/protocols.py +220 -0
  59. codegraph/indexing/scanner.py +23 -0
  60. codegraph/indexing/templates.py +418 -0
  61. codegraph/interrogation.py +207 -39
  62. codegraph/lsp.py +318 -0
  63. codegraph/lsp_bridge/__init__.py +16 -0
  64. codegraph/lsp_bridge/bridge.py +102 -0
  65. codegraph/lsp_bridge/client.py +207 -0
  66. codegraph/lsp_bridge/models.py +71 -0
  67. codegraph/mcp/server.py +502 -29
  68. codegraph/memory/trajectory.py +282 -0
  69. codegraph/models.py +7 -0
  70. codegraph/monorepo.py +3 -0
  71. codegraph/multi_agent/__init__.py +13 -0
  72. codegraph/multi_agent/coordinator.py +229 -0
  73. codegraph/multi_agent/models.py +76 -0
  74. codegraph/optimizer.py +42 -5
  75. codegraph/polymorphism.py +95 -0
  76. codegraph/pr_review.py +296 -0
  77. codegraph/process_lifecycle.py +50 -12
  78. codegraph/reachability.py +198 -0
  79. codegraph/reachability_index.py +74 -0
  80. codegraph/refactor/__init__.py +48 -0
  81. codegraph/refactor/models.py +114 -0
  82. codegraph/refactor/patcher.py +348 -0
  83. codegraph/refactor/renamer.py +844 -0
  84. codegraph/refactor/transactions.py +414 -0
  85. codegraph/repair.py +233 -0
  86. codegraph/resolver.py +1357 -161
  87. codegraph/retrieval_policy.py +22 -1
  88. codegraph/roblox.py +2174 -0
  89. codegraph/route_composer.py +7 -0
  90. codegraph/route_topology.py +172 -0
  91. codegraph/runtime/__init__.py +25 -0
  92. codegraph/runtime/collector.py +155 -0
  93. codegraph/runtime/db_watcher.py +191 -0
  94. codegraph/runtime/ingestor.py +30 -10
  95. codegraph/runtime/otel_receiver.py +208 -0
  96. codegraph/runtime/port_inspector.py +304 -0
  97. codegraph/runtime/runner.py +155 -6
  98. codegraph/runtime/trace_linker.py +175 -0
  99. codegraph/sandbox.py +215 -0
  100. codegraph/search/hybrid.py +26 -0
  101. codegraph/security/supply_chain.py +188 -0
  102. codegraph/security/taint.py +421 -0
  103. codegraph/semantic_decorators.py +34 -3
  104. codegraph/structural_diff.py +2 -0
  105. codegraph/styles.py +144 -0
  106. codegraph/symbol_history.py +2 -0
  107. codegraph/test_gen.py +437 -0
  108. codegraph/tsconfig.py +236 -0
  109. codegraph/ui/__init__.py +6 -0
  110. codegraph/ui/server.py +876 -0
  111. codegraph/watcher.py +323 -0
  112. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/METADATA +365 -67
  113. codegraph_engine-3.0.1.dist-info/RECORD +164 -0
  114. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/WHEEL +1 -1
  115. codegraph_engine-2.3.0.dist-info/RECORD +0 -94
  116. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/entry_points.txt +0 -0
  117. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/licenses/LICENSE +0 -0
  118. {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.1.dist-info}/top_level.txt +0 -0
codegraph/agent_rules.py CHANGED
@@ -238,7 +238,7 @@ Select the smallest targeted tool that answers the question without over-fetchin
238
238
  - **Symbol modification blast radius**: `analyze_impact(symbol=..., max_depth=3)`
239
239
  - **Git commit diff impact**: `get_git_impact(base="HEAD~1", head="HEAD")`
240
240
  - **Architecture & workspace packages**: `get_architecture()`
241
- - **Multi-file debugging / DI / comprehensive task context**: `get_context(query=..., intent="DEBUG" | "TRACE" | "UNDERSTAND" | "IMPACT" | "ARCHITECTURE", max_tokens=4000, max_files=15, max_lines=500)` (`task=...` is also supported as an alias)
241
+ - **Multi-file debugging / DI / comprehensive task context**: `get_context(query=..., intent="DEBUG" | "TRACE" | "UNDERSTAND" | "IMPACT" | "ARCHITECTURE", max_tokens=4000, max_files=25, max_lines=500)` (`task=...` is also supported as an alias)
242
242
 
243
243
  ---
244
244
 
@@ -338,6 +338,7 @@ Synthesize the final answer or apply the code edit:
338
338
  | `STATIC_VERIFIED` | Directly proven by static SQL DDL/DML or migration parsing (`CREATE TABLE`, `INSERT INTO`, `MIGRATES_TABLE`) | Verified Fact (`FACT`) | Cite with SQL/migration file and line coordinates. |
339
339
  | `FRAMEWORK_VERIFIED` | Proven by deterministic framework rules (`HANDLED_BY`, `MOUNTS`, `INJECTS`, `PROVIDES`, `MAPS_TO_TABLE`, `TASK_HANDLER`, `EVENT_LISTENER`) | Verified Fact (`FACT`) | Cite with framework relationship type; never collapse into `CALLS`. |
340
340
  | `DATAFLOW_VERIFIED` | Proven by conservative local/attribute/registry binding (`BINDS_TO`, `RESOLVES_DEPENDENCY`, `REGISTERS`, `DISPATCHES_TO`) | Verified Fact (`FACT`) | Cite with resolved target and binding location. |
341
+ | `ROJO_VERIFIED` | Proven by deterministic Rojo `default.project.json` DataModel-to-filesystem mapping (`REQUIRES_MODULE`) | Verified Fact (`FACT`) | Cite with virtual DataModel path and resolved physical file. |
341
342
  | `RUNTIME_OBSERVED` | Observed in an ingested runtime trace (`observation_count`, `first_seen`, `last_seen`, `runtime_generation`) | Observed Runtime Fact | Cite as runtime observation; never promote into static `AST_VERIFIED` proof. |
342
343
  | `RUNTIME_UNOBSERVED` | Static edge not observed in the ingested runtime trace sample (`NOT_OBSERVED_AT_RUNTIME`) | Unobserved in Sample | Never claim the path is dead code or impossible at runtime. |
343
344
  | `POSSIBLE` | Plausible static candidate (`POSSIBLE_CALLS`, `POSSIBLE_TABLE`, conditional registration, multiple providers) | Candidate Lead (`POSSIBLE`) | Inspect cited lines via `get_file` / `read_file` before making a definitive claim. |
codegraph/api_drift.py ADDED
@@ -0,0 +1,455 @@
1
+ """Production-Grade Cross-Language Client-Server API Contract Drift Detector.
2
+
3
+ Reconciles client-side data fetches (React, Next.js, Vue, Axios, SWR, React-Query)
4
+ against backend route declarations (FastAPI, Flask, Express, Django, NestJS)
5
+ and database models:
6
+ 1. ORPHANED_CLIENT_ROUTE: Client fetch points to a non-existent route (runtime 404).
7
+ 2. HTTP_METHOD_MISMATCH: Client sends POST, but backend endpoint only accepts GET (runtime 405).
8
+ 3. PARAMETER_MISMATCH: Path parameter disparity between client fetch and backend declaration.
9
+ 4. CROSS_LANGUAGE_FIELD_DRIFT: Client TypeScript interface expects a field that backend model/schema lacks.
10
+ 5. NULLABILITY_DRIFT: Client TypeScript interface treats a field as non-nullable, but backend allows NULL/None.
11
+ 6. TYPE_INCOMPATIBILITY: Client TypeScript type conflicts with backend type (e.g., number vs UUID/string).
12
+ 7. UNUSED_BACKEND_ROUTE: Backend endpoint with zero client callers or tests.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import ast
17
+ import json
18
+ import re
19
+ import sqlite3
20
+ from dataclasses import dataclass
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ from codegraph.database.schema_drift import extract_schema_fields_from_class_node
25
+ from codegraph.route_topology import (
26
+ match_route_topology,
27
+ normalize_route,
28
+ )
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class ApiContractDrift:
33
+ kind: str # ORPHANED_CLIENT_ROUTE | HTTP_METHOD_MISMATCH | PARAMETER_MISMATCH | CROSS_LANGUAGE_FIELD_DRIFT | NULLABILITY_DRIFT | TYPE_INCOMPATIBILITY | UNUSED_BACKEND_ROUTE
34
+ client_route: str
35
+ client_method: str = ""
36
+ client_file: str = ""
37
+ client_line: int = 1
38
+ client_type: str = ""
39
+ backend_route: str = ""
40
+ backend_methods: tuple[str, ...] = ()
41
+ backend_handler: str = ""
42
+ backend_file: str = ""
43
+ field_name: str = ""
44
+ client_field_type: str = ""
45
+ backend_field_type: str = ""
46
+ severity: str = "CRITICAL" # CRITICAL | WARNING | INFO
47
+ message: str = ""
48
+
49
+ def as_dict(self) -> dict[str, Any]:
50
+ return {
51
+ "kind": self.kind,
52
+ "client_route": self.client_route,
53
+ "client_method": self.client_method,
54
+ "client_file": self.client_file,
55
+ "client_line": self.client_line,
56
+ "client_type": self.client_type,
57
+ "backend_route": self.backend_route,
58
+ "backend_methods": list(self.backend_methods),
59
+ "backend_handler": self.backend_handler,
60
+ "backend_file": self.backend_file,
61
+ "field_name": self.field_name,
62
+ "client_field_type": self.client_field_type,
63
+ "backend_field_type": self.backend_field_type,
64
+ "severity": self.severity,
65
+ "message": self.message,
66
+ }
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class ApiContractDriftReport:
71
+ has_drift: bool
72
+ total_client_calls: int
73
+ total_backend_routes: int
74
+ matched_calls: int = 0
75
+ drifts: tuple[ApiContractDrift, ...] = ()
76
+
77
+ def as_dict(self) -> dict[str, Any]:
78
+ return {
79
+ "has_drift": self.has_drift,
80
+ "total_client_calls": self.total_client_calls,
81
+ "total_backend_routes": self.total_backend_routes,
82
+ "matched_calls": self.matched_calls,
83
+ "drift_count": len(self.drifts),
84
+ "drifts": [d.as_dict() for d in self.drifts],
85
+ }
86
+
87
+
88
+ _TS_TO_BACKEND_TYPES: dict[str, set[str]] = {
89
+ "string": {
90
+ "str", "varchar", "text", "char", "character varying", "string",
91
+ "citext", "charfield", "emailfield", "uuid", "uuidfield", "date",
92
+ "datetime", "timestamp",
93
+ },
94
+ "number": {
95
+ "int", "float", "integer", "bigint", "smallint", "serial",
96
+ "bigserial", "tinyint", "double", "real", "numeric", "decimal",
97
+ "integerfield", "floatfield", "decimalfield",
98
+ },
99
+ "boolean": {"bool", "boolean", "tinyint", "booleanfield"},
100
+ "date": {"date", "datetime", "timestamp", "timestamptz", "datefield", "datetimefield"},
101
+ "any": set(),
102
+ }
103
+
104
+
105
+ def _are_ts_backend_types_compatible(ts_type: str, backend_type: str) -> bool:
106
+ clean_ts = ts_type.strip().rstrip("?").lower()
107
+ clean_be = backend_type.strip().lower()
108
+ if clean_ts in ("any", "unknown", "object") or clean_be in ("any", "unknown", "object"):
109
+ return True
110
+ if clean_ts == clean_be:
111
+ return True
112
+ compatible_be_types = _TS_TO_BACKEND_TYPES.get(clean_ts)
113
+ if compatible_be_types is None:
114
+ return True
115
+ for be_allowed in compatible_be_types:
116
+ if be_allowed in clean_be or clean_be in be_allowed:
117
+ return True
118
+ return False
119
+
120
+
121
+ def _resolve_backend_fields_for_handler(
122
+ con: sqlite3.Connection,
123
+ repo_root: Path | None,
124
+ handler_canonical_id: str,
125
+ handler_name: str,
126
+ backend_file: str,
127
+ ) -> dict[str, tuple[str, bool]]:
128
+ """Discover schema fields (type_name, is_nullable) for a backend handler.
129
+
130
+ Checks:
131
+ 1. Python AST Pydantic / dataclass response model or return annotation.
132
+ 2. Python AST function parameter input schemas.
133
+ 3. Database columns from db_entities or db_queries.
134
+ """
135
+ fields: dict[str, tuple[str, bool]] = {}
136
+
137
+ # 1. AST inspection if file exists
138
+ if repo_root and backend_file:
139
+ full_path = repo_root / backend_file
140
+ if full_path.exists() and backend_file.endswith(".py"):
141
+ try:
142
+ tree = ast.parse(full_path.read_text(encoding="utf-8", errors="replace"), filename=str(full_path))
143
+ handler_fn: ast.FunctionDef | ast.AsyncFunctionDef | None = None
144
+ for node in ast.walk(tree):
145
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == handler_name:
146
+ handler_fn = node
147
+ break
148
+
149
+ if handler_fn:
150
+ target_model_name: str | None = None
151
+ # Check decorators for response_model=ModelName
152
+ for dec in handler_fn.decorator_list:
153
+ if isinstance(dec, ast.Call):
154
+ for kw in dec.keywords:
155
+ if kw.arg == "response_model":
156
+ try:
157
+ target_model_name = ast.unparse(kw.value).strip()
158
+ except Exception:
159
+ pass
160
+
161
+ # Check return annotation -> ModelName
162
+ if not target_model_name and handler_fn.returns:
163
+ try:
164
+ ret_str = ast.unparse(handler_fn.returns).strip()
165
+ ret_str = ret_str.replace("list[", "").replace("List[", "").replace("]", "")
166
+ ret_str = ret_str.replace("Optional[", "").replace("]", "").split("|")[0].strip()
167
+ target_model_name = ret_str
168
+ except Exception:
169
+ pass
170
+
171
+ # Check parameter input schema
172
+ if not target_model_name:
173
+ excluded = {"request", "db", "session", "self", "cls"}
174
+ for arg in handler_fn.args.args:
175
+ if arg.arg not in excluded and arg.annotation:
176
+ try:
177
+ ann_str = ast.unparse(arg.annotation).strip()
178
+ target_model_name = ann_str.replace("Optional[", "").replace("]", "").split("|")[0].strip()
179
+ break
180
+ except Exception:
181
+ pass
182
+
183
+ if target_model_name:
184
+ # Find class definition
185
+ for c_node in ast.walk(tree):
186
+ if isinstance(c_node, ast.ClassDef) and c_node.name == target_model_name:
187
+ specs = extract_schema_fields_from_class_node(c_node, str(full_path))
188
+ for s in specs:
189
+ fields[s.name] = (s.type_name, s.nullable)
190
+ break
191
+ except Exception:
192
+ pass
193
+
194
+ # 2. Database columns from db_entities if no AST model found
195
+ if not fields:
196
+ # Check if queries exist for this handler
197
+ q_rows = con.execute(
198
+ "SELECT table_name FROM db_queries WHERE caller_symbol_id = ? OR caller_symbol_id LIKE ?",
199
+ (handler_canonical_id, f"%{handler_name}"),
200
+ ).fetchall()
201
+ tables = [str(r[0]) for r in q_rows if r[0]]
202
+ if tables:
203
+ placeholders = ",".join("?" for _ in tables)
204
+ col_rows = con.execute(
205
+ f"SELECT column_name, data_type, nullable FROM db_entities WHERE kind = 'column' AND table_name IN ({placeholders})",
206
+ tables,
207
+ ).fetchall()
208
+ for cr in col_rows:
209
+ c_name = str(cr[0])
210
+ c_type = str(cr[1] or "text")
211
+ c_null = bool(cr[2])
212
+ fields[c_name] = (c_type, c_null)
213
+
214
+ return fields
215
+
216
+
217
+ def detect_api_contract_drift(
218
+ con: sqlite3.Connection,
219
+ repo_root: Path | None = None,
220
+ ) -> ApiContractDriftReport:
221
+ """Analyze client data fetches vs backend route registrations to find contract mismatches."""
222
+ # 1. Fetch backend routes
223
+ route_table = "framework_routes"
224
+ has_fw_routes = con.execute(
225
+ "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='framework_routes'"
226
+ ).fetchone()[0] > 0
227
+ if not has_fw_routes:
228
+ route_table = "raw_framework_routes"
229
+
230
+ backend_routes_rows = con.execute(
231
+ f"SELECT endpoint_id, framework, http_method, route_path, normalized_route, handler_name, handler_canonical_id, file_path "
232
+ f"FROM {route_table}"
233
+ ).fetchall()
234
+
235
+ backend_routes: list[dict[str, Any]] = []
236
+ for r in backend_routes_rows:
237
+ raw_p = str(r["route_path"] or "")
238
+ method = str(r["http_method"] or "GET").upper()
239
+ norm_route = normalize_route(method, raw_p)
240
+ backend_routes.append({
241
+ "endpoint_id": str(r["endpoint_id"]),
242
+ "framework": str(r["framework"]),
243
+ "http_method": method,
244
+ "route_path": raw_p,
245
+ "normalized_route": norm_route,
246
+ "handler_name": str(r["handler_name"] or ""),
247
+ "handler_canonical_id": str(r["handler_canonical_id"] or ""),
248
+ "file_path": str(r["file_path"] or ""),
249
+ })
250
+
251
+ # 2. Fetch TypeScript interfaces from local_bindings
252
+ ts_interfaces: dict[str, list[tuple[str, str]]] = {}
253
+ if con.execute(
254
+ "SELECT count(*) FROM sqlite_master WHERE type='table' AND name='local_bindings'"
255
+ ).fetchone()[0] > 0:
256
+ ts_rows = con.execute(
257
+ "SELECT target_name, dict_entries_json FROM local_bindings WHERE expr_kind = 'TS_INTERFACE'"
258
+ ).fetchall()
259
+ for tr in ts_rows:
260
+ iname = str(tr[0])
261
+ try:
262
+ entries = json.loads(str(tr[1] or "[]"))
263
+ ts_interfaces[iname] = [(str(e[0]), str(e[1])) for e in entries if len(e) >= 2]
264
+ except Exception:
265
+ pass
266
+
267
+ # 3. Fetch client fetch calls from local_bindings
268
+ client_calls_rows = con.execute(
269
+ "SELECT target_name, file_path, line, source_expr, attr_name, base_expr, scope "
270
+ "FROM local_bindings "
271
+ "WHERE expr_kind IN ('REACT_FETCH_ROUTE', 'TEST_ROUTE', 'HTML_FORM_ACTION', 'HTMX_REQUEST')"
272
+ ).fetchall()
273
+
274
+ drifts: list[ApiContractDrift] = []
275
+ matched_backend_endpoints: set[str] = set()
276
+ total_matched_calls = 0
277
+
278
+ for r in client_calls_rows:
279
+ raw_client_url = str(r["target_name"] or "")
280
+ if not raw_client_url.startswith("/") and not raw_client_url.startswith("http"):
281
+ continue
282
+
283
+ if raw_client_url.startswith("http"):
284
+ m_path = re.search(r"https?://[^/]+(/[^?#]*)", raw_client_url)
285
+ client_path = m_path.group(1) if m_path else raw_client_url
286
+ else:
287
+ client_path = raw_client_url
288
+
289
+ client_file = str(r["file_path"] or "")
290
+ client_line = int(r["line"] or 1)
291
+ raw_m = str(r["base_expr"] or "").upper()
292
+ if raw_m in ("GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"):
293
+ client_method = raw_m
294
+ else:
295
+ client_method = "GET"
296
+
297
+ resp_type = str(r["attr_name"] or "").strip()
298
+ client_norm = normalize_route(client_method, client_path)
299
+
300
+ # Check topological matches
301
+ exact_matches: list[dict[str, Any]] = []
302
+ path_matches_wrong_method: list[dict[str, Any]] = []
303
+
304
+ for b in backend_routes:
305
+ matched, _ = match_route_topology(client_norm, b["normalized_route"], allow_proxy_prefix=True, match_methods=True)
306
+ if matched:
307
+ exact_matches.append(b)
308
+ else:
309
+ p_matched, _ = match_route_topology(client_norm, b["normalized_route"], allow_proxy_prefix=True, match_methods=False)
310
+ if p_matched:
311
+ path_matches_wrong_method.append(b)
312
+
313
+ if exact_matches:
314
+ total_matched_calls += 1
315
+ for em in exact_matches:
316
+ matched_backend_endpoints.add(em["endpoint_id"])
317
+
318
+ primary_backend = exact_matches[0]
319
+
320
+ # 4. Cross-Language TypeScript Interface <-> Backend Contract Verification
321
+ if resp_type and resp_type in ts_interfaces:
322
+ ts_fields = ts_interfaces[resp_type]
323
+ backend_fields = _resolve_backend_fields_for_handler(
324
+ con,
325
+ repo_root,
326
+ primary_backend["handler_canonical_id"],
327
+ primary_backend["handler_name"],
328
+ primary_backend["file_path"],
329
+ )
330
+
331
+ if backend_fields:
332
+ for f_name, f_type_str in ts_fields:
333
+ is_opt_client = f_type_str.endswith("?") or "null" in f_type_str or "undefined" in f_type_str
334
+ clean_ts_type = f_type_str.rstrip("?").strip()
335
+
336
+ # Check 1: Missing field in backend
337
+ if f_name not in backend_fields:
338
+ if not is_opt_client:
339
+ drifts.append(
340
+ ApiContractDrift(
341
+ kind="CROSS_LANGUAGE_FIELD_DRIFT",
342
+ client_route=client_path,
343
+ client_method=client_method,
344
+ client_file=client_file,
345
+ client_line=client_line,
346
+ client_type=resp_type,
347
+ backend_route=primary_backend["route_path"],
348
+ backend_methods=(primary_backend["http_method"],),
349
+ backend_handler=primary_backend["handler_name"],
350
+ backend_file=primary_backend["file_path"],
351
+ field_name=f_name,
352
+ client_field_type=f_type_str,
353
+ severity="CRITICAL",
354
+ message=(
355
+ f"Client TypeScript interface '{resp_type}' expects required field '{f_name}: {f_type_str}', "
356
+ f"but backend route '{primary_backend['route_path']}' ({primary_backend['handler_name']}) does not provide this field."
357
+ ),
358
+ )
359
+ )
360
+ else:
361
+ be_type, be_nullable = backend_fields[f_name]
362
+
363
+ # Check 2: Nullability Drift
364
+ if not is_opt_client and be_nullable:
365
+ drifts.append(
366
+ ApiContractDrift(
367
+ kind="NULLABILITY_DRIFT",
368
+ client_route=client_path,
369
+ client_method=client_method,
370
+ client_file=client_file,
371
+ client_line=client_line,
372
+ client_type=resp_type,
373
+ backend_route=primary_backend["route_path"],
374
+ backend_methods=(primary_backend["http_method"],),
375
+ backend_handler=primary_backend["handler_name"],
376
+ backend_file=primary_backend["file_path"],
377
+ field_name=f_name,
378
+ client_field_type=f_type_str,
379
+ backend_field_type=f"{be_type} (nullable)",
380
+ severity="WARNING",
381
+ message=(
382
+ f"Client TypeScript interface '{resp_type}' expects '{f_name}' to be non-nullable, "
383
+ f"but backend schema/column allows NULL/None."
384
+ ),
385
+ )
386
+ )
387
+
388
+ # Check 3: Type Incompatibility
389
+ if not _are_ts_backend_types_compatible(clean_ts_type, be_type):
390
+ drifts.append(
391
+ ApiContractDrift(
392
+ kind="TYPE_INCOMPATIBILITY",
393
+ client_route=client_path,
394
+ client_method=client_method,
395
+ client_file=client_file,
396
+ client_line=client_line,
397
+ client_type=resp_type,
398
+ backend_route=primary_backend["route_path"],
399
+ backend_methods=(primary_backend["http_method"],),
400
+ backend_handler=primary_backend["handler_name"],
401
+ backend_file=primary_backend["file_path"],
402
+ field_name=f_name,
403
+ client_field_type=f_type_str,
404
+ backend_field_type=be_type,
405
+ severity="CRITICAL",
406
+ message=(
407
+ f"Type mismatch on field '{f_name}': client expects '{f_type_str}', "
408
+ f"but backend returns '{be_type}'."
409
+ ),
410
+ )
411
+ )
412
+
413
+ elif path_matches_wrong_method:
414
+ first_m = path_matches_wrong_method[0]
415
+ allowed_methods = tuple(sorted({m["http_method"] for m in path_matches_wrong_method}))
416
+ drifts.append(
417
+ ApiContractDrift(
418
+ kind="HTTP_METHOD_MISMATCH",
419
+ client_route=client_path,
420
+ client_method=client_method,
421
+ client_file=client_file,
422
+ client_line=client_line,
423
+ backend_route=first_m["route_path"],
424
+ backend_methods=allowed_methods,
425
+ backend_handler=first_m["handler_name"],
426
+ backend_file=first_m["file_path"],
427
+ severity="CRITICAL",
428
+ message=(
429
+ f"Client sends '{client_method} {client_path}' at {client_file}:{client_line}, "
430
+ f"but backend handler '{first_m['handler_name']}' only accepts {list(allowed_methods)}."
431
+ ),
432
+ )
433
+ )
434
+ else:
435
+ # Check if this looks like an API route worthy of reporting as orphaned
436
+ if client_path.startswith("/api/") or client_path.startswith("/v1/") or client_path.startswith("/v2/") or "/api" in client_path:
437
+ drifts.append(
438
+ ApiContractDrift(
439
+ kind="ORPHANED_CLIENT_ROUTE",
440
+ client_route=client_path,
441
+ client_method=client_method,
442
+ client_file=client_file,
443
+ client_line=client_line,
444
+ severity="CRITICAL",
445
+ message=f"Client calls '{client_path}' at {client_file}:{client_line}, but no backend route handler is registered.",
446
+ )
447
+ )
448
+
449
+ return ApiContractDriftReport(
450
+ has_drift=len(drifts) > 0,
451
+ total_client_calls=len(client_calls_rows),
452
+ total_backend_routes=len(backend_routes_rows),
453
+ matched_calls=total_matched_calls,
454
+ drifts=tuple(drifts),
455
+ )
@@ -0,0 +1,123 @@
1
+ """Declarative Architecture Invariant Engine for CodeGraph.
2
+
3
+ Enforces architectural rules, layered clean architecture boundaries, and forbidden dependencies:
4
+ - Reads declarative architectural rules from .codegraph/rules.yaml
5
+ - Checks forbidden import edges (e.g. controllers importing database tables directly)
6
+ - Enforces layer isolation (domain layer cannot import infrastructure or UI)
7
+ - Enforces naming conventions and required interface implementations
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import fnmatch
12
+ import json
13
+ import sqlite3
14
+ from dataclasses import asdict, dataclass, field
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class RuleViolation:
21
+ """An architectural invariant violation."""
22
+
23
+ rule_name: str
24
+ source_file: str
25
+ line: int
26
+ target_entity: str
27
+ severity: str # ERROR | WARNING
28
+ message: str
29
+
30
+ def as_dict(self) -> dict[str, Any]:
31
+ return asdict(self)
32
+
33
+
34
+ @dataclass
35
+ class ArchitectureComplianceReport:
36
+ """Consolidated compliance report across all architectural rules."""
37
+
38
+ workspace: str
39
+ status: str # COMPLIANT | VIOLATIONS_DETECTED
40
+ total_rules_checked: int
41
+ violations_count: int
42
+ violations: list[RuleViolation] = field(default_factory=list)
43
+
44
+ def as_dict(self) -> dict[str, Any]:
45
+ return {
46
+ "workspace": self.workspace,
47
+ "status": self.status,
48
+ "total_rules_checked": self.total_rules_checked,
49
+ "violations_count": self.violations_count,
50
+ "violations": [v.as_dict() for v in self.violations],
51
+ }
52
+
53
+
54
+ def evaluate_architecture_rules(
55
+ repository: Path,
56
+ con: sqlite3.Connection,
57
+ rules: list[dict[str, Any]] | None = None,
58
+ ) -> ArchitectureComplianceReport:
59
+ """Evaluate architectural boundaries against the repository graph index."""
60
+ repo = repository.resolve()
61
+ violations: list[RuleViolation] = []
62
+
63
+ # If rules not passed, load from .codegraph/rules.yaml or rules.json
64
+ active_rules: list[dict[str, Any]] = rules or []
65
+ if not active_rules:
66
+ rule_file_json = repo / ".codegraph" / "rules.json"
67
+ if rule_file_json.is_file():
68
+ try:
69
+ active_rules = json.loads(rule_file_json.read_text(encoding="utf-8"))
70
+ except Exception:
71
+ pass
72
+
73
+ # Default starter rules if none defined
74
+ if not active_rules:
75
+ active_rules = [
76
+ {
77
+ "name": "prevent-controller-to-db-leakage",
78
+ "source_pattern": "*controller*.*",
79
+ "cannot_import": ["*models*", "*db*", "*entities*"],
80
+ "message": "Controller must not import database models directly; route through services.",
81
+ }
82
+ ]
83
+
84
+ # Query all imports from database
85
+ cur = con.cursor()
86
+ cur.execute("PRAGMA table_info(imports);")
87
+ imp_cols = {col[1] for col in cur.fetchall()}
88
+ src_col = "source_path" if "source_path" in imp_cols else "source_file"
89
+ cur.execute(f"SELECT {src_col}, module, name, line FROM imports;")
90
+ all_imports = cur.fetchall()
91
+
92
+ for r in active_rules:
93
+ name = r.get("name", "unnamed-rule")
94
+ src_pat = r.get("source_pattern", "*")
95
+ cannot_import = r.get("cannot_import", [])
96
+ msg = r.get("message", f"Rule '{name}' violated.")
97
+
98
+ for imp_source, imp_module, imp_name, imp_line in all_imports:
99
+ # Check if file matches source pattern
100
+ if fnmatch.fnmatch(imp_source, src_pat) or fnmatch.fnmatch(Path(imp_source).name, src_pat):
101
+ # Check forbidden targets
102
+ full_import_target = f"{imp_module}.{imp_name}" if imp_name else imp_module
103
+ for forbidden in cannot_import:
104
+ if fnmatch.fnmatch(full_import_target, forbidden) or fnmatch.fnmatch(imp_module, forbidden):
105
+ violations.append(
106
+ RuleViolation(
107
+ rule_name=name,
108
+ source_file=imp_source,
109
+ line=imp_line,
110
+ target_entity=full_import_target,
111
+ severity="ERROR",
112
+ message=msg,
113
+ )
114
+ )
115
+
116
+ status = "COMPLIANT" if not violations else "VIOLATIONS_DETECTED"
117
+ return ArchitectureComplianceReport(
118
+ workspace=str(repo),
119
+ status=status,
120
+ total_rules_checked=len(active_rules),
121
+ violations_count=len(violations),
122
+ violations=violations,
123
+ )
@@ -0,0 +1,50 @@
1
+ """Asynchronous message queue and cross-service distributed task intelligence.
2
+
3
+ Provides static detection, queue linking, and OpenTelemetry distributed tracing
4
+ reconciliation for Kafka, Celery, AWS SQS, RabbitMQ, Redis Pub/Sub, and Streams.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from codegraph.async_queue.models import (
9
+ AsyncConsumerRecord,
10
+ AsyncMessagingSystem,
11
+ AsyncProducerRecord,
12
+ AsyncQueueEntity,
13
+ AsyncQueueEvidenceClass,
14
+ AsyncQueueLink,
15
+ )
16
+ from codegraph.async_queue.otel_models import (
17
+ OTelEvidenceStatus,
18
+ OTelSpanRecord,
19
+ TraceReconciliationResult,
20
+ )
21
+ from codegraph.async_queue.otel_parser import parse_otel_spans
22
+ from codegraph.async_queue.reconciler import OTelTraceReconciler, ensure_otel_tables
23
+ from codegraph.async_queue.scanner import AsyncQueueScanner, ensure_async_tables
24
+ from codegraph.async_queue.traversal import (
25
+ find_queue_consumers,
26
+ find_queue_producers,
27
+ list_async_queues,
28
+ trace_async_flow,
29
+ )
30
+
31
+ __all__ = [
32
+ "AsyncConsumerRecord",
33
+ "AsyncMessagingSystem",
34
+ "AsyncProducerRecord",
35
+ "AsyncQueueEntity",
36
+ "AsyncQueueEvidenceClass",
37
+ "AsyncQueueLink",
38
+ "AsyncQueueScanner",
39
+ "OTelEvidenceStatus",
40
+ "OTelSpanRecord",
41
+ "OTelTraceReconciler",
42
+ "TraceReconciliationResult",
43
+ "ensure_async_tables",
44
+ "ensure_otel_tables",
45
+ "find_queue_consumers",
46
+ "find_queue_producers",
47
+ "list_async_queues",
48
+ "parse_otel_spans",
49
+ "trace_async_flow",
50
+ ]