ckgraphify 0.1.3__tar.gz → 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/PKG-INFO +1 -1
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/ckgraphify.egg-info/SOURCES.txt +2 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/__main__.py +40 -15
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/business_map.py +65 -5
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/graph_main_backend.py +158 -31
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/graph_main_frontend.py +689 -31
- ckgraphify-0.1.4/graphify/graph_main_frontend_sdk.py +500 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/graph_main_html.py +1 -1
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/graph_main_merge.py +66 -3
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/graph_main_trace.py +68 -7
- ckgraphify-0.1.4/graphify/repo_registry.py +222 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/pyproject.toml +1 -1
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/skill/skill-codex.md +24 -8
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/skill/skill.md +12 -17
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/LICENSE +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/MANIFEST.in +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/README.md +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/__init__.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/analyze.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/benchmark.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/bridge_mtop.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/build.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/cache.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/callflow_html.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/cluster.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/dedup.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/detect.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/export.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/extract.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/global_graph.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/google_workspace.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/hooks.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/ingest.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/llm.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/manifest.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/report.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/security.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/serve.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/transcribe.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/tree_html.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/validate.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/watch.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/graphify/wiki.py +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/setup.cfg +0 -0
- {ckgraphify-0.1.3 → ckgraphify-0.1.4}/skill/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ckgraphify
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: AI coding assistant skill for Claude Code and Codex - graph-main boundary graphs, multi-repo call chains, business-map concepts, and business search
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/safishamsi/graphify
|
|
@@ -20,6 +20,7 @@ graphify/global_graph.py
|
|
|
20
20
|
graphify/google_workspace.py
|
|
21
21
|
graphify/graph_main_backend.py
|
|
22
22
|
graphify/graph_main_frontend.py
|
|
23
|
+
graphify/graph_main_frontend_sdk.py
|
|
23
24
|
graphify/graph_main_html.py
|
|
24
25
|
graphify/graph_main_merge.py
|
|
25
26
|
graphify/graph_main_trace.py
|
|
@@ -27,6 +28,7 @@ graphify/hooks.py
|
|
|
27
28
|
graphify/ingest.py
|
|
28
29
|
graphify/llm.py
|
|
29
30
|
graphify/manifest.py
|
|
31
|
+
graphify/repo_registry.py
|
|
30
32
|
graphify/report.py
|
|
31
33
|
graphify/security.py
|
|
32
34
|
graphify/serve.py
|
|
@@ -570,10 +570,11 @@ def main() -> None:
|
|
|
570
570
|
print(" merge-main-graphs <graph-main.json...> merge multiple graph-main boundary graphs")
|
|
571
571
|
print(" --out <path> output path (default: graphify-out/graph-main-merged.json)")
|
|
572
572
|
print(" --report <path> report path (default: graphify-out/graph-main-merged-report.md)")
|
|
573
|
+
print(" repo-list [path] create/update editable repo-list.json from ./repos git remotes")
|
|
573
574
|
print(" main-trace --graph <path> --from <node> [--api <api>] [--max-depth N] [--sources] [--prefer-repo R] [--exclude-repo R]")
|
|
574
575
|
print(" trace a graph-main chain from an exact entry/API")
|
|
575
|
-
print(" business-init --concept <name> [--out graphify-out/business-map.json]")
|
|
576
|
-
print(" create a business-map
|
|
576
|
+
print(" business-init --concept <name> [--out graphify-out/business-map.json] [--force]")
|
|
577
|
+
print(" create a business-map or append an unexplored concept")
|
|
577
578
|
print(" business-show [--map <path>] [--concept <name>] [--scenario <name>]")
|
|
578
579
|
print(" inspect concepts, scenarios, anchors, and trace hints")
|
|
579
580
|
print(" business-query \"question\" [--map <path>] [--graph <path>] [--trace] [--sources] [--format json|text]")
|
|
@@ -1357,6 +1358,7 @@ def main() -> None:
|
|
|
1357
1358
|
f"repos={stats.repo_count}, nodes={stats.nodes}, edges={stats.edges}, "
|
|
1358
1359
|
f"cross_edges={stats.cross_edges}, mtop={stats.mtop_links}, rest={stats.rest_links}, "
|
|
1359
1360
|
f"hsf_method={stats.hsf_method_links}, hsf_api={stats.hsf_api_links}, metaq={stats.metaq_links}, "
|
|
1361
|
+
f"npm_export={stats.npm_export_links}, "
|
|
1360
1362
|
f"unresolved_dependencies={stats.unresolved_dependencies}"
|
|
1361
1363
|
)
|
|
1362
1364
|
print(f"Graph: {out_path}")
|
|
@@ -1723,6 +1725,12 @@ def main() -> None:
|
|
|
1723
1725
|
args = sys.argv[2:]
|
|
1724
1726
|
from graphify.business_map import default_business_graph_path, default_business_map_path
|
|
1725
1727
|
|
|
1728
|
+
def _require_business_trace_value(option: str, index: int) -> str:
|
|
1729
|
+
if index + 1 >= len(args) or args[index + 1].startswith("--"):
|
|
1730
|
+
print(f"error: business-trace option {option} requires a value", file=sys.stderr)
|
|
1731
|
+
sys.exit(1)
|
|
1732
|
+
return args[index + 1]
|
|
1733
|
+
|
|
1726
1734
|
map_path = default_business_map_path()
|
|
1727
1735
|
graph_path: Path | None = None
|
|
1728
1736
|
concept = ""
|
|
@@ -1733,29 +1741,29 @@ def main() -> None:
|
|
|
1733
1741
|
i = 0
|
|
1734
1742
|
while i < len(args):
|
|
1735
1743
|
a = args[i]
|
|
1736
|
-
if a == "--map"
|
|
1737
|
-
map_path = Path(
|
|
1744
|
+
if a == "--map":
|
|
1745
|
+
map_path = Path(_require_business_trace_value(a, i)); i += 2
|
|
1738
1746
|
elif a.startswith("--map="):
|
|
1739
1747
|
map_path = Path(a.split("=", 1)[1]); i += 1
|
|
1740
|
-
elif a == "--graph"
|
|
1741
|
-
graph_path = Path(
|
|
1748
|
+
elif a == "--graph":
|
|
1749
|
+
graph_path = Path(_require_business_trace_value(a, i)); i += 2
|
|
1742
1750
|
elif a.startswith("--graph="):
|
|
1743
1751
|
graph_path = Path(a.split("=", 1)[1]); i += 1
|
|
1744
|
-
elif a == "--concept"
|
|
1745
|
-
concept =
|
|
1752
|
+
elif a == "--concept":
|
|
1753
|
+
concept = _require_business_trace_value(a, i); i += 2
|
|
1746
1754
|
elif a.startswith("--concept="):
|
|
1747
1755
|
concept = a.split("=", 1)[1]; i += 1
|
|
1748
|
-
elif a == "--scenario"
|
|
1749
|
-
scenario =
|
|
1756
|
+
elif a == "--scenario":
|
|
1757
|
+
scenario = _require_business_trace_value(a, i); i += 2
|
|
1750
1758
|
elif a.startswith("--scenario="):
|
|
1751
1759
|
scenario = a.split("=", 1)[1]; i += 1
|
|
1752
|
-
elif a == "--flow"
|
|
1753
|
-
flow =
|
|
1760
|
+
elif a == "--flow":
|
|
1761
|
+
flow = _require_business_trace_value(a, i); i += 2
|
|
1754
1762
|
elif a.startswith("--flow="):
|
|
1755
1763
|
flow = a.split("=", 1)[1]; i += 1
|
|
1756
|
-
elif a == "--max-depth"
|
|
1764
|
+
elif a == "--max-depth":
|
|
1757
1765
|
try:
|
|
1758
|
-
max_depth = int(
|
|
1766
|
+
max_depth = int(_require_business_trace_value(a, i))
|
|
1759
1767
|
except ValueError:
|
|
1760
1768
|
print("error: --max-depth must be an integer", file=sys.stderr)
|
|
1761
1769
|
sys.exit(1)
|
|
@@ -1921,6 +1929,22 @@ def main() -> None:
|
|
|
1921
1929
|
if thin_out is not None:
|
|
1922
1930
|
print(f"Thin graph: {thin_out}")
|
|
1923
1931
|
|
|
1932
|
+
elif cmd == "repo-list":
|
|
1933
|
+
start = Path(sys.argv[2]) if len(sys.argv) > 2 else Path(".")
|
|
1934
|
+
if len(sys.argv) > 3:
|
|
1935
|
+
print("Usage: graphify repo-list [path]", file=sys.stderr)
|
|
1936
|
+
sys.exit(1)
|
|
1937
|
+
try:
|
|
1938
|
+
from graphify.repo_registry import ensure_repo_list as _ensure_repo_list
|
|
1939
|
+
|
|
1940
|
+
list_path = _ensure_repo_list(start.resolve())
|
|
1941
|
+
data = json.loads(list_path.read_text(encoding="utf-8"))
|
|
1942
|
+
repos = data.get("repos", []) if isinstance(data, dict) else []
|
|
1943
|
+
print(f"Repo list: {list_path} ({len(repos)} repos)")
|
|
1944
|
+
except Exception as exc:
|
|
1945
|
+
print(f"error: repo-list failed: {exc}", file=sys.stderr)
|
|
1946
|
+
sys.exit(1)
|
|
1947
|
+
|
|
1924
1948
|
elif cmd == "main-graph":
|
|
1925
1949
|
args = sys.argv[2:]
|
|
1926
1950
|
root = Path(".")
|
|
@@ -2095,7 +2119,8 @@ def main() -> None:
|
|
|
2095
2119
|
print(
|
|
2096
2120
|
"Node counts: "
|
|
2097
2121
|
f"page={stats.page_nodes}, component={stats.component_nodes}, "
|
|
2098
|
-
f"mtop_api={stats.mtop_api_nodes}, rest_api={stats.rest_api_nodes}"
|
|
2122
|
+
f"mtop_api={stats.mtop_api_nodes}, rest_api={stats.rest_api_nodes}, "
|
|
2123
|
+
f"sdk_export={stats.sdk_export_nodes}, sdk_dependency={stats.sdk_dependency_nodes}"
|
|
2099
2124
|
)
|
|
2100
2125
|
print(f"HTML: {html_out}")
|
|
2101
2126
|
print(f"Report: {report_out}")
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
5
|
import re
|
|
6
|
+
import hashlib
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
@@ -220,6 +221,49 @@ def health_card_seed() -> dict:
|
|
|
220
221
|
}
|
|
221
222
|
|
|
222
223
|
|
|
224
|
+
def _concept_id_from_name(name: str) -> str:
|
|
225
|
+
raw = str(name or "").strip()
|
|
226
|
+
if _matches_text(raw, "健康卡", ["health_card", "healthCard", "health card"]):
|
|
227
|
+
return "health_card"
|
|
228
|
+
lowered = raw.lower()
|
|
229
|
+
if "ai" in lowered and "找药" in raw:
|
|
230
|
+
return "ai_find_drug"
|
|
231
|
+
ascii_parts = _ascii_terms(raw)
|
|
232
|
+
slug = "_".join(ascii_parts)
|
|
233
|
+
slug = re.sub(r"[^a-z0-9_]+", "_", slug).strip("_")
|
|
234
|
+
if slug and not slug[0].isdigit():
|
|
235
|
+
return slug[:48]
|
|
236
|
+
digest = hashlib.sha1(raw.encode("utf-8")).hexdigest()[:8]
|
|
237
|
+
return f"concept_{digest}"
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def concept_seed(concept: str) -> dict:
|
|
241
|
+
name = str(concept or "").strip()
|
|
242
|
+
if not name:
|
|
243
|
+
raise ValueError("concept is required")
|
|
244
|
+
aliases: list[str] = []
|
|
245
|
+
if _concept_id_from_name(name) == "ai_find_drug":
|
|
246
|
+
aliases = ["闪购AI找药", "千问找药"]
|
|
247
|
+
return {
|
|
248
|
+
"id": _concept_id_from_name(name),
|
|
249
|
+
"name": name,
|
|
250
|
+
"aliases": aliases,
|
|
251
|
+
"status": "unexplored",
|
|
252
|
+
"summary": f"待探索{name}相关业务场景、入口、API、后端承接链路和边界。",
|
|
253
|
+
"scenarios": [],
|
|
254
|
+
"gaps": ["缺少场景、入口 repo、页面/API 和后端锚点。"],
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _business_map_seed() -> dict:
|
|
259
|
+
return {
|
|
260
|
+
"version": 1,
|
|
261
|
+
"kind": "business-map",
|
|
262
|
+
"description": "Business concept map layered on top of graph-main facts.",
|
|
263
|
+
"concepts": [],
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
223
267
|
def load_business_map(path: Path) -> dict:
|
|
224
268
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
225
269
|
if not isinstance(data, dict):
|
|
@@ -331,11 +375,27 @@ def default_business_graph_path(map_path: Path, start: Path | None = None) -> Pa
|
|
|
331
375
|
|
|
332
376
|
|
|
333
377
|
def init_business_map(concept: str, out_path: Path, *, force: bool = False) -> dict:
|
|
334
|
-
if out_path.exists()
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
378
|
+
if force or not out_path.exists():
|
|
379
|
+
if _matches_text(concept, "健康卡", ["health_card", "healthCard", "health card"]):
|
|
380
|
+
data = health_card_seed()
|
|
381
|
+
else:
|
|
382
|
+
data = _business_map_seed()
|
|
383
|
+
data["concepts"].append(concept_seed(concept))
|
|
384
|
+
write_business_map(data, out_path)
|
|
385
|
+
return data
|
|
386
|
+
|
|
387
|
+
data = load_business_map(out_path)
|
|
388
|
+
concepts = data.setdefault("concepts", [])
|
|
389
|
+
if not isinstance(concepts, list):
|
|
390
|
+
raise ValueError("business map concepts must be a list")
|
|
391
|
+
|
|
392
|
+
candidate = (
|
|
393
|
+
health_card_seed()["concepts"][0]
|
|
394
|
+
if _matches_text(concept, "健康卡", ["health_card", "healthCard", "health card"])
|
|
395
|
+
else concept_seed(concept)
|
|
396
|
+
)
|
|
397
|
+
if not any(isinstance(item, dict) and _concept_matches(item, concept) for item in concepts):
|
|
398
|
+
concepts.append(candidate)
|
|
339
399
|
write_business_map(data, out_path)
|
|
340
400
|
return data
|
|
341
401
|
|
|
@@ -7,6 +7,8 @@ from dataclasses import dataclass
|
|
|
7
7
|
from datetime import datetime, timezone
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
+
from graphify.repo_registry import repo_metadata_for_root
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
_JAVA_EXTS = {".java"}
|
|
12
14
|
_SKIP_DIRS = {
|
|
@@ -96,12 +98,24 @@ _METAQ_CONSUMER_RE = re.compile(r"@(?:MetaqConsumer|MetaQConsumer|MetaqListener|
|
|
|
96
98
|
_METAQ_PRODUCER_RE = re.compile(r"(?:Metaq|MetaQ).{0,40}(?:send|publish|produce)", re.I | re.S)
|
|
97
99
|
_TOPIC_RE = re.compile(r"""topic\s*=\s*["']([A-Za-z0-9_.-]+)["']""")
|
|
98
100
|
_STR_ASSIGN_RE = re.compile(r'(?m)\bString\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"([A-Za-z0-9_.-]+)"\s*;')
|
|
101
|
+
_STATIC_STRING_CONST_RE = re.compile(
|
|
102
|
+
r'(?m)\b(?:public|protected|private)?\s*static\s+final\s+String\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"([^"]+)"\s*;'
|
|
103
|
+
)
|
|
99
104
|
_BINDING_HEADER_RE = re.compile(r"\bBinding\.header\s*\(([^)]*)\)")
|
|
100
105
|
_SET_LISTENER_RE = re.compile(r"\.setMessageListener\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)")
|
|
101
106
|
_ROCKETMQ_CONSUMER_IFACE_RE = re.compile(r"\bMessageListenerConcurrently\b")
|
|
102
107
|
_ROCKETMQ_SEND_CALL_RE = re.compile(r"\.send\s*\(")
|
|
103
108
|
_NEW_MESSAGE_TOPIC_RE = re.compile(r"\bnew\s+Message\s*\(\s*([A-Za-z0-9_$.\"']+)\s*,")
|
|
109
|
+
_MESSAGE_SENDER_SEND_RE = re.compile(
|
|
110
|
+
r"\b[A-Za-z_][A-Za-z0-9_]*\s*\.\s*sendMessage\s*\(\s*"
|
|
111
|
+
r"([A-Za-z0-9_$.\"']+)\s*,\s*([A-Za-z0-9_$.\"']+)",
|
|
112
|
+
re.S,
|
|
113
|
+
)
|
|
114
|
+
_COMPONENT_VALUE_RE = re.compile(r'@(?:Component|Service)\s*\([^)]*(?:value\s*=\s*)?"([^"]+)"')
|
|
104
115
|
_TOPIC_TOKEN_RE = re.compile(r"\b([A-Za-z0-9_$.]*TOPIC[A-Za-z0-9_$.]*)\b")
|
|
116
|
+
_ANTX_CONSUMER_PROP_RE = re.compile(
|
|
117
|
+
r"(?m)^spring\.metaq\.consumers\[(\d+)\]\.([A-Za-z0-9_.-]+)\s*=\s*(.+?)\s*$"
|
|
118
|
+
)
|
|
105
119
|
|
|
106
120
|
|
|
107
121
|
def _safe_id(s: str) -> str:
|
|
@@ -314,6 +328,77 @@ def _extract_notify_consumers(body_text: str, fields: tuple["FieldInfo", ...]) -
|
|
|
314
328
|
return out
|
|
315
329
|
|
|
316
330
|
|
|
331
|
+
def _constant_key(package: str, class_name: str, const_name: str) -> str:
|
|
332
|
+
fqcn = f"{package}.{class_name}" if package else class_name
|
|
333
|
+
return f"{fqcn}.{const_name}"
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _build_string_constant_index(parsed: list["JavaFileInfo"], repo_root: Path) -> dict[str, str]:
|
|
337
|
+
constants: dict[str, str] = {}
|
|
338
|
+
for info in parsed:
|
|
339
|
+
text = _read_text(repo_root / info.rel_path)
|
|
340
|
+
for m in _STATIC_STRING_CONST_RE.finditer(text):
|
|
341
|
+
name = m.group(1)
|
|
342
|
+
value = m.group(2)
|
|
343
|
+
constants[name] = value
|
|
344
|
+
constants[f"{info.class_name}.{name}"] = value
|
|
345
|
+
constants[_constant_key(info.package, info.class_name, name)] = value
|
|
346
|
+
return constants
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _resolve_metaq_token(token: str, constants: dict[str, str]) -> str:
|
|
350
|
+
raw = str(token or "").strip()
|
|
351
|
+
if not raw:
|
|
352
|
+
return ""
|
|
353
|
+
if raw.startswith('"') and raw.endswith('"') and len(raw) >= 2:
|
|
354
|
+
return raw[1:-1]
|
|
355
|
+
return constants.get(raw) or constants.get(raw.rsplit(".", 1)[-1]) or raw
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _extract_metaq_producer_topics(body_text: str, constants: dict[str, str] | None = None) -> list[tuple[str, str]]:
|
|
359
|
+
constants = constants or {}
|
|
360
|
+
topics: list[tuple[str, str]] = []
|
|
361
|
+
for m in _NEW_MESSAGE_TOPIC_RE.finditer(body_text):
|
|
362
|
+
topics.append((_resolve_metaq_token(m.group(1), constants), ""))
|
|
363
|
+
for m in _MESSAGE_SENDER_SEND_RE.finditer(body_text):
|
|
364
|
+
topics.append((_resolve_metaq_token(m.group(1), constants), _resolve_metaq_token(m.group(2), constants)))
|
|
365
|
+
if topics:
|
|
366
|
+
return list(dict.fromkeys(t for t in topics if t[0]))
|
|
367
|
+
for m in _TOPIC_TOKEN_RE.finditer(body_text):
|
|
368
|
+
topics.append((_resolve_metaq_token(m.group(1), constants), ""))
|
|
369
|
+
return list(dict.fromkeys(t for t in topics if t[0]))
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _parse_metaq_consumer_config(repo_root: Path) -> dict[str, dict[str, str]]:
|
|
373
|
+
by_listener: dict[str, dict[str, str]] = {}
|
|
374
|
+
candidates = [
|
|
375
|
+
repo_root / "antx.properties",
|
|
376
|
+
*repo_root.glob("**/antx.properties"),
|
|
377
|
+
*repo_root.glob("**/auto-config.xml"),
|
|
378
|
+
]
|
|
379
|
+
for path in candidates:
|
|
380
|
+
if not path.exists() or path.is_dir():
|
|
381
|
+
continue
|
|
382
|
+
text = _read_text(path)
|
|
383
|
+
grouped: dict[str, dict[str, str]] = {}
|
|
384
|
+
for m in _ANTX_CONSUMER_PROP_RE.finditer(text):
|
|
385
|
+
idx = m.group(1)
|
|
386
|
+
key = m.group(2)
|
|
387
|
+
value = m.group(3).strip()
|
|
388
|
+
grouped.setdefault(idx, {})[key] = value
|
|
389
|
+
for props in grouped.values():
|
|
390
|
+
listener = props.get("message-listener-ref", "").strip()
|
|
391
|
+
if not listener:
|
|
392
|
+
continue
|
|
393
|
+
by_listener[listener.lower()] = {
|
|
394
|
+
"topic": props.get("topic", "").strip(),
|
|
395
|
+
"tag": props.get("sub-expression", "").strip(),
|
|
396
|
+
"consumer_group": props.get("consumer-group", "").strip(),
|
|
397
|
+
"config_file": _norm_rel(repo_root, path),
|
|
398
|
+
}
|
|
399
|
+
return by_listener
|
|
400
|
+
|
|
401
|
+
|
|
317
402
|
def _simple_name(fqcn: str) -> str:
|
|
318
403
|
s = (fqcn or "").strip()
|
|
319
404
|
if not s:
|
|
@@ -366,6 +451,11 @@ def _looks_like_local_service_type(fqcn: str) -> bool:
|
|
|
366
451
|
|
|
367
452
|
|
|
368
453
|
def _bean_name_for_class(info: JavaFileInfo) -> str:
|
|
454
|
+
m = _COMPONENT_VALUE_RE.search(info.class_ann)
|
|
455
|
+
if m:
|
|
456
|
+
value = m.group(1).strip()
|
|
457
|
+
if value:
|
|
458
|
+
return value
|
|
369
459
|
simple = info.class_name
|
|
370
460
|
if not simple:
|
|
371
461
|
return ""
|
|
@@ -501,21 +591,6 @@ def _direct_calls_from_method_body(method: MethodInfo) -> list[tuple[str, int]]:
|
|
|
501
591
|
return calls
|
|
502
592
|
|
|
503
593
|
|
|
504
|
-
def _extract_metaq_producer_topics(body_text: str) -> list[str]:
|
|
505
|
-
topics: list[str] = []
|
|
506
|
-
for m in _NEW_MESSAGE_TOPIC_RE.finditer(body_text):
|
|
507
|
-
tok = m.group(1).strip()
|
|
508
|
-
if tok.startswith('"') and tok.endswith('"') and len(tok) >= 2:
|
|
509
|
-
topics.append(tok[1:-1])
|
|
510
|
-
else:
|
|
511
|
-
topics.append(tok)
|
|
512
|
-
if topics:
|
|
513
|
-
return list(dict.fromkeys(topics))
|
|
514
|
-
for m in _TOPIC_TOKEN_RE.finditer(body_text):
|
|
515
|
-
topics.append(m.group(1))
|
|
516
|
-
return list(dict.fromkeys(topics))
|
|
517
|
-
|
|
518
|
-
|
|
519
594
|
def _parse_java_file(rel_path: str, text: str) -> JavaFileInfo | None:
|
|
520
595
|
pkg_m = _PACKAGE_RE.search(text)
|
|
521
596
|
package = pkg_m.group(1) if pkg_m else ""
|
|
@@ -755,6 +830,7 @@ def _build_map_impl_report(
|
|
|
755
830
|
|
|
756
831
|
def build_graph_main_backend(*, repo_root: Path, out_path: Path) -> GraphMainBackendStats:
|
|
757
832
|
repo_root = repo_root.resolve()
|
|
833
|
+
repo_meta = repo_metadata_for_root(repo_root)
|
|
758
834
|
files = _iter_java_files(repo_root)
|
|
759
835
|
parsed: list[JavaFileInfo] = []
|
|
760
836
|
for p in files:
|
|
@@ -763,6 +839,8 @@ def build_graph_main_backend(*, repo_root: Path, out_path: Path) -> GraphMainBac
|
|
|
763
839
|
if info:
|
|
764
840
|
parsed.append(info)
|
|
765
841
|
|
|
842
|
+
string_constants = _build_string_constant_index(parsed, repo_root)
|
|
843
|
+
metaq_consumer_config = _parse_metaq_consumer_config(repo_root)
|
|
766
844
|
sdk = _detect_sdk(repo_root, parsed)
|
|
767
845
|
is_sdk = bool(sdk.get("is_sdk_repo"))
|
|
768
846
|
mode = "backend-sdk" if is_sdk else "backend-normal"
|
|
@@ -1130,10 +1208,14 @@ def build_graph_main_backend(*, repo_root: Path, out_path: Path) -> GraphMainBac
|
|
|
1130
1208
|
dep_method_by_key[canonical] = nid
|
|
1131
1209
|
return nid
|
|
1132
1210
|
|
|
1133
|
-
def ensure_producer(topic: str, source_file: str, line: int) -> str:
|
|
1211
|
+
def ensure_producer(topic: str, source_file: str, line: int, *, tag: str = "") -> str:
|
|
1134
1212
|
canonical = f"metaq_producer:{topic.lower()}"
|
|
1135
1213
|
if canonical in prod_by_key:
|
|
1136
|
-
|
|
1214
|
+
existing_id = prod_by_key[canonical]
|
|
1215
|
+
existing = node_by_id.get(existing_id)
|
|
1216
|
+
if existing is not None and tag and not existing.get("tag"):
|
|
1217
|
+
existing["tag"] = tag
|
|
1218
|
+
return existing_id
|
|
1137
1219
|
nid = f"metaq_producer__{_safe_id(topic)}"
|
|
1138
1220
|
add_node(
|
|
1139
1221
|
{
|
|
@@ -1141,6 +1223,7 @@ def build_graph_main_backend(*, repo_root: Path, out_path: Path) -> GraphMainBac
|
|
|
1141
1223
|
"label": topic,
|
|
1142
1224
|
"canonical_key": canonical,
|
|
1143
1225
|
"topic": topic,
|
|
1226
|
+
"tag": tag,
|
|
1144
1227
|
"file_type": "rationale",
|
|
1145
1228
|
"source_file": source_file,
|
|
1146
1229
|
"source_location": f"L{line}",
|
|
@@ -1150,17 +1233,39 @@ def build_graph_main_backend(*, repo_root: Path, out_path: Path) -> GraphMainBac
|
|
|
1150
1233
|
prod_by_key[canonical] = nid
|
|
1151
1234
|
return nid
|
|
1152
1235
|
|
|
1153
|
-
def ensure_consumer(topic_or_ref: str, listener_ref: str, source_file: str, line: int) -> str:
|
|
1236
|
+
def ensure_consumer(topic_or_ref: str, listener_ref: str, source_file: str, line: int, *, topic: str = "", tag: str = "", consumer_group: str = "", config_file: str = "") -> str:
|
|
1154
1237
|
canonical = f"metaq_consumer:{listener_ref.lower()}"
|
|
1155
1238
|
if canonical in cons_by_key:
|
|
1156
|
-
|
|
1239
|
+
existing_id = cons_by_key[canonical]
|
|
1240
|
+
existing = node_by_id.get(existing_id)
|
|
1241
|
+
if existing is not None:
|
|
1242
|
+
for key, value in (
|
|
1243
|
+
("topic", topic),
|
|
1244
|
+
("tag", tag),
|
|
1245
|
+
("consumer_group", consumer_group),
|
|
1246
|
+
("config_file", config_file),
|
|
1247
|
+
):
|
|
1248
|
+
if value and not existing.get(key):
|
|
1249
|
+
existing[key] = value
|
|
1250
|
+
if topic and existing.get("canonical_key", "").startswith("metaq_consumer:"):
|
|
1251
|
+
existing["topic_key"] = f"metaq_topic:{topic.lower()}"
|
|
1252
|
+
return existing_id
|
|
1253
|
+
label = listener_ref
|
|
1254
|
+
if not label.endswith("#consumeMessage"):
|
|
1255
|
+
label = f"{listener_ref}#consumeMessage"
|
|
1157
1256
|
nid = f"metaq_consumer__{_safe_id(listener_ref)}"
|
|
1158
1257
|
add_node(
|
|
1159
1258
|
{
|
|
1160
1259
|
"id": nid,
|
|
1161
|
-
"label":
|
|
1260
|
+
"label": label,
|
|
1162
1261
|
"canonical_key": canonical,
|
|
1262
|
+
"topic_key": f"metaq_topic:{topic.lower()}" if topic else "",
|
|
1263
|
+
"topic": topic or topic_or_ref,
|
|
1264
|
+
"tag": tag,
|
|
1265
|
+
"consumer_group": consumer_group,
|
|
1266
|
+
"config_file": config_file,
|
|
1163
1267
|
"listener_ref": listener_ref,
|
|
1268
|
+
"listener_class": listener_ref.split("#", 1)[0],
|
|
1164
1269
|
"file_type": "rationale",
|
|
1165
1270
|
"source_file": source_file,
|
|
1166
1271
|
"source_location": f"L{line}",
|
|
@@ -1228,14 +1333,17 @@ def build_graph_main_backend(*, repo_root: Path, out_path: Path) -> GraphMainBac
|
|
|
1228
1333
|
method_topic = _TOPIC_RE.search(f"{m.ann_text}\n{m.body_text}")
|
|
1229
1334
|
notify_consumers = _extract_notify_consumers(m.body_text, info.fields)
|
|
1230
1335
|
rocket_consume_method = bool(class_is_rocket_consumer and m.name == "consumeMessage")
|
|
1336
|
+
bean_name = _bean_name_for_class(info)
|
|
1337
|
+
consumer_cfg = metaq_consumer_config.get(bean_name.lower(), {})
|
|
1231
1338
|
if rocket_consume_method and not notify_consumers:
|
|
1232
|
-
topic_tokens = _extract_metaq_producer_topics(m.body_text)
|
|
1233
|
-
fallback_topic = topic_tokens[0] if topic_tokens else f"{base_iface}#{m.name}"
|
|
1339
|
+
topic_tokens = _extract_metaq_producer_topics(m.body_text, string_constants)
|
|
1340
|
+
fallback_topic = consumer_cfg.get("topic") or (topic_tokens[0][0] if topic_tokens else f"{base_iface}#{m.name}")
|
|
1234
1341
|
notify_consumers = [(fallback_topic, info.class_fqcn)]
|
|
1235
|
-
producer_topics = _extract_metaq_producer_topics(m.body_text)
|
|
1342
|
+
producer_topics = _extract_metaq_producer_topics(m.body_text, string_constants)
|
|
1236
1343
|
produces_metaq = bool(
|
|
1237
1344
|
_METAQ_PRODUCER_RE.search(m.body_text)
|
|
1238
1345
|
or (_ROCKETMQ_SEND_CALL_RE.search(m.body_text) and producer_topics)
|
|
1346
|
+
or (_MESSAGE_SENDER_SEND_RE.search(m.body_text) and producer_topics)
|
|
1239
1347
|
)
|
|
1240
1348
|
consumes_metaq = bool(_METAQ_CONSUMER_RE.search(m.ann_text) or notify_consumers or rocket_consume_method)
|
|
1241
1349
|
hsf_dependency_calls = collect_reachable_hsf_deps(info, m)
|
|
@@ -1260,14 +1368,15 @@ def build_graph_main_backend(*, repo_root: Path, out_path: Path) -> GraphMainBac
|
|
|
1260
1368
|
|
|
1261
1369
|
any_kept_method = True
|
|
1262
1370
|
provider_kind = "sdk_facade" if has_sdk_facade_exposure else "hsf_provider"
|
|
1371
|
+
method_owner = info.class_fqcn if rocket_consume_method else base_iface
|
|
1263
1372
|
api_nid, method_nid = ensure_provider_method(
|
|
1264
|
-
|
|
1373
|
+
method_owner,
|
|
1265
1374
|
m.name,
|
|
1266
1375
|
info.rel_path,
|
|
1267
1376
|
m.line,
|
|
1268
1377
|
provider_kind=provider_kind,
|
|
1269
1378
|
)
|
|
1270
|
-
method_ck = f"hsf_method:{
|
|
1379
|
+
method_ck = f"hsf_method:{method_owner.lower()}#{m.name.lower()}"
|
|
1271
1380
|
code_method_keys.add(method_ck)
|
|
1272
1381
|
|
|
1273
1382
|
for mtop in method_mtops:
|
|
@@ -1327,9 +1436,9 @@ def build_graph_main_backend(*, repo_root: Path, out_path: Path) -> GraphMainBac
|
|
|
1327
1436
|
)
|
|
1328
1437
|
|
|
1329
1438
|
if produces_metaq:
|
|
1330
|
-
topics = producer_topics or ([method_topic.group(1)] if method_topic else [f"{base_iface}#{m.name}"])
|
|
1331
|
-
for topic in topics:
|
|
1332
|
-
producer_nid = ensure_producer(topic, info.rel_path, m.line)
|
|
1439
|
+
topics = producer_topics or ([(method_topic.group(1), "")] if method_topic else [(f"{base_iface}#{m.name}", "")])
|
|
1440
|
+
for topic, tag in topics:
|
|
1441
|
+
producer_nid = ensure_producer(topic, info.rel_path, m.line, tag=tag)
|
|
1333
1442
|
add_edge(
|
|
1334
1443
|
{
|
|
1335
1444
|
"source": method_nid,
|
|
@@ -1337,6 +1446,8 @@ def build_graph_main_backend(*, repo_root: Path, out_path: Path) -> GraphMainBac
|
|
|
1337
1446
|
"relation": "produces_metaq",
|
|
1338
1447
|
"confidence": "INFERRED",
|
|
1339
1448
|
"confidence_score": 0.85,
|
|
1449
|
+
"topic": topic,
|
|
1450
|
+
"tag": tag,
|
|
1340
1451
|
"source_file": info.rel_path,
|
|
1341
1452
|
"source_location": f"L{m.line}",
|
|
1342
1453
|
"weight": 1.0,
|
|
@@ -1346,7 +1457,19 @@ def build_graph_main_backend(*, repo_root: Path, out_path: Path) -> GraphMainBac
|
|
|
1346
1457
|
if consumes_metaq:
|
|
1347
1458
|
if notify_consumers:
|
|
1348
1459
|
for topic, listener in notify_consumers:
|
|
1349
|
-
|
|
1460
|
+
cfg = metaq_consumer_config.get(_bean_name_for_class(info).lower(), {})
|
|
1461
|
+
resolved_topic = cfg.get("topic") or _resolve_metaq_token(topic, string_constants)
|
|
1462
|
+
tag = cfg.get("tag", "")
|
|
1463
|
+
consumer_nid = ensure_consumer(
|
|
1464
|
+
resolved_topic or topic,
|
|
1465
|
+
listener,
|
|
1466
|
+
info.rel_path,
|
|
1467
|
+
m.line,
|
|
1468
|
+
topic=resolved_topic,
|
|
1469
|
+
tag=tag,
|
|
1470
|
+
consumer_group=cfg.get("consumer_group", ""),
|
|
1471
|
+
config_file=cfg.get("config_file", ""),
|
|
1472
|
+
)
|
|
1350
1473
|
add_edge(
|
|
1351
1474
|
{
|
|
1352
1475
|
"source": consumer_nid,
|
|
@@ -1354,15 +1477,17 @@ def build_graph_main_backend(*, repo_root: Path, out_path: Path) -> GraphMainBac
|
|
|
1354
1477
|
"relation": "consumes_metaq",
|
|
1355
1478
|
"confidence": "INFERRED",
|
|
1356
1479
|
"confidence_score": 0.85,
|
|
1480
|
+
"topic": resolved_topic,
|
|
1481
|
+
"tag": tag,
|
|
1357
1482
|
"source_file": info.rel_path,
|
|
1358
1483
|
"source_location": f"L{m.line}",
|
|
1359
1484
|
"weight": 1.0,
|
|
1360
1485
|
}
|
|
1361
1486
|
)
|
|
1362
1487
|
else:
|
|
1363
|
-
topic = method_topic.group(1) if method_topic else f"{base_iface}#{m.name}"
|
|
1488
|
+
topic = _resolve_metaq_token(method_topic.group(1), string_constants) if method_topic else f"{base_iface}#{m.name}"
|
|
1364
1489
|
listener_ref = f"{info.class_fqcn}#{m.name}"
|
|
1365
|
-
consumer_nid = ensure_consumer(topic, listener_ref, info.rel_path, m.line)
|
|
1490
|
+
consumer_nid = ensure_consumer(topic, listener_ref, info.rel_path, m.line, topic=topic)
|
|
1366
1491
|
add_edge(
|
|
1367
1492
|
{
|
|
1368
1493
|
"source": consumer_nid,
|
|
@@ -1370,6 +1495,7 @@ def build_graph_main_backend(*, repo_root: Path, out_path: Path) -> GraphMainBac
|
|
|
1370
1495
|
"relation": "consumes_metaq",
|
|
1371
1496
|
"confidence": "INFERRED",
|
|
1372
1497
|
"confidence_score": 0.85,
|
|
1498
|
+
"topic": topic,
|
|
1373
1499
|
"source_file": info.rel_path,
|
|
1374
1500
|
"source_location": f"L{m.line}",
|
|
1375
1501
|
"weight": 1.0,
|
|
@@ -1386,6 +1512,7 @@ def build_graph_main_backend(*, repo_root: Path, out_path: Path) -> GraphMainBac
|
|
|
1386
1512
|
"kind": "graph-main-backend",
|
|
1387
1513
|
"description": "Back-end focused graph (hsf/rest/mtop/dependency/metaq)",
|
|
1388
1514
|
"repo_name": repo_root.name,
|
|
1515
|
+
"repo": repo_meta,
|
|
1389
1516
|
"mode": mode,
|
|
1390
1517
|
"sdk_detection": sdk,
|
|
1391
1518
|
},
|