cg-code-graph 0.10.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 (174) hide show
  1. cg_code_graph-0.10.1.dist-info/METADATA +678 -0
  2. cg_code_graph-0.10.1.dist-info/RECORD +174 -0
  3. cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
  4. cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
  5. cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
  6. cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
  7. codegraph/__init__.py +2 -0
  8. codegraph/aitools.py +129 -0
  9. codegraph/apps.py +76 -0
  10. codegraph/blindspots.py +428 -0
  11. codegraph/bridges.py +1701 -0
  12. codegraph/cli.py +725 -0
  13. codegraph/concepts.py +362 -0
  14. codegraph/config.py +559 -0
  15. codegraph/core/__init__.py +0 -0
  16. codegraph/core/cache.py +375 -0
  17. codegraph/core/detect.py +80 -0
  18. codegraph/core/extractors.py +187 -0
  19. codegraph/core/fsutil.py +61 -0
  20. codegraph/core/generated.py +575 -0
  21. codegraph/core/model.py +174 -0
  22. codegraph/core/paths.py +175 -0
  23. codegraph/core/plugin.py +160 -0
  24. codegraph/core/store.py +80 -0
  25. codegraph/core/syntax_errors.py +132 -0
  26. codegraph/coverage.py +928 -0
  27. codegraph/doctor.py +453 -0
  28. codegraph/external.py +613 -0
  29. codegraph/indexer.py +336 -0
  30. codegraph/link.py +434 -0
  31. codegraph/lint_async.py +524 -0
  32. codegraph/mcp_server.py +1303 -0
  33. codegraph/parity.py +473 -0
  34. codegraph/parity_structure.py +307 -0
  35. codegraph/payload.py +321 -0
  36. codegraph/plans.py +1285 -0
  37. codegraph/platform_scan.py +643 -0
  38. codegraph/platforms.py +1369 -0
  39. codegraph/plugins/__init__.py +0 -0
  40. codegraph/plugins/cfamily/__init__.py +0 -0
  41. codegraph/plugins/cfamily/plugin.py +930 -0
  42. codegraph/plugins/cfamily/syntax.py +881 -0
  43. codegraph/plugins/dart/__init__.py +0 -0
  44. codegraph/plugins/dart/bridges.py +345 -0
  45. codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
  46. codegraph/plugins/dart/extractor/pubspec.lock +149 -0
  47. codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
  48. codegraph/plugins/dart/http.py +904 -0
  49. codegraph/plugins/dart/models.py +308 -0
  50. codegraph/plugins/dart/plugin.py +625 -0
  51. codegraph/plugins/dart/program.py +907 -0
  52. codegraph/plugins/django/__init__.py +0 -0
  53. codegraph/plugins/django/extras.py +378 -0
  54. codegraph/plugins/django/models.py +508 -0
  55. codegraph/plugins/django/plugin.py +728 -0
  56. codegraph/plugins/django/schemas.py +339 -0
  57. codegraph/plugins/django/shapes.py +216 -0
  58. codegraph/plugins/django/urls.py +603 -0
  59. codegraph/plugins/express/__init__.py +0 -0
  60. codegraph/plugins/express/plugin.py +428 -0
  61. codegraph/plugins/flutter/__init__.py +0 -0
  62. codegraph/plugins/flutter/plugin.py +538 -0
  63. codegraph/plugins/kotlin/__init__.py +0 -0
  64. codegraph/plugins/kotlin/exact.py +457 -0
  65. codegraph/plugins/kotlin/plugin.py +1961 -0
  66. codegraph/plugins/kotlin/reparse.py +234 -0
  67. codegraph/plugins/laravel/__init__.py +0 -0
  68. codegraph/plugins/laravel/broadcast.py +351 -0
  69. codegraph/plugins/laravel/plugin.py +863 -0
  70. codegraph/plugins/laravel/tests.py +262 -0
  71. codegraph/plugins/laravel/values.py +728 -0
  72. codegraph/plugins/native/__init__.py +0 -0
  73. codegraph/plugins/native/gates.py +286 -0
  74. codegraph/plugins/native/runner.py +183 -0
  75. codegraph/plugins/native/scipread.py +194 -0
  76. codegraph/plugins/native/ts.py +54 -0
  77. codegraph/plugins/nest/__init__.py +0 -0
  78. codegraph/plugins/nest/plugin.py +654 -0
  79. codegraph/plugins/nextjs/__init__.py +0 -0
  80. codegraph/plugins/nextjs/plugin.py +336 -0
  81. codegraph/plugins/nuxt/__init__.py +0 -0
  82. codegraph/plugins/nuxt/plugin.py +308 -0
  83. codegraph/plugins/php/__init__.py +0 -0
  84. codegraph/plugins/php/extractor/composer.json +5 -0
  85. codegraph/plugins/php/extractor/composer.lock +76 -0
  86. codegraph/plugins/php/extractor/extract.php +743 -0
  87. codegraph/plugins/php/gating.py +573 -0
  88. codegraph/plugins/php/plugin.py +668 -0
  89. codegraph/plugins/php/strings.py +197 -0
  90. codegraph/plugins/python/__init__.py +0 -0
  91. codegraph/plugins/python/aitools.py +664 -0
  92. codegraph/plugins/python/external.py +245 -0
  93. codegraph/plugins/python/fields.py +107 -0
  94. codegraph/plugins/python/plugin.py +1733 -0
  95. codegraph/plugins/python/refs.py +485 -0
  96. codegraph/plugins/python/roots.py +412 -0
  97. codegraph/plugins/python/socketio.py +210 -0
  98. codegraph/plugins/python/subproc.py +864 -0
  99. codegraph/plugins/python/tests.py +1040 -0
  100. codegraph/plugins/python/values.py +179 -0
  101. codegraph/plugins/pyweb/__init__.py +0 -0
  102. codegraph/plugins/pyweb/plugin.py +1334 -0
  103. codegraph/plugins/pyweb/values.py +68 -0
  104. codegraph/plugins/rust/__init__.py +0 -0
  105. codegraph/plugins/rust/cargo.py +226 -0
  106. codegraph/plugins/rust/plugin.py +980 -0
  107. codegraph/plugins/rust/syntax.py +678 -0
  108. codegraph/plugins/scip/__init__.py +0 -0
  109. codegraph/plugins/scip/importer.py +129 -0
  110. codegraph/plugins/scip/scip.proto +962 -0
  111. codegraph/plugins/scip/scip_pb2.py +97 -0
  112. codegraph/plugins/stubs/__init__.py +0 -0
  113. codegraph/plugins/stubs/plugins.py +38 -0
  114. codegraph/plugins/swift/__init__.py +0 -0
  115. codegraph/plugins/swift/baseurl.py +109 -0
  116. codegraph/plugins/swift/exact.py +415 -0
  117. codegraph/plugins/swift/indexstore.py +209 -0
  118. codegraph/plugins/swift/packages.py +174 -0
  119. codegraph/plugins/swift/plugin.py +2890 -0
  120. codegraph/plugins/ts/__init__.py +0 -0
  121. codegraph/plugins/ts/baseurl.py +185 -0
  122. codegraph/plugins/ts/extractor/extract.mjs +2652 -0
  123. codegraph/plugins/ts/extractor/fw.mjs +685 -0
  124. codegraph/plugins/ts/extractor/package-lock.json +205 -0
  125. codegraph/plugins/ts/extractor/package.json +9 -0
  126. codegraph/plugins/ts/plugin.py +480 -0
  127. codegraph/plugins/tsweb/__init__.py +0 -0
  128. codegraph/plugins/tsweb/common.py +290 -0
  129. codegraph/plugins/tsweb/data.py +276 -0
  130. codegraph/presets/__init__.py +146 -0
  131. codegraph/presets/c_cpp.yaml +9 -0
  132. codegraph/presets/common.yaml +66 -0
  133. codegraph/presets/dart.yaml +9 -0
  134. codegraph/presets/django-ninja.yaml +15 -0
  135. codegraph/presets/django.yaml +25 -0
  136. codegraph/presets/djangorestframework.yaml +17 -0
  137. codegraph/presets/express.yaml +17 -0
  138. codegraph/presets/kotlin.yaml +11 -0
  139. codegraph/presets/laravel.yaml +40 -0
  140. codegraph/presets/nest.yaml +11 -0
  141. codegraph/presets/nextjs.yaml +15 -0
  142. codegraph/presets/nuxt.yaml +9 -0
  143. codegraph/presets/php.yaml +5 -0
  144. codegraph/presets/python.yaml +10 -0
  145. codegraph/presets/rust.yaml +5 -0
  146. codegraph/presets/swift.yaml +10 -0
  147. codegraph/presets/typescript.yaml +13 -0
  148. codegraph/process_runs.py +328 -0
  149. codegraph/protocols/__init__.py +299 -0
  150. codegraph/protocols/builtin.py +67 -0
  151. codegraph/protocols/matchers.py +144 -0
  152. codegraph/protocols/view.py +334 -0
  153. codegraph/query.py +2089 -0
  154. codegraph/realtime.py +260 -0
  155. codegraph/roundtrip.py +346 -0
  156. codegraph/routes.py +442 -0
  157. codegraph/starters.py +218 -0
  158. codegraph/tests_index.py +117 -0
  159. codegraph/viz/__init__.py +0 -0
  160. codegraph/viz/graph.py +369 -0
  161. codegraph/viz/server.py +198 -0
  162. codegraph/viz/static/app.css +148 -0
  163. codegraph/viz/static/app.js +1082 -0
  164. codegraph/viz/static/index.html +81 -0
  165. codegraph/viz/static/layered.js +237 -0
  166. codegraph/viz/static/vendor/VERSIONS.txt +4 -0
  167. codegraph/viz/static/vendor/cose-base.js +3214 -0
  168. codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
  169. codegraph/viz/static/vendor/cytoscape.min.js +31 -0
  170. codegraph/viz/static/vendor/layout-base.js +5230 -0
  171. codegraph/viz/tools/package-lock.json +303 -0
  172. codegraph/viz/tools/package.json +7 -0
  173. codegraph/viz/tools/shoot.mjs +165 -0
  174. codegraph/xcode.py +251 -0
@@ -0,0 +1,67 @@
1
+ """Built-in protocols. Adapter protocols describe node kinds cg already emitted before #31 (ids unchanged, read by
2
+ protocols/view.py); `endpoint` protocols are filled by plugins through protocol_send / protocol_receive."""
3
+ from __future__ import annotations
4
+
5
+ from . import Protocol, register
6
+ from . import matchers as M
7
+
8
+ A = "adapter"
9
+ # ---- existing kinds, adapted (no new nodes or edges)
10
+ register(Protocol("http", "tcp", "HTTP request: client endpoint http:<METHOD> <path> -> route (MATCHES_ROUTE, cg link)",
11
+ matcher=M.path, ports=(80, 443), schemes=("http", "https"), source=A, kinds=("http", "route"), guards=True))
12
+ register(Protocol("ws", "tcp", "WebSocket route (Django Channels consumer, route:WS <path>)", matcher=M.path, ports=(80, 443),
13
+ schemes=("ws", "wss"), source=A, kinds=("route",), guards=True))
14
+ register(Protocol("graphql", "tcp", "GraphQL operation resolver (route:GRAPHQL Query.x)", source=A, kinds=("route",), guards=True))
15
+ register(Protocol("pusher", "tcp", "Pusher protocol broadcast channel (Laravel Broadcast::channel / broadcastOn -> Echo / "
16
+ "pusher-js subscription; MATCHES_CHANNEL; the `cg channels` view)", matcher=M.dotted, fanout=True,
17
+ source=A, kinds=("channel", "channel_sub")))
18
+ register(Protocol("nest-rpc", "tcp", "NestJS microservice request (ClientProxy.send -> @MessagePattern)", source=A,
19
+ kinds=("message",), guards=True))
20
+ register(Protocol("nest-event", "tcp", "NestJS microservice event (ClientProxy.emit -> @EventPattern)", fanout=True, source=A,
21
+ kinds=("message",), guards=True))
22
+ register(Protocol("nest-ws", "tcp", "NestJS WebSocket gateway message (@SubscribeMessage)", source=A, kinds=("message",),
23
+ guards=True))
24
+ register(Protocol("grpc", "tcp", "gRPC method (NestJS @GrpcMethod)", source=A, kinds=("message",), guards=True))
25
+ register(Protocol("bull", "tcp", "Bull / BullMQ job queue (Queue.add -> @Processor / @Process, WorkerHost.process)",
26
+ source=A, kinds=("job",)))
27
+ register(Protocol("laravel-queue", "tcp", "Laravel queued job (dispatch / Bus / schedule -> Job::handle)", source=A,
28
+ kinds=("job",)))
29
+ register(Protocol("celery", "tcp", "Celery / RQ / Dramatiq task (.delay / .apply_async / send_task -> task function)",
30
+ source=A, kinds=("job",)))
31
+ register(Protocol("laravel-event", "local", "Laravel application event (event() / dispatch -> listener handle())",
32
+ fanout=True, entry=False, source=A, kinds=("event",)))
33
+ register(Protocol("nest-event-emitter", "local", "NestJS EventEmitter2 event (emit -> @OnEvent)", fanout=True, entry=False,
34
+ source=A, kinds=("event",)))
35
+ register(Protocol("django-signal", "local", "Django signal (send -> @receiver)", fanout=True, entry=False, source=A,
36
+ kinds=("event",), framework_senders=("django.*",)))
37
+
38
+ # ---- web / native bridges and desktop process boundaries (codegraph/bridges.py: endpoint nodes with their own checks)
39
+ for _n, _t, _d in (("capacitor", "local", "Capacitor plugin method"), ("react-native", "local", "React Native / Expo native module method"),
40
+ ("flutter", "local", "Flutter MethodChannel method"), ("flutter-event", "local", "Flutter EventChannel stream"),
41
+ ("pigeon", "local", "Pigeon API method"), ("electron-ipc", "ipc", "Electron IPC channel"),
42
+ ("electron-preload", "local", "Electron context bridge member"), ("tauri", "ipc", "Tauri command")):
43
+ register(Protocol(_n, _t, _d + " (`cg bridges`)", entry=False, source="bridges"))
44
+
45
+ # ---- endpoint protocols (protocol_send / protocol_receive; matchers run at index and link time)
46
+ register(Protocol("mqtt", "tcp", "MQTT topic (publish -> subscribe, `+` / `#` wildcards)", matcher=M.mqtt, fanout=True,
47
+ ports=(1883, 8883), schemes=("mqtt", "mqtts")))
48
+ register(Protocol("nats", "tcp", "NATS subject (publish / request -> subscribe, `*` / `>` wildcards)", matcher=M.nats,
49
+ fanout=True, ports=(4222,), schemes=("nats", "tls")))
50
+ register(Protocol("amqp", "tcp", "AMQP 0-9-1 routing key on a topic exchange (`*` / `#`)", matcher=M.amqp_topic,
51
+ fanout=True, ports=(5672, 5671), schemes=("amqp", "amqps")))
52
+ register(Protocol("kafka", "tcp", "Kafka topic (produce -> consumer group subscription; regex subscriptions as globs)",
53
+ matcher=M.glob, fanout=True, ports=(9092,), schemes=("kafka",)))
54
+ register(Protocol("redis-pubsub", "tcp", "Redis PUBLISH -> SUBSCRIBE / PSUBSCRIBE (glob patterns)", matcher=M.glob,
55
+ fanout=True, ports=(6379,), schemes=("redis", "rediss")))
56
+ register(Protocol("socketio", "tcp", "Socket.IO event (<namespace>#<event>; emit -> on, both directions; python-socketio "
57
+ "server and client)", matcher=M.template, ports=(80, 443), schemes=("ws", "wss", "http", "https"), guards=True,
58
+ directional=True))
59
+
60
+ # ---- AI harnesses (#66, codegraph/plugins/python/aitools.py): tools the model or an MCP client calls by name
61
+ register(Protocol("llm_tool", "local", "LLM tool / function: schema offered to the model (OpenAI, Anthropic, LangChain, "
62
+ "Agents SDK, LlamaIndex) -> handler (decorated function, dict registry, agent-loop branch)",
63
+ entry_kind="llm_tool"))
64
+ for _n, _d in (("mcp_tool", "MCP tool"), ("mcp_resource", "MCP resource (URI template)"), ("mcp_prompt", "MCP prompt")):
65
+ register(Protocol(_n, "tcp", f"{_d}: <server>/<name>; client call_tool / read_resource / get_prompt -> FastMCP / "
66
+ "MCPServer handler (`*/<name>` from a client that does not name the server)", matcher=M.mcp,
67
+ entry_kind="llm_tool", schemes=("stdio", "http", "https")))
@@ -0,0 +1,144 @@
1
+ """Shared name matchers of the protocol registry (codegraph/protocols/__init__.py).
2
+
3
+ A matcher takes the name a sender uses and the name a receiver declares and returns None (no match) or a dict of
4
+ segment counts, used to rank candidates and to set the confidence of the MATCHES_ENDPOINT edge:
5
+
6
+ lit literal segment == literal segment
7
+ wild a receiver wildcard / {param} took one segment (`orders.*` / `orders.{id}` ~ `orders.42`)
8
+ multi a receiver multi-level wildcard took zero or more segments (MQTT `#`, NATS `>`, AMQP `#`, glob `**`)
9
+ ph_into_lit a sender placeholder (`orders.{id}` built from a template) fitted a receiver literal (heuristic)
10
+
11
+ Names use `{param}` for parameters on both sides (the builder normalises `${x}` / `:x` to it).
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import fnmatch
16
+ import re
17
+
18
+ PARAM = re.compile(r"^\{[^{}]*\}$")
19
+
20
+
21
+ def _info(**kw) -> dict:
22
+ d = {"lit": 0, "wild": 0, "multi": 0, "ph_into_lit": 0}
23
+ d.update(kw)
24
+ return d
25
+
26
+
27
+ def exact(send: str, recv: str) -> dict | None:
28
+ """Same name (case-sensitive)."""
29
+ return _info(lit=1) if send == recv else None
30
+
31
+
32
+ def glob(send: str, recv: str) -> dict | None:
33
+ """Receiver name as a shell-style glob (`*`, `?`, `[...]`) over the whole name; literal characters rank."""
34
+ if send == recv:
35
+ return _info(lit=1)
36
+ if not any(c in recv for c in "*?["):
37
+ return None
38
+ if not fnmatch.fnmatchcase(send, recv):
39
+ return None
40
+ return _info(lit=len(re.sub(r"[*?]|\[[^\]]*\]", "", recv)), wild=1)
41
+
42
+
43
+ def path(send: str, recv: str) -> dict | None:
44
+ """URL-style paths split on `/`: the HTTP route matcher of cg link (`{param}`, `{rest*}` catch-alls, embedded
45
+ params), at least one literal segment in common."""
46
+ from ..link import match_path
47
+ ok, info = match_path(send, recv)
48
+ if not ok or (info["lit"] == 0 and (send.strip("/") or recv.strip("/"))):
49
+ return None
50
+ return _info(lit=info["lit"], wild=info["param"] + info["lit_into_param"], ph_into_lit=info["ph_into_lit"])
51
+
52
+
53
+ def topic(sep: str, one: str | None, many: str | None, many_min: int = 0, many_last: bool = True):
54
+ """Dotted / slashed topic names with single- and multi-level wildcards in the receiver's subscription:
55
+ MQTT topic('/', '+', '#') (`#` last, also matches the parent level), NATS topic('.', '*', '>', many_min=1),
56
+ AMQP topic exchange topic('.', '*', '#', many_last=False) (`#` anywhere, zero or more words).
57
+ `{param}` segments act as single-level wildcards on the receiver side and as placeholders on the sender side."""
58
+
59
+ def match(send: str, recv: str) -> dict | None:
60
+ s, r = send.split(sep), recv.split(sep)
61
+ if many_last and many in r[:-1]:
62
+ return None # `#` / `>` only as the last level
63
+ best: list[dict | None] = [None]
64
+
65
+ def go(i: int, j: int, acc: dict):
66
+ if j == len(r):
67
+ if i == len(s):
68
+ rank = (acc["lit"], -acc["multi"], -acc["ph_into_lit"])
69
+ if best[0] is None or rank > (best[0]["lit"], -best[0]["multi"], -best[0]["ph_into_lit"]):
70
+ best[0] = dict(acc)
71
+ return
72
+ rs = r[j]
73
+ if many is not None and rs == many:
74
+ for k in range(i + many_min, len(s) + 1):
75
+ go(k, j + 1, dict(acc, multi=acc["multi"] + 1))
76
+ return
77
+ if i >= len(s):
78
+ return
79
+ ss = s[i]
80
+ s_ph = bool(PARAM.match(ss))
81
+ if (one is not None and rs == one) or PARAM.match(rs):
82
+ go(i + 1, j + 1, dict(acc, wild=acc["wild"] + 1))
83
+ elif ss == rs:
84
+ go(i + 1, j + 1, dict(acc, lit=acc["lit"] + 1))
85
+ elif s_ph:
86
+ go(i + 1, j + 1, dict(acc, ph_into_lit=acc["ph_into_lit"] + 1))
87
+
88
+ go(0, 0, _info())
89
+ return best[0]
90
+
91
+ match.__doc__ = f"topic matcher sep={sep!r} one={one!r} many={many!r}"
92
+ return match
93
+
94
+
95
+ def _tpl_rx(name: str) -> re.Pattern:
96
+ return re.compile("^" + "".join(".+?" if PARAM.match(part) else re.escape(part)
97
+ for part in re.split(r"(\{[^{}]*\})", name)) + "$", re.S)
98
+
99
+
100
+ def template(send: str, recv: str) -> dict | None:
101
+ """Whole-name `{param}` templates (`order:{id}` ~ `order:42`, Socket.IO `<namespace>#<event>`): a receiver
102
+ template takes any non-empty text per parameter; a sender template fitting a receiver literal is a placeholder fit."""
103
+ if send == recv:
104
+ return _info(lit=1)
105
+ if "{" in recv and _tpl_rx(recv).match(send):
106
+ return _info(lit=len(re.sub(r"\{[^{}]*\}", "", recv)), wild=len(re.findall(r"\{[^{}]*\}", recv)))
107
+ if "{" in send and "{" not in recv and _tpl_rx(send).match(recv):
108
+ return _info(lit=len(re.sub(r"\{[^{}]*\}", "", send)), ph_into_lit=1)
109
+ return None
110
+
111
+
112
+ def mcp(send: str, recv: str) -> dict | None:
113
+ """MCP primitives `<server>/<name or uri template>`: a client that does not know the server sends `*/<name>`; the
114
+ rest matches exactly or by `{param}` URI template (`notes://{id}` ~ `notes://42`)."""
115
+ ss, _, sr = send.partition("/")
116
+ rs, _, rr = recv.partition("/")
117
+ if ss not in ("*", rs):
118
+ return None
119
+ i = template(sr, rr)
120
+ if i is None:
121
+ return None
122
+ if ss == "*":
123
+ i["wild"] += 1
124
+ return i
125
+
126
+
127
+ mqtt = topic("/", "+", "#")
128
+ nats = topic(".", "*", ">", many_min=1)
129
+ amqp_topic = topic(".", "*", "#", many_last=False)
130
+ dotted = topic(".", None, None) # `{param}` templates only (Laravel channel names, Socket.IO rooms)
131
+
132
+ MATCHERS = {"exact": exact, "glob": glob, "path": path, "template": template, "mqtt": mqtt, "nats": nats, "amqp_topic": amqp_topic,
133
+ "dotted": dotted, "mcp": mcp}
134
+
135
+
136
+ def rank(info: dict) -> tuple:
137
+ """Higher is more specific: fewer placeholder-into-literal fits, more literal segments, fewer wildcards."""
138
+ return (-info["ph_into_lit"], info["lit"], -info["multi"], -info["wild"])
139
+
140
+
141
+ def confidence(info: dict, tied: bool) -> str:
142
+ if info["ph_into_lit"] or tied:
143
+ return "heuristic"
144
+ return "resolved" if (info["wild"] or info["multi"]) else "exact"
@@ -0,0 +1,334 @@
1
+ """`cg protocols` / MCP `protocol_links`: every protocol endpoint of a graph in one view.
2
+
3
+ Endpoints come from the generic model (`endpoint:<protocol>:<name>` with SENDS_TO / RECEIVED_BY / MATCHES_ENDPOINT)
4
+ and from the node kinds cg emitted before it, read through adapters with their ids unchanged:
5
+
6
+ kind protocol senders (into the node) receivers matches
7
+ http http (ws for http:WS ...) HTTP_CALLS - MATCHES_ROUTE ->
8
+ route http / ws / graphql (test: TEST_HTTP) ROUTES_TO <- MATCHES_ROUTE
9
+ channel pusher BROADCASTS_ON (events) - <- MATCHES_CHANNEL
10
+ channel_sub pusher - SUBSCRIBES_CHANNEL (code) MATCHES_CHANNEL ->
11
+ message nest-rpc / nest-event / nest-ws / grpc DISPATCHES HANDLED_BY
12
+ job bull / laravel-queue / celery DISPATCHES (also to the handler, HANDLED_BY
13
+ via=job), SCHEDULES
14
+ event laravel-event / nest-event-emitter / django-signal DISPATCHES LISTENED_BY / HANDLED_BY
15
+ endpoint attrs.protocol (bridges, MQTT, Socket.IO, ...) SENDS_TO RECEIVED_BY MATCHES_ENDPOINT
16
+
17
+ Checks (per endpoint; a side is judged only when the graph holds some endpoint of that protocol on the other side, so
18
+ a single backend graph does not call every route `no_sender`):
19
+ no_receiver sent, nothing receives it here (directly or through a match)
20
+ no_sender received, nothing sends it (dead handler, or the producer is outside the analysed repos)
21
+ test_sender_only received, sent from tests only
22
+ ambiguous one sender matched several receivers equally well
23
+ schema_mismatch senders and receivers name different message types
24
+ unguarded a receiver reachable from outside (http / ws / graphql routes, Socket.IO handlers, ...) with no
25
+ auth guard recorded (the same classification as `cg routes --unguarded`)
26
+ external declared external in .cg.yaml (protocols.external: ["kafka:audit.*"]), a third-party HTTP origin,
27
+ or a bridge module implemented outside the repo
28
+ Bridge endpoints (Capacitor, React Native, Flutter, Electron, Tauri) keep the checks codegraph/bridges.py computed.
29
+ """
30
+ from __future__ import annotations
31
+
32
+ import fnmatch
33
+ import json
34
+ from collections import defaultdict
35
+
36
+ from . import REGISTRY, compatible, external_match
37
+
38
+ KINDS = ("http", "route", "channel", "channel_sub", "message", "job", "event", "endpoint")
39
+ EDGE_ROLE_KINDS = ("HTTP_CALLS", "MATCHES_ROUTE", "ROUTES_TO", "USES_MIDDLEWARE", "TEST_HTTP", "BROADCASTS_ON",
40
+ "SUBSCRIBES_CHANNEL", "MATCHES_CHANNEL", "DISPATCHES", "HANDLED_BY", "LISTENED_BY", "SCHEDULES",
41
+ "SENDS_TO", "RECEIVED_BY", "MATCHES_ENDPOINT", "TEST_CALLS")
42
+ NEST = {"rpc": "nest-rpc", "event": "nest-event", "ws": "nest-ws", "grpc": "grpc"}
43
+ SEND_IN = {"http": ("HTTP_CALLS",), "channel": ("BROADCASTS_ON",), "message": ("DISPATCHES",),
44
+ "job": ("DISPATCHES", "SCHEDULES"), "event": ("DISPATCHES",), "endpoint": ("SENDS_TO",)}
45
+ RECV_OUT = {"route": ("ROUTES_TO",), "message": ("HANDLED_BY",), "job": ("HANDLED_BY",),
46
+ "event": ("LISTENED_BY", "HANDLED_BY"), "endpoint": ("RECEIVED_BY",)}
47
+ RECV_IN = {"channel_sub": ("SUBSCRIBES_CHANNEL",)}
48
+ MATCH = ("MATCHES_ROUTE", "MATCHES_CHANNEL", "MATCHES_ENDPOINT")
49
+ TEST_ORIG = ("HTTP_CALLS", "DISPATCHES", "SENDS_TO", "SUBSCRIBES_CHANNEL")
50
+
51
+
52
+ def protocol_of(kind: str, nid: str, a: dict, lang: str | None) -> str | None:
53
+ if kind == "endpoint":
54
+ return a.get("protocol")
55
+ if kind == "http":
56
+ return "ws" if a.get("method") == "WS" else "http"
57
+ if kind == "route":
58
+ m = (a.get("method") or "").upper()
59
+ return "ws" if m == "WS" else "graphql" if m == "GRAPHQL" else "http"
60
+ if kind in ("channel", "channel_sub"):
61
+ return "pusher"
62
+ if kind == "message":
63
+ return NEST.get(a.get("transport") or nid.split(":")[1], "nest-" + (a.get("transport") or "message"))
64
+ if kind == "job":
65
+ fw = a.get("framework") or ""
66
+ return "bull" if fw.startswith("bull") or lang == "ts" else "laravel-queue" if lang == "php" else "celery"
67
+ if kind == "event":
68
+ fw = a.get("framework") or ""
69
+ if fw == "nest-event-emitter" or lang == "ts":
70
+ return "nest-event-emitter"
71
+ return "django-signal" if a.get("signal") or lang == "python" else "laravel-event"
72
+ return None
73
+
74
+
75
+ def name_of(kind: str, nid: str, name: str, a: dict) -> str:
76
+ key = nid.split(":", 1)[1]
77
+ if kind == "channel":
78
+ return a.get("pattern") or key
79
+ if kind == "channel_sub":
80
+ return a.get("name") or key
81
+ if kind == "message":
82
+ return key.split(":", 1)[1] if ":" in key else key
83
+ if kind == "endpoint":
84
+ return name or key.split(":", 1)[-1]
85
+ return key
86
+
87
+
88
+ def _externals(st) -> list[str]:
89
+ """protocols.external of the indexed repo(s) (.cg.yaml, recorded in the index stats)."""
90
+ from ..routes import _meta_of
91
+ try:
92
+ m = st.meta()
93
+ except Exception: # noqa: BLE001
94
+ return []
95
+ metas = [_meta_of((m.get("sources") or {}).get(r)) for r in m.get("repos") or []] or [m]
96
+ out = []
97
+ for mm in metas:
98
+ out += (((mm.get("stats") or {}).get("config") or {}).get("protocols") or {}).get("external") or []
99
+ return list(dict.fromkeys(out))
100
+
101
+
102
+ def _load(st):
103
+ nodes = {}
104
+ q = ",".join("?" * len(KINDS))
105
+ for r in st.q(f"SELECT id, kind, name, file, line, lang, attrs, entry_kind FROM nodes WHERE kind IN ({q})", KINDS):
106
+ a = json.loads(r["attrs"] or "{}") if r["attrs"] else {}
107
+ proto = protocol_of(r["kind"], r["id"], a, r["lang"])
108
+ if not proto:
109
+ continue
110
+ nodes[r["id"]] = {"id": r["id"], "kind": r["kind"], "protocol": proto, "name": name_of(r["kind"], r["id"], r["name"], a),
111
+ "file": r["file"], "line": r["line"], "attrs": a, "entry_kind": r["entry_kind"]}
112
+ q2 = ",".join("?" * len(EDGE_ROLE_KINDS))
113
+ edges = [dict(r) for r in st.q(f"SELECT src, dst, kind, file, line, confidence, attrs FROM edges WHERE kind IN ({q2})",
114
+ EDGE_ROLE_KINDS)]
115
+ return nodes, edges
116
+
117
+
118
+ def collect(st) -> dict:
119
+ """Every endpoint with senders / receivers / matches / guards / checks (no per-endpoint queries)."""
120
+ from ..routes import AuthMatcher, guard_setup, route_guards
121
+ nodes, edges = _load(st)
122
+ ep = {nid: dict(n, senders=[], test_senders=[], receivers=[], matches=[], mw=[]) for nid, n in nodes.items()}
123
+ job_of_handler = {}
124
+ for e in edges:
125
+ if e["kind"] == "HANDLED_BY" and e["src"] in ep and ep[e["src"]]["kind"] == "job":
126
+ job_of_handler.setdefault(e["dst"], e["src"])
127
+ for e in edges:
128
+ k, s, d = e["kind"], e["src"], e["dst"]
129
+ a = json.loads(e["attrs"] or "{}") if e["attrs"] else {}
130
+ at = f"{e['file']}:{e['line']}"
131
+ if k in MATCH:
132
+ if s in ep and d in ep:
133
+ ep[s]["matches"].append({"endpoint": d, "confidence": e["confidence"], "dir": "out", "kind": k,
134
+ "ambiguous": a.get("ambiguous")})
135
+ ep[d]["matches"].append({"endpoint": s, "confidence": e["confidence"], "dir": "in", "kind": k})
136
+ continue
137
+ if k == "USES_MIDDLEWARE":
138
+ if s in ep:
139
+ ep[s]["mw"].append(e)
140
+ continue
141
+ if k in ("TEST_CALLS", "TEST_HTTP"):
142
+ if d in ep and (k == "TEST_HTTP" or a.get("orig") in TEST_ORIG):
143
+ ep[d]["test_senders"].append({"fn": s, "at": at, "confidence": e["confidence"]})
144
+ continue
145
+ if d in ep and k in SEND_IN.get(ep[d]["kind"], ()):
146
+ ep[d]["senders"].append({"fn": s, "at": at, "confidence": e["confidence"], "via": a.get("via") or a.get("role"),
147
+ **({k2: a[k2] for k2 in ("schema", "process") if a.get(k2)})})
148
+ elif k == "DISPATCHES" and a.get("via") == "job" and d in job_of_handler:
149
+ ep[job_of_handler[d]]["senders"].append({"fn": s, "at": at, "confidence": e["confidence"], "via": "dispatch"})
150
+ if s in ep and k in RECV_OUT.get(ep[s]["kind"], ()):
151
+ ep[s]["receivers"].append({"handler": d, "at": at, "confidence": e["confidence"],
152
+ **({k2: a[k2] for k2 in ("platform", "process", "schema") if a.get(k2)})})
153
+ if d in ep and k in RECV_IN.get(ep[d]["kind"], ()):
154
+ ep[d]["receivers"].append({"handler": s, "at": at, "confidence": e["confidence"]})
155
+ # sides per protocol
156
+ psend, precv = defaultdict(bool), defaultdict(bool)
157
+ for n in ep.values():
158
+ psend[n["protocol"]] |= bool(n["senders"])
159
+ precv[n["protocol"]] |= bool(n["receivers"])
160
+ gs = guard_setup(st)
161
+ is_auth = AuthMatcher(None, gs["applied"], gs["auth_patterns"], gs["secret_patterns"])
162
+ ext = _externals(st)
163
+ for n in ep.values():
164
+ a, p = n["attrs"], REGISTRY.get(n["protocol"])
165
+ peers = [ep[m["endpoint"]] for m in n["matches"]]
166
+ # a directional protocol (Socket.IO): this endpoint's own senders and receivers pair up only across processes
167
+ own = compatible(p, {x["process"] for x in n["senders"] if x.get("process")},
168
+ {x["process"] for x in n["receivers"] if x.get("process")})
169
+ psent = any(x["senders"] for x in peers)
170
+ precv_ = any(x["receivers"] or x["kind"] == "route" for x in peers) # a matched route receives
171
+ if own:
172
+ sent = bool(n["senders"]) or psent
173
+ recv = bool(n["receivers"]) or precv_
174
+ else: # e.g. a client emits `x` and a client handles `x`: neither reaches the other
175
+ sent = psent or (bool(n["senders"]) and not n["receivers"])
176
+ recv = precv_ or (bool(n["receivers"]) and not n["senders"])
177
+ n["side"] = "both" if n["senders"] and n["receivers"] else "send" if n["senders"] or (n["kind"] == "http") \
178
+ else "receive" if n["receivers"] or n["kind"] in ("route",) else ("send" if n["test_senders"] else "none")
179
+ n["linked"] = sent and recv
180
+ if not own:
181
+ n["linked"] = bool((n["senders"] and precv_) or (n["receivers"] and psent))
182
+ ck = []
183
+ n["external"] = None
184
+ if p and p.source == "bridges":
185
+ ck = list(a.get("checks") or [])
186
+ n["external"] = a.get("package") or ("implemented outside this repo" if a.get("external") else None)
187
+ else:
188
+ hit = external_match(ext, n["protocol"], n["name"])
189
+ if hit:
190
+ n["external"] = f".cg.yaml protocols.external {hit}"
191
+ elif p and not sent and any(fnmatch.fnmatchcase(n["name"], g) for g in p.framework_senders):
192
+ n["external"] = "sent by the framework"
193
+ elif n["kind"] == "http" and a.get("origin_kind") == "other":
194
+ n["external"] = f"other origin {a.get('origin')}"
195
+ test_only = bool(a.get("test_only"))
196
+ skip_route = n["kind"] == "route" and (a.get("mounted", True) is False or a.get("framework") == "django-admin")
197
+ if (sent and not recv or not own and n["senders"] and not precv_) and precv[n["protocol"]] \
198
+ and not n["external"] and not test_only:
199
+ ck.append("no_receiver")
200
+ if (recv and not sent or not own and n["receivers"] and not psent) and psend[n["protocol"]] \
201
+ and not n["external"] and not skip_route:
202
+ ck.append("test_sender_only" if n["test_senders"] or any(x["test_senders"] for x in peers) else "no_sender")
203
+ outs = [m for m in n["matches"] if m["dir"] == "out"]
204
+ amb = any(m.get("ambiguous") for m in outs)
205
+ if not amb and len(outs) > 1:
206
+ if n["kind"] == "http":
207
+ amb = True # several routes equally specific (cg link keeps the ties, heuristic)
208
+ elif n["kind"] == "channel_sub":
209
+ amb = any(m["confidence"] == "heuristic" for m in outs)
210
+ elif n["kind"] == "endpoint":
211
+ amb = not (p and p.fanout)
212
+ if amb:
213
+ ck.append("ambiguous")
214
+ ss = {x["schema"] for x in n["senders"] + [y for q in peers for y in q["senders"]] if x.get("schema")}
215
+ rs = {x["schema"] for x in n["receivers"] + [y for q in peers for y in q["receivers"]] if x.get("schema")}
216
+ if a.get("schema"):
217
+ (rs if n["receivers"] else ss).add(a["schema"])
218
+ if ss and rs and not (ss & rs):
219
+ ck.append("schema_mismatch")
220
+ n["schemas"] = {"sent": sorted(ss), "received": sorted(rs)}
221
+ # guards: routes as in `cg routes`, generic receivers from attrs.guards
222
+ if n["kind"] == "route" or (n["kind"] in ("endpoint", "message") and "guards" in a):
223
+ if n["kind"] == "route":
224
+ g = route_guards(a, n["mw"], is_auth)
225
+ elif n["kind"] == "message": # Nest @UseGuards / APP_GUARD class names: classified like route guards (#69)
226
+ g = [{"name": x, "auth": is_auth(x), "secret": is_auth.secret(x)} for x in a.get("guards") or []]
227
+ else: # recorded by the plugin as a check that rejects (connect handler, io.use, interceptor): auth
228
+ g = [{"name": x if isinstance(x, str) else x.get("name"), "auth": True} for x in a.get("guards") or []]
229
+ n["guards"] = [x["name"] for x in g]
230
+ has_auth = any(x["auth"] for x in g)
231
+ secret = any(x.get("secret") for x in g)
232
+ if (p and p.guards) and (n["receivers"] or n["kind"] == "route") and not has_auth and not secret and not n["external"] \
233
+ and not (n["kind"] == "route" and (a.get("mounted", True) is False or a.get("framework") == "django-admin")):
234
+ ck.append("unguarded")
235
+ else:
236
+ n["guards"] = None
237
+ n["checks"] = ck
238
+ return ep
239
+
240
+
241
+ def protocols(st, pattern: str | None = None, protocol: str | None = None, side: str | None = None,
242
+ unmatched: bool = False, max_items: int = 200) -> dict:
243
+ ep = collect(st)
244
+ summ = defaultdict(lambda: defaultdict(int))
245
+ for n in ep.values():
246
+ s = summ[n["protocol"]]
247
+ s["endpoints"] += 1
248
+ s["send"] += n["side"] in ("send", "both")
249
+ s["receive"] += n["side"] in ("receive", "both")
250
+ s["linked"] += n["linked"]
251
+ for c in n["checks"]:
252
+ s[c] += 1
253
+ s["external"] += bool(n["external"])
254
+ sel = []
255
+ listing = bool(pattern or protocol or side or unmatched)
256
+ if listing:
257
+ p = pattern[len("endpoint:"):] if pattern and pattern.startswith("endpoint:") else pattern
258
+ for nid in sorted(ep):
259
+ n = ep[nid]
260
+ if protocol and n["protocol"] != protocol:
261
+ continue
262
+ if side and n["side"] not in (side, "both"):
263
+ continue
264
+ if unmatched and not (n["checks"] or n["external"]):
265
+ continue
266
+ if p and not (n["id"] == p or fnmatch.fnmatchcase(n["name"], p) or fnmatch.fnmatchcase(n["id"].split(":", 1)[1], p)
267
+ or fnmatch.fnmatchcase(f"{n['protocol']}:{n['name']}", p) or p.lower() in n["name"].lower()):
268
+ continue
269
+ sel.append(n)
270
+ detail = 0 < len(sel) <= 6
271
+ out = []
272
+ for n in sel[:max_items]:
273
+ item = {k: n[k] for k in ("id", "kind", "protocol", "name", "side", "linked", "checks", "external", "guards", "senders",
274
+ "test_senders", "receivers", "matches")}
275
+ item["at"] = f"{n['file']}:{n['line']}" if n["file"] else None
276
+ item["transport"] = (REGISTRY.get(n["protocol"]).transport if REGISTRY.get(n["protocol"]) else None)
277
+ if n.get("schemas"):
278
+ item["schemas"] = n["schemas"]
279
+ if n["attrs"].get("test_only"):
280
+ item["test_only"] = True
281
+ if detail:
282
+ for s in item["senders"]:
283
+ s["entry_kinds"] = {e["entry_kind"]: e["entry_count"] for e in
284
+ st.q("SELECT entry_kind, entry_count FROM node_entry WHERE node_id=?", (s["fn"],))}
285
+ out.append(item)
286
+ reg = [{"name": p.name, "transport": p.transport, "matcher": getattr(p.matcher, "__name__", "custom"), "fanout": p.fanout,
287
+ "source": p.source, "description": p.description} for p in REGISTRY.values()]
288
+ return {"pattern": pattern, "protocol": protocol, "side": side, "unmatched": unmatched,
289
+ "summary": {k: dict(v) for k, v in sorted(summ.items())}, "endpoints": out, "selected": len(sel),
290
+ "listing": listing, "registry": reg}
291
+
292
+
293
+ def render_protocols(res: dict, max_items: int = 60) -> str:
294
+ from ..query import short_id
295
+ L = []
296
+ if not res["summary"]:
297
+ return "no protocol endpoints in this graph (HTTP calls / routes, channels, messages, jobs, events, bridges)"
298
+ if not res["listing"]:
299
+ L.append("protocol endpoints send receive linked checks")
300
+ for p, s in res["summary"].items():
301
+ ck = ", ".join(f"{k} {v}" for k, v in s.items() if k not in ("endpoints", "send", "receive", "linked") and v)
302
+ L.append(f"{p:<19} {s['endpoints']:>9} {s.get('send', 0):>4} {s.get('receive', 0):>7} {s.get('linked', 0):>6} {ck or '-'}")
303
+ L.append("(cg protocols --protocol P | PATTERN | --unmatched for endpoints; cg link / channels / bridges for the "
304
+ "HTTP, Pusher and bridge views)")
305
+ return "\n".join(L)
306
+ eps = res["endpoints"]
307
+ if not eps:
308
+ return "no protocol endpoint matches " + ", ".join(
309
+ f"{k}={res[k]!r}" for k in ("pattern", "protocol", "side") if res.get(k)) + (" (unmatched only)" if res["unmatched"] else "")
310
+ detail = len(eps) <= 6
311
+ for i in eps[:max_items]:
312
+ flags = list(i["checks"]) + ([f"external ({i['external']})"] if i["external"] else [])
313
+ L.append(f"[{i['protocol']}] {i['name']} ({i['side']}{', linked' if i['linked'] else ''})"
314
+ + (f" guards: {', '.join(i['guards']) or 'none'}" if i["guards"] is not None else "")
315
+ + (f" ! {'; '.join(flags)}" if flags else ""))
316
+ if detail:
317
+ L.append(f" node {i['id']}" + (f" @ {i['at']}" if i.get("at") else ""))
318
+ for s in i["senders"][:8]:
319
+ ek = ", ".join(f"{k}({v})" for k, v in sorted((s.get("entry_kinds") or {}).items()))
320
+ L.append(f" sent by {short_id(s['fn'])} @ {s['at']} [{s['confidence']}]" + (f" entries: {ek}" if ek else ""))
321
+ for s in i["test_senders"][:4]:
322
+ L.append(f" test {short_id(s['fn'])} @ {s['at']}")
323
+ for r in i["receivers"][:8]:
324
+ L.append(f" received by {short_id(r['handler'])} @ {r['at']} [{r['confidence']}]")
325
+ for m in i["matches"][:8]:
326
+ L.append(f" {'matches' if m['dir'] == 'out' else 'matched by'} {m['endpoint']} [{m['confidence']}]")
327
+ if i.get("schemas"):
328
+ L.append(f" schemas: sent {i['schemas']['sent']} / received {i['schemas']['received']}")
329
+ else:
330
+ L.append(f" senders {len(i['senders'])}" + (f" (+{len(i['test_senders'])} test)" if i["test_senders"] else "")
331
+ + f", receivers {len(i['receivers'])}, matches {len(i['matches'])}")
332
+ if res["selected"] > min(len(eps), max_items):
333
+ L.append(f"... {res['selected'] - min(len(eps), max_items)} more")
334
+ return "\n".join(L)