ckgraphify 0.1.2__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.
Files changed (45) hide show
  1. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/PKG-INFO +1 -1
  2. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/ckgraphify.egg-info/SOURCES.txt +2 -0
  3. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/__main__.py +48 -15
  4. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/business_map.py +65 -5
  5. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/graph_main_backend.py +159 -31
  6. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/graph_main_frontend.py +690 -31
  7. ckgraphify-0.1.4/graphify/graph_main_frontend_sdk.py +500 -0
  8. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/graph_main_html.py +1 -1
  9. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/graph_main_merge.py +74 -14
  10. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/graph_main_trace.py +84 -21
  11. ckgraphify-0.1.4/graphify/repo_registry.py +222 -0
  12. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/pyproject.toml +1 -1
  13. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/skill/skill-codex.md +24 -8
  14. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/skill/skill.md +49 -20
  15. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/LICENSE +0 -0
  16. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/MANIFEST.in +0 -0
  17. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/README.md +0 -0
  18. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/__init__.py +0 -0
  19. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/analyze.py +0 -0
  20. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/benchmark.py +0 -0
  21. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/bridge_mtop.py +0 -0
  22. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/build.py +0 -0
  23. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/cache.py +0 -0
  24. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/callflow_html.py +0 -0
  25. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/cluster.py +0 -0
  26. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/dedup.py +0 -0
  27. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/detect.py +0 -0
  28. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/export.py +0 -0
  29. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/extract.py +0 -0
  30. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/global_graph.py +0 -0
  31. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/google_workspace.py +0 -0
  32. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/hooks.py +0 -0
  33. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/ingest.py +0 -0
  34. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/llm.py +0 -0
  35. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/manifest.py +0 -0
  36. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/report.py +0 -0
  37. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/security.py +0 -0
  38. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/serve.py +0 -0
  39. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/transcribe.py +0 -0
  40. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/tree_html.py +0 -0
  41. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/validate.py +0 -0
  42. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/watch.py +0 -0
  43. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/graphify/wiki.py +0 -0
  44. {ckgraphify-0.1.2 → ckgraphify-0.1.4}/setup.cfg +0 -0
  45. {ckgraphify-0.1.2 → 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.2
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
@@ -97,7 +97,15 @@ def _claude_plugin_root() -> Path | None:
97
97
  return Path(raw).expanduser().resolve() if raw else None
98
98
 
99
99
 
100
+ def _claude_project_dir() -> Path | None:
101
+ raw = os.environ.get("CLAUDE_PROJECT_DIR", "").strip()
102
+ return Path(raw).expanduser().resolve() if raw else None
103
+
104
+
100
105
  def _default_repos_root() -> Path:
106
+ project_dir = _claude_project_dir()
107
+ if project_dir is not None:
108
+ return project_dir / "repos"
101
109
  plugin_root = _claude_plugin_root()
102
110
  if plugin_root is not None:
103
111
  return plugin_root / "repos"
@@ -562,10 +570,11 @@ def main() -> None:
562
570
  print(" merge-main-graphs <graph-main.json...> merge multiple graph-main boundary graphs")
563
571
  print(" --out <path> output path (default: graphify-out/graph-main-merged.json)")
564
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")
565
574
  print(" main-trace --graph <path> --from <node> [--api <api>] [--max-depth N] [--sources] [--prefer-repo R] [--exclude-repo R]")
566
575
  print(" trace a graph-main chain from an exact entry/API")
567
- print(" business-init --concept <name> [--out graphify-out/business-map.json]")
568
- print(" create a business-map seed (currently: 健康卡)")
576
+ print(" business-init --concept <name> [--out graphify-out/business-map.json] [--force]")
577
+ print(" create a business-map or append an unexplored concept")
569
578
  print(" business-show [--map <path>] [--concept <name>] [--scenario <name>]")
570
579
  print(" inspect concepts, scenarios, anchors, and trace hints")
571
580
  print(" business-query \"question\" [--map <path>] [--graph <path>] [--trace] [--sources] [--format json|text]")
@@ -1349,6 +1358,7 @@ def main() -> None:
1349
1358
  f"repos={stats.repo_count}, nodes={stats.nodes}, edges={stats.edges}, "
1350
1359
  f"cross_edges={stats.cross_edges}, mtop={stats.mtop_links}, rest={stats.rest_links}, "
1351
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}, "
1352
1362
  f"unresolved_dependencies={stats.unresolved_dependencies}"
1353
1363
  )
1354
1364
  print(f"Graph: {out_path}")
@@ -1715,6 +1725,12 @@ def main() -> None:
1715
1725
  args = sys.argv[2:]
1716
1726
  from graphify.business_map import default_business_graph_path, default_business_map_path
1717
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
+
1718
1734
  map_path = default_business_map_path()
1719
1735
  graph_path: Path | None = None
1720
1736
  concept = ""
@@ -1725,29 +1741,29 @@ def main() -> None:
1725
1741
  i = 0
1726
1742
  while i < len(args):
1727
1743
  a = args[i]
1728
- if a == "--map" and i + 1 < len(args):
1729
- map_path = Path(args[i + 1]); i += 2
1744
+ if a == "--map":
1745
+ map_path = Path(_require_business_trace_value(a, i)); i += 2
1730
1746
  elif a.startswith("--map="):
1731
1747
  map_path = Path(a.split("=", 1)[1]); i += 1
1732
- elif a == "--graph" and i + 1 < len(args):
1733
- graph_path = Path(args[i + 1]); i += 2
1748
+ elif a == "--graph":
1749
+ graph_path = Path(_require_business_trace_value(a, i)); i += 2
1734
1750
  elif a.startswith("--graph="):
1735
1751
  graph_path = Path(a.split("=", 1)[1]); i += 1
1736
- elif a == "--concept" and i + 1 < len(args):
1737
- concept = args[i + 1]; i += 2
1752
+ elif a == "--concept":
1753
+ concept = _require_business_trace_value(a, i); i += 2
1738
1754
  elif a.startswith("--concept="):
1739
1755
  concept = a.split("=", 1)[1]; i += 1
1740
- elif a == "--scenario" and i + 1 < len(args):
1741
- scenario = args[i + 1]; i += 2
1756
+ elif a == "--scenario":
1757
+ scenario = _require_business_trace_value(a, i); i += 2
1742
1758
  elif a.startswith("--scenario="):
1743
1759
  scenario = a.split("=", 1)[1]; i += 1
1744
- elif a == "--flow" and i + 1 < len(args):
1745
- flow = args[i + 1]; i += 2
1760
+ elif a == "--flow":
1761
+ flow = _require_business_trace_value(a, i); i += 2
1746
1762
  elif a.startswith("--flow="):
1747
1763
  flow = a.split("=", 1)[1]; i += 1
1748
- elif a == "--max-depth" and i + 1 < len(args):
1764
+ elif a == "--max-depth":
1749
1765
  try:
1750
- max_depth = int(args[i + 1])
1766
+ max_depth = int(_require_business_trace_value(a, i))
1751
1767
  except ValueError:
1752
1768
  print("error: --max-depth must be an integer", file=sys.stderr)
1753
1769
  sys.exit(1)
@@ -1913,6 +1929,22 @@ def main() -> None:
1913
1929
  if thin_out is not None:
1914
1930
  print(f"Thin graph: {thin_out}")
1915
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
+
1916
1948
  elif cmd == "main-graph":
1917
1949
  args = sys.argv[2:]
1918
1950
  root = Path(".")
@@ -2087,7 +2119,8 @@ def main() -> None:
2087
2119
  print(
2088
2120
  "Node counts: "
2089
2121
  f"page={stats.page_nodes}, component={stats.component_nodes}, "
2090
- 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}"
2091
2124
  )
2092
2125
  print(f"HTML: {html_out}")
2093
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() and not force:
335
- raise FileExistsError(f"business map already exists: {out_path}")
336
- if not _matches_text(concept, "健康卡", ["health_card", "healthCard", "health card"]):
337
- raise ValueError(f"unsupported seed concept: {concept}. Currently supported: 健康卡")
338
- data = health_card_seed()
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
- return prod_by_key[canonical]
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
- return cons_by_key[canonical]
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": topic_or_ref,
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
- base_iface,
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:{base_iface.lower()}#{m.name.lower()}"
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
- consumer_nid = ensure_consumer(topic, listener, info.rel_path, m.line)
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,
@@ -1385,6 +1511,8 @@ def build_graph_main_backend(*, repo_root: Path, out_path: Path) -> GraphMainBac
1385
1511
  "graph": {
1386
1512
  "kind": "graph-main-backend",
1387
1513
  "description": "Back-end focused graph (hsf/rest/mtop/dependency/metaq)",
1514
+ "repo_name": repo_root.name,
1515
+ "repo": repo_meta,
1388
1516
  "mode": mode,
1389
1517
  "sdk_detection": sdk,
1390
1518
  },