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.
- cg_code_graph-0.10.1.dist-info/METADATA +678 -0
- cg_code_graph-0.10.1.dist-info/RECORD +174 -0
- cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
- cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
- cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
- cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
- codegraph/__init__.py +2 -0
- codegraph/aitools.py +129 -0
- codegraph/apps.py +76 -0
- codegraph/blindspots.py +428 -0
- codegraph/bridges.py +1701 -0
- codegraph/cli.py +725 -0
- codegraph/concepts.py +362 -0
- codegraph/config.py +559 -0
- codegraph/core/__init__.py +0 -0
- codegraph/core/cache.py +375 -0
- codegraph/core/detect.py +80 -0
- codegraph/core/extractors.py +187 -0
- codegraph/core/fsutil.py +61 -0
- codegraph/core/generated.py +575 -0
- codegraph/core/model.py +174 -0
- codegraph/core/paths.py +175 -0
- codegraph/core/plugin.py +160 -0
- codegraph/core/store.py +80 -0
- codegraph/core/syntax_errors.py +132 -0
- codegraph/coverage.py +928 -0
- codegraph/doctor.py +453 -0
- codegraph/external.py +613 -0
- codegraph/indexer.py +336 -0
- codegraph/link.py +434 -0
- codegraph/lint_async.py +524 -0
- codegraph/mcp_server.py +1303 -0
- codegraph/parity.py +473 -0
- codegraph/parity_structure.py +307 -0
- codegraph/payload.py +321 -0
- codegraph/plans.py +1285 -0
- codegraph/platform_scan.py +643 -0
- codegraph/platforms.py +1369 -0
- codegraph/plugins/__init__.py +0 -0
- codegraph/plugins/cfamily/__init__.py +0 -0
- codegraph/plugins/cfamily/plugin.py +930 -0
- codegraph/plugins/cfamily/syntax.py +881 -0
- codegraph/plugins/dart/__init__.py +0 -0
- codegraph/plugins/dart/bridges.py +345 -0
- codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
- codegraph/plugins/dart/extractor/pubspec.lock +149 -0
- codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
- codegraph/plugins/dart/http.py +904 -0
- codegraph/plugins/dart/models.py +308 -0
- codegraph/plugins/dart/plugin.py +625 -0
- codegraph/plugins/dart/program.py +907 -0
- codegraph/plugins/django/__init__.py +0 -0
- codegraph/plugins/django/extras.py +378 -0
- codegraph/plugins/django/models.py +508 -0
- codegraph/plugins/django/plugin.py +728 -0
- codegraph/plugins/django/schemas.py +339 -0
- codegraph/plugins/django/shapes.py +216 -0
- codegraph/plugins/django/urls.py +603 -0
- codegraph/plugins/express/__init__.py +0 -0
- codegraph/plugins/express/plugin.py +428 -0
- codegraph/plugins/flutter/__init__.py +0 -0
- codegraph/plugins/flutter/plugin.py +538 -0
- codegraph/plugins/kotlin/__init__.py +0 -0
- codegraph/plugins/kotlin/exact.py +457 -0
- codegraph/plugins/kotlin/plugin.py +1961 -0
- codegraph/plugins/kotlin/reparse.py +234 -0
- codegraph/plugins/laravel/__init__.py +0 -0
- codegraph/plugins/laravel/broadcast.py +351 -0
- codegraph/plugins/laravel/plugin.py +863 -0
- codegraph/plugins/laravel/tests.py +262 -0
- codegraph/plugins/laravel/values.py +728 -0
- codegraph/plugins/native/__init__.py +0 -0
- codegraph/plugins/native/gates.py +286 -0
- codegraph/plugins/native/runner.py +183 -0
- codegraph/plugins/native/scipread.py +194 -0
- codegraph/plugins/native/ts.py +54 -0
- codegraph/plugins/nest/__init__.py +0 -0
- codegraph/plugins/nest/plugin.py +654 -0
- codegraph/plugins/nextjs/__init__.py +0 -0
- codegraph/plugins/nextjs/plugin.py +336 -0
- codegraph/plugins/nuxt/__init__.py +0 -0
- codegraph/plugins/nuxt/plugin.py +308 -0
- codegraph/plugins/php/__init__.py +0 -0
- codegraph/plugins/php/extractor/composer.json +5 -0
- codegraph/plugins/php/extractor/composer.lock +76 -0
- codegraph/plugins/php/extractor/extract.php +743 -0
- codegraph/plugins/php/gating.py +573 -0
- codegraph/plugins/php/plugin.py +668 -0
- codegraph/plugins/php/strings.py +197 -0
- codegraph/plugins/python/__init__.py +0 -0
- codegraph/plugins/python/aitools.py +664 -0
- codegraph/plugins/python/external.py +245 -0
- codegraph/plugins/python/fields.py +107 -0
- codegraph/plugins/python/plugin.py +1733 -0
- codegraph/plugins/python/refs.py +485 -0
- codegraph/plugins/python/roots.py +412 -0
- codegraph/plugins/python/socketio.py +210 -0
- codegraph/plugins/python/subproc.py +864 -0
- codegraph/plugins/python/tests.py +1040 -0
- codegraph/plugins/python/values.py +179 -0
- codegraph/plugins/pyweb/__init__.py +0 -0
- codegraph/plugins/pyweb/plugin.py +1334 -0
- codegraph/plugins/pyweb/values.py +68 -0
- codegraph/plugins/rust/__init__.py +0 -0
- codegraph/plugins/rust/cargo.py +226 -0
- codegraph/plugins/rust/plugin.py +980 -0
- codegraph/plugins/rust/syntax.py +678 -0
- codegraph/plugins/scip/__init__.py +0 -0
- codegraph/plugins/scip/importer.py +129 -0
- codegraph/plugins/scip/scip.proto +962 -0
- codegraph/plugins/scip/scip_pb2.py +97 -0
- codegraph/plugins/stubs/__init__.py +0 -0
- codegraph/plugins/stubs/plugins.py +38 -0
- codegraph/plugins/swift/__init__.py +0 -0
- codegraph/plugins/swift/baseurl.py +109 -0
- codegraph/plugins/swift/exact.py +415 -0
- codegraph/plugins/swift/indexstore.py +209 -0
- codegraph/plugins/swift/packages.py +174 -0
- codegraph/plugins/swift/plugin.py +2890 -0
- codegraph/plugins/ts/__init__.py +0 -0
- codegraph/plugins/ts/baseurl.py +185 -0
- codegraph/plugins/ts/extractor/extract.mjs +2652 -0
- codegraph/plugins/ts/extractor/fw.mjs +685 -0
- codegraph/plugins/ts/extractor/package-lock.json +205 -0
- codegraph/plugins/ts/extractor/package.json +9 -0
- codegraph/plugins/ts/plugin.py +480 -0
- codegraph/plugins/tsweb/__init__.py +0 -0
- codegraph/plugins/tsweb/common.py +290 -0
- codegraph/plugins/tsweb/data.py +276 -0
- codegraph/presets/__init__.py +146 -0
- codegraph/presets/c_cpp.yaml +9 -0
- codegraph/presets/common.yaml +66 -0
- codegraph/presets/dart.yaml +9 -0
- codegraph/presets/django-ninja.yaml +15 -0
- codegraph/presets/django.yaml +25 -0
- codegraph/presets/djangorestframework.yaml +17 -0
- codegraph/presets/express.yaml +17 -0
- codegraph/presets/kotlin.yaml +11 -0
- codegraph/presets/laravel.yaml +40 -0
- codegraph/presets/nest.yaml +11 -0
- codegraph/presets/nextjs.yaml +15 -0
- codegraph/presets/nuxt.yaml +9 -0
- codegraph/presets/php.yaml +5 -0
- codegraph/presets/python.yaml +10 -0
- codegraph/presets/rust.yaml +5 -0
- codegraph/presets/swift.yaml +10 -0
- codegraph/presets/typescript.yaml +13 -0
- codegraph/process_runs.py +328 -0
- codegraph/protocols/__init__.py +299 -0
- codegraph/protocols/builtin.py +67 -0
- codegraph/protocols/matchers.py +144 -0
- codegraph/protocols/view.py +334 -0
- codegraph/query.py +2089 -0
- codegraph/realtime.py +260 -0
- codegraph/roundtrip.py +346 -0
- codegraph/routes.py +442 -0
- codegraph/starters.py +218 -0
- codegraph/tests_index.py +117 -0
- codegraph/viz/__init__.py +0 -0
- codegraph/viz/graph.py +369 -0
- codegraph/viz/server.py +198 -0
- codegraph/viz/static/app.css +148 -0
- codegraph/viz/static/app.js +1082 -0
- codegraph/viz/static/index.html +81 -0
- codegraph/viz/static/layered.js +237 -0
- codegraph/viz/static/vendor/VERSIONS.txt +4 -0
- codegraph/viz/static/vendor/cose-base.js +3214 -0
- codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
- codegraph/viz/static/vendor/cytoscape.min.js +31 -0
- codegraph/viz/static/vendor/layout-base.js +5230 -0
- codegraph/viz/tools/package-lock.json +303 -0
- codegraph/viz/tools/package.json +7 -0
- codegraph/viz/tools/shoot.mjs +165 -0
- 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)
|