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
codegraph/concepts.py
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""Deterministic concept queries over resolution facts.
|
|
2
|
+
|
|
3
|
+
`resolutions(st, concept)` lists every place a value for the concept (e.g. "currency", "timezone", "locale") is *resolved*: a variable,
|
|
4
|
+
fallback return or resolver function whose value comes from an ordered fallback chain (request key, JSON setting
|
|
5
|
+
and its default, model attribute / queried column, config/env, literal). Sites are grouped by their chain
|
|
6
|
+
signature, divergent chains are compared step by step, and on a combined graph the client side is attached:
|
|
7
|
+
which frontend endpoints reach each site, whether their requests can/do send the concept's request key, and the
|
|
8
|
+
client's own literal fallbacks (`report.value?.timezone ?? 'UTC'`).
|
|
9
|
+
|
|
10
|
+
Matching is lexical and deterministic: a site belongs to the concept when the *head word* of its target
|
|
11
|
+
(`$timezone`, `$customerTimezone`, `resolveTimezone()`, `report.value?.timezone`) is the concept (singular/plural),
|
|
12
|
+
or when its chain starts with a request key / setting / column whose head word is the concept.
|
|
13
|
+
The output is the deterministic facts themselves, ready for a reviewer or an agent to summarise.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import re
|
|
19
|
+
from collections import defaultdict
|
|
20
|
+
|
|
21
|
+
from .core.store import GraphStore
|
|
22
|
+
|
|
23
|
+
SOURCE_KINDS = ("input", "setting", "column", "config", "env")
|
|
24
|
+
CALL_LIKE_UP = ["CALLS", "IMPLEMENTED_BY", "OVERRIDDEN_BY", "BOUND_TO", "ROUTES_TO", "HANDLED_BY"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def head_token(ident: str) -> str:
|
|
28
|
+
toks = re.findall(r"[A-Z]?[a-z0-9]+|[A-Z]+(?![a-z])", (ident or "").replace("$", ""))
|
|
29
|
+
return toks[-1].lower() if toks else ""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def concept_forms(concept: str) -> set[str]:
|
|
33
|
+
c = concept.lower().strip()
|
|
34
|
+
forms = {c}
|
|
35
|
+
if c.endswith("ies"):
|
|
36
|
+
forms.add(c[:-3] + "y")
|
|
37
|
+
elif c.endswith("y"):
|
|
38
|
+
forms.add(c[:-1] + "ies")
|
|
39
|
+
elif c.endswith("s"):
|
|
40
|
+
forms.add(c[:-1])
|
|
41
|
+
else:
|
|
42
|
+
forms.add(c + "s")
|
|
43
|
+
return forms
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def short(nid: str | None) -> str:
|
|
47
|
+
if not nid:
|
|
48
|
+
return "?"
|
|
49
|
+
x = nid.split(":", 1)[-1]
|
|
50
|
+
if "#" in x and nid.split(":", 1)[0] in ("page", "component", "layout", "composable", "function", "store", "module"):
|
|
51
|
+
return x
|
|
52
|
+
x = x.split("\\")[-1] if "\\" in x and "::" not in x else x
|
|
53
|
+
if "::" in x:
|
|
54
|
+
cls, m = x.rsplit("::", 1)
|
|
55
|
+
x = f"{cls.split(chr(92))[-1]}::{m}"
|
|
56
|
+
return x
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def flat_atoms(atoms):
|
|
60
|
+
for a in atoms or []:
|
|
61
|
+
if a["kind"] == "call" and a.get("atoms"):
|
|
62
|
+
yield from flat_atoms(a["atoms"])
|
|
63
|
+
elif a["kind"] == "param" and a.get("sources"):
|
|
64
|
+
seen = []
|
|
65
|
+
for s in a["sources"]:
|
|
66
|
+
for x in flat_atoms(s["atoms"]):
|
|
67
|
+
k = json.dumps({kk: vv for kk, vv in x.items() if kk in ("kind", "key", "column", "value", "text")}, sort_keys=True)
|
|
68
|
+
if k not in seen:
|
|
69
|
+
seen.append(k)
|
|
70
|
+
yield {**x, "via_param": a["name"], "callers": len(a["sources"])}
|
|
71
|
+
else:
|
|
72
|
+
yield a
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def atom_text(a) -> str:
|
|
76
|
+
k = a["kind"]
|
|
77
|
+
norm = f" [{', '.join(a['norm'])}]" if a.get("norm") else ""
|
|
78
|
+
if k == "input":
|
|
79
|
+
flow = f"; flow: {' <- '.join(a['flow'])}" if a.get("flow") else ""
|
|
80
|
+
vp = f" (param ${a['via_param']} from {a['callers']} call site(s))" if a.get("via_param") else ""
|
|
81
|
+
return f"request key '{a['key']}' via {a.get('via')}{vp}{flow}{norm}"
|
|
82
|
+
if k == "setting":
|
|
83
|
+
own = f" ({'/'.join(a['owner'])}::getSetting)" if a.get("owner") else ""
|
|
84
|
+
return f"setting '{a['key']}'{own} default {a.get('default')!r}{norm}"
|
|
85
|
+
if k == "column":
|
|
86
|
+
if a.get("candidates"):
|
|
87
|
+
return f"column {' | '.join(a['candidates'])} ({a.get('how')}, one per model){norm}"
|
|
88
|
+
return f"column {a['column']} ({a.get('how')}){norm}"
|
|
89
|
+
if k in ("config", "env"):
|
|
90
|
+
return f"{k} '{a['key']}' default {a.get('default')!r}"
|
|
91
|
+
if k == "literal":
|
|
92
|
+
src = f" ({a['from']})" if a.get("from") else ""
|
|
93
|
+
return f"literal {a.get('value')!r}{src}"
|
|
94
|
+
if k == "call":
|
|
95
|
+
return f"call {short(a['target'])}()"
|
|
96
|
+
if k == "param":
|
|
97
|
+
return f"param ${a['name']} (no resolvable callers)"
|
|
98
|
+
return f"expr {a.get('text')}"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _matches(attrs: dict, forms: set[str], lang: str) -> str | None:
|
|
102
|
+
tname = attrs.get("target_name") or attrs.get("target") or ""
|
|
103
|
+
if lang == "ts":
|
|
104
|
+
m = re.findall(r"[A-Za-z_$][\w$]*", tname)
|
|
105
|
+
if m and head_token(m[-1]) in forms:
|
|
106
|
+
return "target"
|
|
107
|
+
return None
|
|
108
|
+
if head_token(tname) in forms:
|
|
109
|
+
return "target"
|
|
110
|
+
sig = attrs.get("signature") or []
|
|
111
|
+
if sig:
|
|
112
|
+
k, _, key = sig[0].partition(":")
|
|
113
|
+
if k in SOURCE_KINDS and head_token(key) in forms:
|
|
114
|
+
return "first-source"
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _routes_reaching(st: GraphStore, fn: str, max_depth=12) -> list[str]:
|
|
119
|
+
from .query import reverse_closure
|
|
120
|
+
d = reverse_closure(st, [fn], kinds=CALL_LIKE_UP, max_depth=max_depth)
|
|
121
|
+
return sorted(n for n in d if n.startswith("route:"))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _diff(a: list[str], b: list[str]) -> dict:
|
|
125
|
+
i = 0
|
|
126
|
+
while i < min(len(a), len(b)) and a[i] == b[i]:
|
|
127
|
+
i += 1
|
|
128
|
+
return {"common_prefix": a[:i], "a_next": a[i] if i < len(a) else None, "b_next": b[i] if i < len(b) else None,
|
|
129
|
+
"final_a": a[-1] if a else None, "final_b": b[-1] if b else None}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def resolutions(st: GraphStore, concept: str, within: str | None = None, client: bool = True) -> dict:
|
|
133
|
+
forms = concept_forms(concept)
|
|
134
|
+
w = within.lower() if within else None
|
|
135
|
+
rows = st.q("SELECT id, name, file, line, lang, attrs FROM nodes WHERE kind='resolution'")
|
|
136
|
+
backend, frontend = [], []
|
|
137
|
+
for r in rows:
|
|
138
|
+
a = json.loads(r["attrs"] or "{}")
|
|
139
|
+
how = _matches(a, forms, r["lang"])
|
|
140
|
+
if not how:
|
|
141
|
+
continue
|
|
142
|
+
site = {"id": r["id"], "fn": a.get("fn"), "target": a.get("target"), "form": a.get("form"), "file": r["file"],
|
|
143
|
+
"line": r["line"], "signature": a.get("signature") or [], "chain": a.get("chain") or [],
|
|
144
|
+
"final_literal": a.get("final_literal"), "match": how, "repo": a.get("repo")}
|
|
145
|
+
(frontend if r["lang"] == "ts" else backend).append(site)
|
|
146
|
+
if w:
|
|
147
|
+
backend = [s for s in backend if w in (s["fn"] or "").lower() or w in (s["file"] or "").lower()]
|
|
148
|
+
backend.sort(key=lambda s: (s["file"] or "", s["line"] or 0))
|
|
149
|
+
# chains by signature
|
|
150
|
+
groups: dict[tuple, list] = defaultdict(list)
|
|
151
|
+
for s in backend:
|
|
152
|
+
groups[tuple(s["signature"])].append(s)
|
|
153
|
+
chains = []
|
|
154
|
+
for i, (sig, sites) in enumerate(sorted(groups.items(), key=lambda kv: (-len(kv[1]), kv[0]))):
|
|
155
|
+
label = chr(ord("A") + i) if i < 26 else chr(ord("A") + i // 26 - 1) + chr(ord("A") + i % 26)
|
|
156
|
+
for s in sites:
|
|
157
|
+
s["chain_label"] = label
|
|
158
|
+
chains.append({"label": label, "signature": list(sig), "sites": [s["id"] for s in sites]})
|
|
159
|
+
# request-driven sites: chain contains a request key of the concept
|
|
160
|
+
concept_keys = sorted({x.split(":", 1)[1] for s in backend for x in s["signature"]
|
|
161
|
+
if x.startswith("input:") and head_token(x.split(":", 1)[1]) in forms})
|
|
162
|
+
req_sites = [s for s in backend if any(x.startswith("input:") and x.split(":", 1)[1] in concept_keys for x in s["signature"])]
|
|
163
|
+
# routes per site (who can trigger it)
|
|
164
|
+
for s in backend:
|
|
165
|
+
s["routes"] = _routes_reaching(st, s["fn"]) if s["fn"] else []
|
|
166
|
+
divergence = []
|
|
167
|
+
by_label = {c["label"]: c for c in chains}
|
|
168
|
+
req_labels = sorted({s["chain_label"] for s in req_sites}, key=lambda x: (len(x), x))
|
|
169
|
+
for i, la in enumerate(req_labels):
|
|
170
|
+
for lb in req_labels[i + 1:]:
|
|
171
|
+
d = _diff(by_label[la]["signature"], by_label[lb]["signature"])
|
|
172
|
+
divergence.append({"a": la, "b": lb, **d})
|
|
173
|
+
res = {"concept": concept, "forms": sorted(forms), "within": within, "backend_sites": backend, "chains": chains,
|
|
174
|
+
"concept_request_keys": concept_keys, "request_driven_chains": req_labels, "divergence": divergence,
|
|
175
|
+
"client": [], "client_fallbacks": []}
|
|
176
|
+
if client:
|
|
177
|
+
res["client"], res["client_fallbacks"] = _client_side(st, req_sites, concept_keys, frontend, forms)
|
|
178
|
+
res["not_forwarded"] = _not_forwarded(st, res, forms)
|
|
179
|
+
return res
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _not_forwarded(st: GraphStore, res: dict, forms: set[str]) -> list[dict]:
|
|
183
|
+
"""Keys a client call site passes that the request it calls never sends: for the requests listed under CLIENT,
|
|
184
|
+
and every gap whose dropped key is a form of the concept (e.g. date_from, which then has no backend site at all)."""
|
|
185
|
+
from .query import forwarding_gaps
|
|
186
|
+
issuers = {rq["issuer"] for it in res["client"] for rq in it["requests"]}
|
|
187
|
+
c = res["concept"].lower()
|
|
188
|
+
out = []
|
|
189
|
+
for g in forwarding_gaps(st):
|
|
190
|
+
concept_hit = [k for k in g["dropped"] if head_token(k) in forms or c in k.lower()]
|
|
191
|
+
if g["issuer"] in issuers or concept_hit:
|
|
192
|
+
out.append({**g, "concept_keys": concept_hit})
|
|
193
|
+
return out
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _key_status(keys: dict | None, k: str) -> str:
|
|
197
|
+
if not keys:
|
|
198
|
+
return "absent"
|
|
199
|
+
if keys.get("opaque") and k not in (keys.get("keys") or []) + (keys.get("conditional") or []):
|
|
200
|
+
return "unknown"
|
|
201
|
+
if k in (keys.get("keys") or []):
|
|
202
|
+
return "always"
|
|
203
|
+
if k in (keys.get("conditional") or []):
|
|
204
|
+
return "conditional"
|
|
205
|
+
return "absent"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _client_side(st: GraphStore, req_sites: list[dict], concept_keys: list[str], frontend: list[dict], forms: set[str]):
|
|
209
|
+
route_sites = defaultdict(list)
|
|
210
|
+
for s in req_sites:
|
|
211
|
+
for r in s["routes"]:
|
|
212
|
+
route_sites[r].append(s)
|
|
213
|
+
if not route_sites:
|
|
214
|
+
return [], []
|
|
215
|
+
eps = defaultdict(set)
|
|
216
|
+
for r in route_sites:
|
|
217
|
+
for e in st.q("SELECT src, confidence FROM edges WHERE kind='MATCHES_ROUTE' AND dst=?", (r,)):
|
|
218
|
+
eps[e["src"]].add(r)
|
|
219
|
+
fb_by_owner = defaultdict(list)
|
|
220
|
+
for f in frontend:
|
|
221
|
+
fb_by_owner[f["fn"]].append(f)
|
|
222
|
+
out, used_fb = [], {}
|
|
223
|
+
for ep in sorted(eps):
|
|
224
|
+
routes = sorted(eps[ep])
|
|
225
|
+
sites = sorted({(s["chain_label"], s["id"]) for r in routes for s in route_sites[r]})
|
|
226
|
+
item = {"endpoint": ep, "routes": routes, "backend_sites": [{"chain": l, "site": i} for l, i in sites], "requests": []}
|
|
227
|
+
for h in st.q("SELECT src, file, line, attrs FROM edges WHERE kind='HTTP_CALLS' AND dst=?", (ep,)):
|
|
228
|
+
ha = json.loads(h["attrs"] or "{}")
|
|
229
|
+
fwd = next((x for x in (ha.get("query_keys"), ha.get("body_keys")) if x and x.get("forwarded")), None) or {}
|
|
230
|
+
req = {"issuer": h["src"], "at": f"{h['file']}:{h['line']}", "keys": {}, "callers": [],
|
|
231
|
+
"forwarded": fwd.get("forwarded"), "forwarded_index": fwd.get("forwarded_index")}
|
|
232
|
+
for k in concept_keys:
|
|
233
|
+
req["keys"][k] = max(_key_status(ha.get("query_keys"), k), _key_status(ha.get("body_keys"), k),
|
|
234
|
+
key=["absent", "unknown", "conditional", "always"].index)
|
|
235
|
+
owners = [h["src"]]
|
|
236
|
+
for c in st.q("SELECT src, file, line, attrs FROM edges WHERE kind='CALLS' AND dst=?", (h["src"],)):
|
|
237
|
+
ca = json.loads(c["attrs"] or "{}")
|
|
238
|
+
ak = ca.get("arg_keys")
|
|
239
|
+
akc = ca.get("arg_keys_conditional") or []
|
|
240
|
+
akp = ca.get("arg_keys_partial") or []
|
|
241
|
+
if isinstance(akp, bool): # older DBs
|
|
242
|
+
akp = [akp] * len(ak or [])
|
|
243
|
+
fi = req["forwarded_index"]
|
|
244
|
+
if ak is not None and isinstance(fi, int):
|
|
245
|
+
# the issuer forwards one parameter into the request: only that argument position matters
|
|
246
|
+
one = ak[fi] if fi < len(ak) else None
|
|
247
|
+
ak = None if one is None else [one]
|
|
248
|
+
akc = [akc[fi]] if fi < len(akc) and akc[fi] else []
|
|
249
|
+
akp = [akp[fi]] if fi < len(akp) else []
|
|
250
|
+
passed = sorted({x for ks in (ak or []) for x in (ks or [])})
|
|
251
|
+
cond = sorted({x for ks in akc for x in (ks or [])})
|
|
252
|
+
partial = any(akp)
|
|
253
|
+
|
|
254
|
+
def sends(k):
|
|
255
|
+
if ak is None:
|
|
256
|
+
return None
|
|
257
|
+
if k in passed:
|
|
258
|
+
return True
|
|
259
|
+
if k in cond:
|
|
260
|
+
return "conditional"
|
|
261
|
+
return None if partial else False
|
|
262
|
+
req["callers"].append({"caller": c["src"], "at": f"{c['file']}:{c['line']}", "arg_keys": passed if ak is not None else None,
|
|
263
|
+
"arg_keys_conditional": cond or None, "sends": {k: sends(k) for k in concept_keys}})
|
|
264
|
+
owners.append(c["src"])
|
|
265
|
+
fbs = []
|
|
266
|
+
for o in owners:
|
|
267
|
+
for f in fb_by_owner.get(o, []):
|
|
268
|
+
fbs.append(f)
|
|
269
|
+
used_fb[f["id"]] = f
|
|
270
|
+
req["client_fallbacks"] = [{"at": f"{f['file']}:{f['line']}", "expr": " ?? ".join(f["signature"][:-1]) if f["signature"] else None,
|
|
271
|
+
"literal": f["final_literal"], "owner": f["fn"]} for f in fbs]
|
|
272
|
+
# verdict per key: sent / maybe / never
|
|
273
|
+
for k in concept_keys:
|
|
274
|
+
st_ = req["keys"][k]
|
|
275
|
+
if st_ == "always":
|
|
276
|
+
v = "sent"
|
|
277
|
+
elif st_ == "absent":
|
|
278
|
+
v = "never sent (request builder has no such key)"
|
|
279
|
+
elif st_ == "unknown" and not any(c["sends"].get(k) for c in req["callers"]):
|
|
280
|
+
v = "unknown (request params not statically known)"
|
|
281
|
+
else:
|
|
282
|
+
vals = [c["sends"].get(k) for c in req["callers"]]
|
|
283
|
+
unknown = sum(1 for x in vals if x is None)
|
|
284
|
+
if any(x is True for x in vals):
|
|
285
|
+
v = "sent by some call sites"
|
|
286
|
+
elif any(x == "conditional" for x in vals):
|
|
287
|
+
v = "sent conditionally by some call sites"
|
|
288
|
+
elif vals and not unknown:
|
|
289
|
+
v = "never sent (builder key is conditional and no call site passes it)"
|
|
290
|
+
elif vals and unknown < len(vals):
|
|
291
|
+
v = f"not sent by {len(vals) - unknown} call site(s) with known keys; {unknown} unknown"
|
|
292
|
+
else:
|
|
293
|
+
v = "conditional (call-site keys unknown)"
|
|
294
|
+
req.setdefault("verdict", {})[k] = v
|
|
295
|
+
item["requests"].append(req)
|
|
296
|
+
out.append(item)
|
|
297
|
+
return out, sorted(used_fb.values(), key=lambda f: (f["file"], f["line"]))
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def render_resolutions(res: dict, max_sites_per_chain=8, show_client=True, compact=False) -> str:
|
|
301
|
+
L = [f"concept: {res['concept']} (head-word forms: {', '.join(res['forms'])})" + (f" within: {res['within']}" if res.get("within") else ""),
|
|
302
|
+
f"backend resolution sites: {len(res['backend_sites'])} in {len(res['chains'])} distinct fallback chains; "
|
|
303
|
+
f"request keys of the concept: {', '.join(res['concept_request_keys']) or '-'}; "
|
|
304
|
+
f"request-driven chains: {', '.join(res['request_driven_chains']) or '-'}"]
|
|
305
|
+
sites = {s["id"]: s for s in res["backend_sites"]}
|
|
306
|
+
L += ["", "== FALLBACK CHAINS (deterministic, grouped by signature)"]
|
|
307
|
+
for c in res["chains"]:
|
|
308
|
+
L.append(f"[{c['label']}] {' > '.join(c['signature'])} ({len(c['sites'])} site{'s' if len(c['sites']) != 1 else ''})")
|
|
309
|
+
for sid in c["sites"][:max_sites_per_chain]:
|
|
310
|
+
s = sites[sid]
|
|
311
|
+
L.append(f" {short(s['fn'])} {s['target']} ({s['form']}) @{s['file']}:{s['line']}")
|
|
312
|
+
i = 0
|
|
313
|
+
seen_atoms = set()
|
|
314
|
+
for a in flat_atoms(s["chain"]):
|
|
315
|
+
if a["kind"] == "literal" and a.get("value") in (None, "", [], False):
|
|
316
|
+
continue
|
|
317
|
+
ak = json.dumps({kk: vv for kk, vv in a.items() if kk in ("kind", "key", "column", "value", "text")}, sort_keys=True)
|
|
318
|
+
if ak in seen_atoms: # repeated source (e.g. `($a ?? $fb) ?: $fb`): already listed
|
|
319
|
+
continue
|
|
320
|
+
seen_atoms.add(ak)
|
|
321
|
+
i += 1
|
|
322
|
+
L.append(f" {i}. {atom_text(a)} @{a.get('at')}")
|
|
323
|
+
if s.get("routes"):
|
|
324
|
+
L.append(f" reached from {len(s['routes'])} route(s): {', '.join(r[6:] for r in s['routes'][:4])}{' …' if len(s['routes']) > 4 else ''}")
|
|
325
|
+
if len(c["sites"]) > max_sites_per_chain:
|
|
326
|
+
L.append(f" … +{len(c['sites']) - max_sites_per_chain} more")
|
|
327
|
+
if res["divergence"]:
|
|
328
|
+
L += ["", f"== DIVERGENCE between request-driven chains ({', '.join(res['request_driven_chains'])})"]
|
|
329
|
+
for d in res["divergence"]:
|
|
330
|
+
cp = " > ".join(d["common_prefix"]) or "(nothing)"
|
|
331
|
+
L.append(f" [{d['a']}] vs [{d['b']}]: same up to {cp}; then [{d['a']}] {d['a_next'] or '(end)'} vs [{d['b']}] {d['b_next'] or '(end)'}"
|
|
332
|
+
+ (f"; final fallback {d['final_a']} vs {d['final_b']}" if d["final_a"] != d["final_b"] else f"; same final fallback {d['final_a']}"))
|
|
333
|
+
if show_client and res["client"]:
|
|
334
|
+
L += ["", "== CLIENT (frontend endpoints reaching request-driven sites)"]
|
|
335
|
+
for it in res["client"]:
|
|
336
|
+
labels = sorted({b["chain"] for b in it["backend_sites"]})
|
|
337
|
+
L.append(f" {it['endpoint'][5:]} -> chain {', '.join(labels)} via {', '.join(r[6:] for r in it['routes'][:2])}")
|
|
338
|
+
if compact: # one line per request: issuer, verdict, client fallbacks
|
|
339
|
+
for rq in it["requests"]:
|
|
340
|
+
v = "; ".join(f"'{k}': {x}" for k, x in (rq.get("verdict") or {}).items())
|
|
341
|
+
fb = "".join(f"; client fallback @{f['at']}: {f['expr']} ?? {f['literal']!r}" for f in rq["client_fallbacks"][:2])
|
|
342
|
+
L.append(f" {short(rq['issuer'])} @{rq['at']} ({len(rq['callers'])} caller(s)) => {v}{fb}")
|
|
343
|
+
continue
|
|
344
|
+
for rq in it["requests"]:
|
|
345
|
+
keys = ", ".join(f"{k}: {v}" for k, v in rq["keys"].items()) + (f" (forwards parameter `{rq['forwarded']}`)" if rq.get("forwarded") else "")
|
|
346
|
+
L.append(f" request {short(rq['issuer'])} @{rq['at']} builder keys: {keys}")
|
|
347
|
+
for c in rq["callers"][:6]:
|
|
348
|
+
sends = ", ".join(f"{k}={'yes' if v is True else ('conditional' if v == 'conditional' else ('no' if v is False else '?'))}" for k, v in c["sends"].items())
|
|
349
|
+
L.append(f" called by {short(c['caller'])} @{c['at']} sends {sends}" + (f" (passes: {', '.join(c['arg_keys'])})" if c["arg_keys"] else ""))
|
|
350
|
+
for f in rq["client_fallbacks"][:4]:
|
|
351
|
+
L.append(f" client fallback @{f['at']}: {f['expr']} ?? {f['literal']!r}")
|
|
352
|
+
for k, v in (rq.get("verdict") or {}).items():
|
|
353
|
+
L.append(f" => '{k}': {v}")
|
|
354
|
+
if show_client and res.get("not_forwarded"):
|
|
355
|
+
L += ["", "== SENT BUT NOT FORWARDED (a call site passes the key; the request it calls never sends it)"]
|
|
356
|
+
for g in res["not_forwarded"]:
|
|
357
|
+
L.append(f" {short(g['caller'])} @{g['call_at']} passes {', '.join(g['dropped'])} to {short(g['issuer'])}; "
|
|
358
|
+
f"the request @{g['request_at']} ({g['endpoint'][5:]}) sends only {', '.join(g['request_keys'])}")
|
|
359
|
+
elif show_client and not res["backend_sites"] and not res["client"]:
|
|
360
|
+
L += ["", f"no resolution sites match {res['concept']!r}. try: a head word such as timezone, locale, currency or store; "
|
|
361
|
+
"search() / `cg search` for the name"]
|
|
362
|
+
return "\n".join(L)
|