ckgraphify 0.1.4__tar.gz → 0.2.0__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.4 → ckgraphify-0.2.0}/PKG-INFO +8 -8
  2. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/README.md +7 -7
  3. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/__main__.py +2 -2
  4. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/business_map.py +6 -3
  5. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/graph_main_frontend.py +32 -3
  6. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/graph_main_frontend_sdk.py +311 -22
  7. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/graph_main_merge.py +30 -21
  8. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/graph_main_trace.py +8 -0
  9. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/repo_registry.py +36 -6
  10. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/pyproject.toml +1 -1
  11. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/skill/skill-codex.md +4 -4
  12. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/skill/skill.md +15 -66
  13. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/LICENSE +0 -0
  14. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/MANIFEST.in +0 -0
  15. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/ckgraphify.egg-info/SOURCES.txt +0 -0
  16. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/__init__.py +0 -0
  17. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/analyze.py +0 -0
  18. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/benchmark.py +0 -0
  19. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/bridge_mtop.py +0 -0
  20. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/build.py +0 -0
  21. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/cache.py +0 -0
  22. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/callflow_html.py +0 -0
  23. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/cluster.py +0 -0
  24. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/dedup.py +0 -0
  25. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/detect.py +0 -0
  26. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/export.py +0 -0
  27. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/extract.py +0 -0
  28. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/global_graph.py +0 -0
  29. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/google_workspace.py +0 -0
  30. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/graph_main_backend.py +0 -0
  31. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/graph_main_html.py +0 -0
  32. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/hooks.py +0 -0
  33. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/ingest.py +0 -0
  34. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/llm.py +0 -0
  35. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/manifest.py +0 -0
  36. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/report.py +0 -0
  37. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/security.py +0 -0
  38. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/serve.py +0 -0
  39. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/transcribe.py +0 -0
  40. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/tree_html.py +0 -0
  41. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/validate.py +0 -0
  42. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/watch.py +0 -0
  43. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/wiki.py +0 -0
  44. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/setup.cfg +0 -0
  45. {ckgraphify-0.1.4 → ckgraphify-0.2.0}/skill/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ckgraphify
3
- Version: 0.1.4
3
+ Version: 0.2.0
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
@@ -106,17 +106,17 @@ python3 -m pip install -e ./ckgraphify && CLAUDE_PLUGIN_ROOT=/Users/alsc/code/sh
106
106
 
107
107
  ## 生成并合并仓库图谱
108
108
 
109
- 下面命令会分别进入 `${CLAUDE_PLUGIN_ROOT}/repos/kl-health` 和 `${CLAUDE_PLUGIN_ROOT}/repos/ele-newretail-health-audit` 生成 `graphify-out/graph-main.json`,然后合并到 `${CLAUDE_PLUGIN_ROOT}/graphify-out/graph-hc.json`。
109
+ 下面命令会分别进入 `${CLAUDE_PLUGIN_ROOT}/repos/kl-health` 和 `${CLAUDE_PLUGIN_ROOT}/repos/ele-newretail-health-audit` 生成 `graphify-out/graph-main.json`,然后合并到 `${CLAUDE_PLUGIN_ROOT}/graphify-out/graph.json`。
110
110
 
111
111
  ```bash
112
- scripts/graph-main-repos.sh --repos-root "${CLAUDE_PLUGIN_ROOT}/repos" --out "${CLAUDE_PLUGIN_ROOT}/graphify-out/graph-hc.json" kl-health ele-newretail-health-audit
112
+ scripts/graph-main-repos.sh --repos-root "${CLAUDE_PLUGIN_ROOT}/repos" --out "${CLAUDE_PLUGIN_ROOT}/graphify-out/graph.json" kl-health ele-newretail-health-audit
113
113
  ```
114
114
 
115
115
  也可以传入更多 `${CLAUDE_PLUGIN_ROOT}/repos/` 下的仓库名:
116
116
 
117
117
  ```bash
118
118
  # 健康卡 init all repos
119
- scripts/graph-main-repos.sh --repos-root "${CLAUDE_PLUGIN_ROOT}/repos" --out "${CLAUDE_PLUGIN_ROOT}/graphify-out/graph-hc.json" ele-newretail-drug ele-newretail-health-task ele-newretail-drug-grow ele-newretail-health-audit ele-newretail-health-client ele-newretail-drug-trade kl-health health-vip-card medicine-unicore p ele-newretail-venus ele-newretail-summaryx china-alsc-sales-eleme-newretail-app
119
+ scripts/graph-main-repos.sh --repos-root "${CLAUDE_PLUGIN_ROOT}/repos" --out "${CLAUDE_PLUGIN_ROOT}/graphify-out/graph.json" ele-newretail-drug ele-newretail-health-task ele-newretail-drug-grow ele-newretail-health-audit ele-newretail-health-client ele-newretail-drug-trade kl-health health-vip-card medicine-unicore p ele-newretail-venus ele-newretail-summaryx china-alsc-sales-eleme-newretail-app
120
120
  ```
121
121
 
122
122
  ```bash
@@ -124,7 +124,7 @@ scripts/graph-main-repos.sh --repos-root "${CLAUDE_PLUGIN_ROOT}/repos" --out "${
124
124
  scripts/graph-main-repos.sh \
125
125
  --incremental \
126
126
  --repos-root "${CLAUDE_PLUGIN_ROOT}/repos" \
127
- --out "${CLAUDE_PLUGIN_ROOT}/graphify-out/graph-hc.json" \
127
+ --out "${CLAUDE_PLUGIN_ROOT}/graphify-out/graph.json" \
128
128
  p medicine-unicore
129
129
  ```
130
130
 
@@ -136,7 +136,7 @@ scripts/graph-main-repos.sh \
136
136
  关于ehealth-member实体的全部链路
137
137
  ```bash
138
138
  graphify main-trace \
139
- --graph "${CLAUDE_PLUGIN_ROOT}/graphify-out/graph-hc.json" \
139
+ --graph "${CLAUDE_PLUGIN_ROOT}/graphify-out/graph.json" \
140
140
  --from ehealth-member \
141
141
  --max-depth 8
142
142
  ```
@@ -144,7 +144,7 @@ graphify main-trace \
144
144
  选择ehealth-member实体的/shopping/healthCard/createHealthCard 支线链路
145
145
  ```bash
146
146
  graphify main-trace \
147
- --graph "${CLAUDE_PLUGIN_ROOT}/graphify-out/graph-hc.json" \
147
+ --graph "${CLAUDE_PLUGIN_ROOT}/graphify-out/graph.json" \
148
148
  --from ehealth-member \
149
149
  --api /shopping/healthCard/createHealthCard \
150
150
  --max-depth 8
@@ -154,7 +154,7 @@ graphify main-trace \
154
154
 
155
155
  ```bash
156
156
  graphify main-trace \
157
- --graph "${CLAUDE_PLUGIN_ROOT}/graphify-out/graph-hc.json" \
157
+ --graph "${CLAUDE_PLUGIN_ROOT}/graphify-out/graph.json" \
158
158
  --from ehealth-member \
159
159
  --max-depth 8 --sources
160
160
  ```
@@ -13,17 +13,17 @@ python3 -m pip install -e ./ckgraphify && CLAUDE_PLUGIN_ROOT=/Users/alsc/code/sh
13
13
 
14
14
  ## 生成并合并仓库图谱
15
15
 
16
- 下面命令会分别进入 `${CLAUDE_PLUGIN_ROOT}/repos/kl-health` 和 `${CLAUDE_PLUGIN_ROOT}/repos/ele-newretail-health-audit` 生成 `graphify-out/graph-main.json`,然后合并到 `${CLAUDE_PLUGIN_ROOT}/graphify-out/graph-hc.json`。
16
+ 下面命令会分别进入 `${CLAUDE_PLUGIN_ROOT}/repos/kl-health` 和 `${CLAUDE_PLUGIN_ROOT}/repos/ele-newretail-health-audit` 生成 `graphify-out/graph-main.json`,然后合并到 `${CLAUDE_PLUGIN_ROOT}/graphify-out/graph.json`。
17
17
 
18
18
  ```bash
19
- scripts/graph-main-repos.sh --repos-root "${CLAUDE_PLUGIN_ROOT}/repos" --out "${CLAUDE_PLUGIN_ROOT}/graphify-out/graph-hc.json" kl-health ele-newretail-health-audit
19
+ scripts/graph-main-repos.sh --repos-root "${CLAUDE_PLUGIN_ROOT}/repos" --out "${CLAUDE_PLUGIN_ROOT}/graphify-out/graph.json" kl-health ele-newretail-health-audit
20
20
  ```
21
21
 
22
22
  也可以传入更多 `${CLAUDE_PLUGIN_ROOT}/repos/` 下的仓库名:
23
23
 
24
24
  ```bash
25
25
  # 健康卡 init all repos
26
- scripts/graph-main-repos.sh --repos-root "${CLAUDE_PLUGIN_ROOT}/repos" --out "${CLAUDE_PLUGIN_ROOT}/graphify-out/graph-hc.json" ele-newretail-drug ele-newretail-health-task ele-newretail-drug-grow ele-newretail-health-audit ele-newretail-health-client ele-newretail-drug-trade kl-health health-vip-card medicine-unicore p ele-newretail-venus ele-newretail-summaryx china-alsc-sales-eleme-newretail-app
26
+ scripts/graph-main-repos.sh --repos-root "${CLAUDE_PLUGIN_ROOT}/repos" --out "${CLAUDE_PLUGIN_ROOT}/graphify-out/graph.json" ele-newretail-drug ele-newretail-health-task ele-newretail-drug-grow ele-newretail-health-audit ele-newretail-health-client ele-newretail-drug-trade kl-health health-vip-card medicine-unicore p ele-newretail-venus ele-newretail-summaryx china-alsc-sales-eleme-newretail-app
27
27
  ```
28
28
 
29
29
  ```bash
@@ -31,7 +31,7 @@ scripts/graph-main-repos.sh --repos-root "${CLAUDE_PLUGIN_ROOT}/repos" --out "${
31
31
  scripts/graph-main-repos.sh \
32
32
  --incremental \
33
33
  --repos-root "${CLAUDE_PLUGIN_ROOT}/repos" \
34
- --out "${CLAUDE_PLUGIN_ROOT}/graphify-out/graph-hc.json" \
34
+ --out "${CLAUDE_PLUGIN_ROOT}/graphify-out/graph.json" \
35
35
  p medicine-unicore
36
36
  ```
37
37
 
@@ -43,7 +43,7 @@ scripts/graph-main-repos.sh \
43
43
  关于ehealth-member实体的全部链路
44
44
  ```bash
45
45
  graphify main-trace \
46
- --graph "${CLAUDE_PLUGIN_ROOT}/graphify-out/graph-hc.json" \
46
+ --graph "${CLAUDE_PLUGIN_ROOT}/graphify-out/graph.json" \
47
47
  --from ehealth-member \
48
48
  --max-depth 8
49
49
  ```
@@ -51,7 +51,7 @@ graphify main-trace \
51
51
  选择ehealth-member实体的/shopping/healthCard/createHealthCard 支线链路
52
52
  ```bash
53
53
  graphify main-trace \
54
- --graph "${CLAUDE_PLUGIN_ROOT}/graphify-out/graph-hc.json" \
54
+ --graph "${CLAUDE_PLUGIN_ROOT}/graphify-out/graph.json" \
55
55
  --from ehealth-member \
56
56
  --api /shopping/healthCard/createHealthCard \
57
57
  --max-depth 8
@@ -61,7 +61,7 @@ graphify main-trace \
61
61
 
62
62
  ```bash
63
63
  graphify main-trace \
64
- --graph "${CLAUDE_PLUGIN_ROOT}/graphify-out/graph-hc.json" \
64
+ --graph "${CLAUDE_PLUGIN_ROOT}/graphify-out/graph.json" \
65
65
  --from ehealth-member \
66
66
  --max-depth 8 --sources
67
67
  ```
@@ -1330,9 +1330,9 @@ def main() -> None:
1330
1330
  sys.exit(1)
1331
1331
  else:
1332
1332
  graph_paths.append(Path(a)); i += 1
1333
- if len(graph_paths) < 2:
1333
+ if len(graph_paths) < 1:
1334
1334
  print(
1335
- "Usage: graphify merge-main-graphs <graph-main1.json> <graph-main2.json> [...] "
1335
+ "Usage: graphify merge-main-graphs <graph-main1.json> [graph-main2.json ...] "
1336
1336
  "[--out graph-main-merged.json] [--report report.md]",
1337
1337
  file=sys.stderr,
1338
1338
  )
@@ -345,9 +345,12 @@ def default_business_graph_path(map_path: Path, start: Path | None = None) -> Pa
345
345
  plugin_root = os.environ.get("CLAUDE_PLUGIN_ROOT", "").strip()
346
346
  if plugin_root:
347
347
  graphify_out = Path(plugin_root).expanduser().resolve() / "graphify-out"
348
- hc_graph = graphify_out / "graph-hc.json"
349
- if hc_graph.exists():
350
- return hc_graph
348
+ graph = graphify_out / "graph.json"
349
+ if graph.exists():
350
+ return graph
351
+ legacy_graph = graphify_out / "graph-hc.json"
352
+ if legacy_graph.exists():
353
+ return legacy_graph
351
354
  candidates = sorted(
352
355
  p for p in graphify_out.glob("graph*.json")
353
356
  if p.name != "business-map.json"
@@ -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 collections import deque
7
8
  from dataclasses import dataclass
8
9
  from datetime import datetime, timezone
@@ -12,6 +13,7 @@ from urllib.parse import urlparse
12
13
  from graphify.graph_main_frontend_sdk import (
13
14
  collect_sdk_dependencies,
14
15
  collect_sdk_exports,
16
+ collect_sdk_internal_links,
15
17
  detect_frontend_sdk_repo,
16
18
  )
17
19
  from graphify.repo_registry import repo_metadata_for_root
@@ -1132,6 +1134,13 @@ def build_graph_main_frontend(*, repo_root: Path, out_path: Path) -> GraphMainFr
1132
1134
  edge_ids.add(k)
1133
1135
  edges.append(e)
1134
1136
 
1137
+ def unique_node_id(prefix: str, suffix: str, canonical: str) -> str:
1138
+ base = f"{prefix}__{_safe_id(suffix)}"
1139
+ if base not in node_ids:
1140
+ return base
1141
+ digest = hashlib.sha1(canonical.encode("utf-8")).hexdigest()[:8]
1142
+ return f"{base}_{digest}"
1143
+
1135
1144
  page_by_key: dict[str, str] = {}
1136
1145
  component_by_key: dict[str, str] = {}
1137
1146
  mtop_by_key: dict[str, str] = {}
@@ -1232,7 +1241,7 @@ def build_graph_main_frontend(*, repo_root: Path, out_path: Path) -> GraphMainFr
1232
1241
  if canonical in sdk_export_by_key:
1233
1242
  return sdk_export_by_key[canonical]
1234
1243
  suffix = canonical.split(":", 1)[-1]
1235
- nid = f"sdk_export__{_safe_id(suffix)}"
1244
+ nid = unique_node_id("sdk_export", suffix, canonical)
1236
1245
  add_node(
1237
1246
  {
1238
1247
  "id": nid,
@@ -1266,7 +1275,7 @@ def build_graph_main_frontend(*, repo_root: Path, out_path: Path) -> GraphMainFr
1266
1275
  if canonical in sdk_dependency_by_key:
1267
1276
  return sdk_dependency_by_key[canonical]
1268
1277
  suffix = canonical.split(":", 1)[-1]
1269
- nid = f"sdk_dependency__{_safe_id(suffix)}"
1278
+ nid = unique_node_id("sdk_dependency", suffix, canonical)
1270
1279
  add_node(
1271
1280
  {
1272
1281
  "id": nid,
@@ -1768,7 +1777,8 @@ def build_graph_main_frontend(*, repo_root: Path, out_path: Path) -> GraphMainFr
1768
1777
  mode = "frontend-sdk"
1769
1778
  detection["mode"] = mode
1770
1779
  detection["sdk_detection"] = sdk_detection.as_dict()
1771
- for export in collect_sdk_exports(repo_root):
1780
+ sdk_exports = collect_sdk_exports(repo_root)
1781
+ for export in sdk_exports:
1772
1782
  ensure_sdk_export(
1773
1783
  kind=export.kind,
1774
1784
  label=export.label,
@@ -1779,6 +1789,25 @@ def build_graph_main_frontend(*, repo_root: Path, out_path: Path) -> GraphMainFr
1779
1789
  source_file=export.source_file,
1780
1790
  source_location=export.source_location,
1781
1791
  )
1792
+ for link in collect_sdk_internal_links(repo_root, sdk_exports):
1793
+ source_nid = sdk_export_by_key.get(link.source_key)
1794
+ target_nid = sdk_export_by_key.get(link.target_key)
1795
+ if not source_nid or not target_nid:
1796
+ continue
1797
+ add_edge(
1798
+ {
1799
+ "source": source_nid,
1800
+ "target": target_nid,
1801
+ "relation": link.relation,
1802
+ "confidence": "EXTRACTED",
1803
+ "confidence_score": 1.0,
1804
+ "source_file": link.source_file,
1805
+ "source_location": link.source_location,
1806
+ "weight": 1.0,
1807
+ "evidence": link.evidence,
1808
+ **({"resolution_kind": link.resolution_kind} if link.resolution_kind else {}),
1809
+ }
1810
+ )
1782
1811
  else:
1783
1812
  detection["sdk_detection"] = sdk_detection.as_dict()
1784
1813
  propagated_file_owners = propagate_file_owners(max_depth=3)
@@ -111,6 +111,17 @@ class SdkDependency:
111
111
  return base
112
112
 
113
113
 
114
+ @dataclass(frozen=True)
115
+ class SdkInternalLink:
116
+ source_key: str
117
+ target_key: str
118
+ relation: str
119
+ source_file: str
120
+ source_location: str = "L1"
121
+ evidence: str = ""
122
+ resolution_kind: str = ""
123
+
124
+
114
125
  def _read_json(path: Path) -> dict:
115
126
  try:
116
127
  return json.loads(path.read_text(encoding="utf-8"))
@@ -256,17 +267,45 @@ def _strip_known_ext(rel: str) -> str:
256
267
  return rel
257
268
 
258
269
 
259
- def _export_path_for_rel(rel: str) -> str:
260
- if rel.startswith("src/"):
261
- rel = rel[len("src/") :]
270
+ def _export_path_for_rel(rel: str, package_root: str = "src") -> str:
271
+ prefix = f"{package_root.strip('/')}/"
272
+ if package_root.strip("/") == "src" and rel.startswith(prefix):
273
+ rel = rel[len(prefix) :]
262
274
  return _strip_known_ext(rel)
263
275
 
264
276
 
277
+ def _package_source_roots(repo_root: Path) -> list[Path]:
278
+ roots: list[Path] = []
279
+ pkg = package_json(repo_root)
280
+ main = str(pkg.get("main", "") or "").strip()
281
+ if main:
282
+ first = main.split("/", 1)[0]
283
+ if first and (repo_root / first).is_dir():
284
+ roots.append(repo_root / first)
285
+ for name in ("src", "lib"):
286
+ p = repo_root / name
287
+ if p.is_dir() and p not in roots:
288
+ roots.append(p)
289
+ return roots
290
+
291
+
292
+ def _source_root_name(repo_root: Path, path: Path) -> str:
293
+ try:
294
+ rel = path.relative_to(repo_root)
295
+ except ValueError:
296
+ return ""
297
+ return rel.parts[0] if rel.parts else ""
298
+
299
+
265
300
  def _is_component_entry(rel: str) -> bool:
266
- if not rel.startswith("src/components/"):
301
+ if rel.startswith("src/"):
302
+ rel = rel[len("src/") :]
303
+ elif rel.startswith("lib/"):
304
+ rel = rel[len("lib/") :]
305
+ if not rel.startswith("components/") and not rel.startswith("component/"):
267
306
  return False
268
307
  parts = rel.split("/")
269
- if len(parts) != 5:
308
+ if len(parts) < 4:
270
309
  return False
271
310
  if parts[-1].split(".", 1)[0] != "index":
272
311
  return False
@@ -274,7 +313,49 @@ def _is_component_entry(rel: str) -> bool:
274
313
 
275
314
 
276
315
  def _is_sdk_extension_file(rel: str) -> bool:
277
- return rel.startswith("src/extensions/")
316
+ if rel.startswith("src/"):
317
+ rel = rel[len("src/") :]
318
+ elif rel.startswith("lib/"):
319
+ rel = rel[len("lib/") :]
320
+ return rel.startswith("extensions/")
321
+
322
+
323
+ def _strip_package_root(rel: str) -> str:
324
+ if rel.startswith("src/"):
325
+ return rel[len("src/") :]
326
+ if rel.startswith("lib/"):
327
+ return rel[len("lib/") :]
328
+ return rel
329
+
330
+
331
+ def _is_publishable_sdk_file(rel: str) -> bool:
332
+ rel = _strip_package_root(rel)
333
+ basename = Path(rel).name
334
+ if basename.startswith("index."):
335
+ return True
336
+ if rel.startswith("extensions/"):
337
+ return True
338
+ if "/Hooks/" in rel or "/hooks/" in rel:
339
+ return True
340
+ if "/component/" in f"/{rel}" or "/components/" in f"/{rel}":
341
+ return True
342
+ if "/core/" in f"/{rel}" or "/util/" in f"/{rel}":
343
+ return True
344
+ return False
345
+
346
+
347
+ def _sdk_kind_for_rel(rel: str, export_path: str, export_name: str = "") -> str:
348
+ stripped = _strip_package_root(rel)
349
+ lower = stripped.lower()
350
+ if "/hooks/" in lower or "/hook/" in lower:
351
+ return "hook"
352
+ if _is_component_entry(rel) or "/component/" in f"/{lower}" or "/components/" in f"/{lower}":
353
+ return "component" if not export_name or export_name[:1].isupper() else "function"
354
+ if _is_sdk_extension_file(rel):
355
+ return "function"
356
+ if export_path.startswith("core/"):
357
+ return "function" if export_name else "module"
358
+ return "function" if export_name else "module"
278
359
 
279
360
 
280
361
  def _line_of(text: str, idx: int) -> str:
@@ -304,6 +385,26 @@ def _named_exports(text: str) -> list[tuple[str, str]]:
304
385
  return sorted(dedup.items())
305
386
 
306
387
 
388
+ def _root_reexport_names(text: str) -> list[tuple[str, str]]:
389
+ out: list[tuple[str, str]] = []
390
+ for m in _NAMED_EXPORT_RE.finditer(text):
391
+ body = m.group("body")
392
+ for part in body.split(","):
393
+ token = part.strip()
394
+ if not token:
395
+ continue
396
+ if " as " in token:
397
+ token = token.split(" as ", 1)[1].strip()
398
+ else:
399
+ token = token.split(" ", 1)[0].strip()
400
+ if token:
401
+ out.append((token, _line_of(text, m.start())))
402
+ dedup: dict[str, str] = {}
403
+ for name, line in out:
404
+ dedup.setdefault(name, line)
405
+ return sorted(dedup.items())
406
+
407
+
307
408
  def _has_default_export(text: str) -> tuple[bool, str]:
308
409
  m = _EXPORT_DEFAULT_RE.search(text)
309
410
  if not m:
@@ -335,24 +436,48 @@ def collect_sdk_exports(repo_root: Path) -> list[SdkExport]:
335
436
  def add(item: SdkExport) -> None:
336
437
  exports[(item.kind, item.export_path, item.export_name)] = item
337
438
 
338
- for path in _iter_code_files(repo_root / "src"):
339
- rel = _norm_rel(repo_root, path)
340
- export_path = _export_path_for_rel(rel)
341
- text = _read_text(path)
342
- default_export, default_line = _has_default_export(text)
343
- default_name = _default_export_name(text, rel) if default_export else ""
344
- named_exports = _named_exports(text)
345
-
346
- if _is_component_entry(rel):
347
- add(SdkExport("component", pkg_name, export_path, "", rel, default_line or "L1"))
348
-
349
- if _is_sdk_extension_file(rel):
439
+ for source_root in _package_source_roots(repo_root):
440
+ root_name = _source_root_name(repo_root, source_root)
441
+ if not root_name:
442
+ continue
443
+ for path in _iter_code_files(source_root):
444
+ rel = _norm_rel(repo_root, path)
445
+ if not _is_publishable_sdk_file(rel):
446
+ continue
447
+ export_path = _export_path_for_rel(rel, root_name)
448
+ text = _read_text(path)
449
+ default_export, default_line = _has_default_export(text)
450
+ default_name = _default_export_name(text, rel) if default_export else ""
451
+ named_exports = _named_exports(text)
452
+
453
+ if _strip_known_ext(_strip_package_root(rel)) == "index":
454
+ if default_export:
455
+ add(SdkExport("module", pkg_name, "", "default", rel, default_line or "L1"))
456
+ for name, line in _root_reexport_names(text):
457
+ kind = "component" if name[:1].isupper() else "function"
458
+ add(SdkExport(kind, pkg_name, "", name, rel, line))
459
+
460
+ if _is_component_entry(rel):
461
+ add(SdkExport("component", pkg_name, export_path, "", rel, default_line or "L1"))
462
+
463
+ if _is_sdk_extension_file(rel):
464
+ for name, line in named_exports:
465
+ kind = "hook" if "/hooks/" in rel.lower() else "function"
466
+ add(SdkExport(kind, pkg_name, export_path, name, rel, line))
467
+ if default_export:
468
+ kind = "hook" if "/hooks/" in rel.lower() else "function"
469
+ add(SdkExport(kind, pkg_name, export_path, default_name, rel, default_line))
470
+
471
+ if default_export and export_path:
472
+ kind = _sdk_kind_for_rel(rel, export_path, default_name)
473
+ export_name = default_name if kind != "component" else ""
474
+ add(SdkExport(kind, pkg_name, export_path, export_name, rel, default_line or "L1"))
475
+ file_export_name = Path(_strip_known_ext(export_path)).name
476
+ if kind != "component" and file_export_name and file_export_name != export_name:
477
+ add(SdkExport(kind, pkg_name, export_path, file_export_name, rel, default_line or "L1"))
350
478
  for name, line in named_exports:
351
- kind = "hook" if "/hooks/" in rel else "function"
479
+ kind = _sdk_kind_for_rel(rel, export_path, name)
352
480
  add(SdkExport(kind, pkg_name, export_path, name, rel, line))
353
- if default_export:
354
- kind = "hook" if "/hooks/" in rel else "function"
355
- add(SdkExport(kind, pkg_name, export_path, default_name, rel, default_line))
356
481
 
357
482
  return sorted(exports.values(), key=lambda e: (e.kind, e.export_path, e.export_name))
358
483
 
@@ -438,6 +563,170 @@ def _sdk_member_uses(text: str, alias: str) -> set[str]:
438
563
  return {m.group(1) for m in pat.finditer(text)}
439
564
 
440
565
 
566
+ def _resolve_sdk_relative_import(repo_root: Path, current_file: Path, import_path: str) -> Path | None:
567
+ imp = (import_path or "").strip()
568
+ if not imp.startswith("."):
569
+ return None
570
+ base = (current_file.parent / imp).resolve()
571
+ candidates = [
572
+ base,
573
+ base.with_suffix(".ts"),
574
+ base.with_suffix(".tsx"),
575
+ base.with_suffix(".js"),
576
+ base.with_suffix(".jsx"),
577
+ base / "index.ts",
578
+ base / "index.tsx",
579
+ base / "index.js",
580
+ base / "index.jsx",
581
+ ]
582
+ try:
583
+ resolved_root = repo_root.resolve()
584
+ except OSError:
585
+ return None
586
+ for candidate in candidates:
587
+ if not candidate.exists() or not candidate.is_file():
588
+ continue
589
+ try:
590
+ candidate.resolve().relative_to(resolved_root)
591
+ except ValueError:
592
+ continue
593
+ return candidate
594
+ return None
595
+
596
+
597
+ def _export_basename(export: SdkExport) -> str:
598
+ raw = export.export_path.rstrip("/")
599
+ return raw.rsplit("/", 1)[-1] if raw else ""
600
+
601
+
602
+ def _target_exports_for_import(
603
+ exports: list[SdkExport],
604
+ *,
605
+ default_alias: str = "",
606
+ named_name: str = "",
607
+ ) -> list[SdkExport]:
608
+ if not exports:
609
+ return []
610
+ if named_name:
611
+ wanted = named_name.lower()
612
+ return [item for item in exports if item.export_name.lower() == wanted]
613
+ alias = default_alias.lower()
614
+ if alias:
615
+ matched = [
616
+ item
617
+ for item in exports
618
+ if item.export_name.lower() == alias or _export_basename(item).lower() == alias
619
+ ]
620
+ if matched:
621
+ return matched
622
+ return [item for item in exports if item.export_name in {"", "default"}] or exports[:1]
623
+
624
+
625
+ def _source_exports_for_alias(exports: list[SdkExport], alias: str) -> list[SdkExport]:
626
+ if not alias:
627
+ return exports
628
+ wanted = alias.lower()
629
+ return [
630
+ item
631
+ for item in exports
632
+ if item.export_name.lower() == wanted or _export_basename(item).lower() == wanted
633
+ ]
634
+
635
+
636
+ def _line_for_text(text: str, token: str) -> str:
637
+ idx = text.find(token)
638
+ return _line_of(text, idx if idx >= 0 else 0)
639
+
640
+
641
+ def _alias_is_used(text: str, alias: str) -> bool:
642
+ if not alias:
643
+ return False
644
+ return bool(re.search(rf"""\b{re.escape(alias)}\b""", text))
645
+
646
+
647
+ def collect_sdk_internal_links(repo_root: Path, exports: list[SdkExport]) -> list[SdkInternalLink]:
648
+ by_file: dict[str, list[SdkExport]] = {}
649
+ for export in exports:
650
+ by_file.setdefault(export.source_file, []).append(export)
651
+
652
+ links: dict[tuple[str, str, str], SdkInternalLink] = {}
653
+
654
+ def add(link: SdkInternalLink) -> None:
655
+ if link.source_key == link.target_key:
656
+ return
657
+ links[(link.source_key, link.target_key, link.relation)] = link
658
+
659
+ for rel, source_exports in sorted(by_file.items()):
660
+ source_path = repo_root / rel
661
+ text = _read_text(source_path)
662
+ if not text:
663
+ continue
664
+ stripped = _strip_js_comments(text)
665
+ import_aliases: dict[str, list[SdkExport]] = {}
666
+
667
+ for m in _IMPORT_FROM_RE.finditer(stripped):
668
+ raw_import = m.group("path")
669
+ target_path = _resolve_sdk_relative_import(repo_root, source_path, raw_import)
670
+ if not target_path:
671
+ continue
672
+ target_exports = by_file.get(_norm_rel(repo_root, target_path), [])
673
+ if not target_exports:
674
+ continue
675
+ default_alias, named = _parse_import_clause(m.group("clause"))
676
+ if default_alias:
677
+ import_aliases[default_alias] = _target_exports_for_import(target_exports, default_alias=default_alias)
678
+ for name in named:
679
+ import_aliases[name] = _target_exports_for_import(target_exports, named_name=name)
680
+
681
+ exported_aliases = {name for name, _ in _root_reexport_names(stripped)}
682
+ for alias, targets in import_aliases.items():
683
+ if not targets:
684
+ continue
685
+ relation = "resolves_sdk_export" if alias in exported_aliases else "uses_sdk_export"
686
+ if relation == "uses_sdk_export" and not _alias_is_used(stripped, alias):
687
+ continue
688
+ source_candidates = _source_exports_for_alias(source_exports, alias) if relation == "resolves_sdk_export" else source_exports
689
+ for source in source_candidates:
690
+ for target in targets:
691
+ add(
692
+ SdkInternalLink(
693
+ source_key=source.npm_export_key,
694
+ target_key=target.npm_export_key,
695
+ relation=relation,
696
+ source_file=rel,
697
+ source_location=_line_for_text(text, alias),
698
+ evidence="sdk_relative_import",
699
+ resolution_kind="reexport" if relation == "resolves_sdk_export" else "import_usage",
700
+ )
701
+ )
702
+
703
+ for m in _EXPORT_FROM_RE.finditer(stripped):
704
+ raw_import = m.group("path")
705
+ target_path = _resolve_sdk_relative_import(repo_root, source_path, raw_import)
706
+ if not target_path:
707
+ continue
708
+ target_exports = by_file.get(_norm_rel(repo_root, target_path), [])
709
+ if not target_exports:
710
+ continue
711
+ default_alias, named = _parse_import_clause(m.group("clause"))
712
+ for alias in named or ([default_alias] if default_alias else []):
713
+ for source in _source_exports_for_alias(source_exports, alias):
714
+ for target in _target_exports_for_import(target_exports, named_name=alias):
715
+ add(
716
+ SdkInternalLink(
717
+ source_key=source.npm_export_key,
718
+ target_key=target.npm_export_key,
719
+ relation="resolves_sdk_export",
720
+ source_file=rel,
721
+ source_location=_line_for_text(text, raw_import),
722
+ evidence="sdk_export_from_relative_import",
723
+ resolution_kind="export_from",
724
+ )
725
+ )
726
+
727
+ return sorted(links.values(), key=lambda item: (item.source_key, item.relation, item.target_key))
728
+
729
+
441
730
  def collect_sdk_dependencies(repo_root: Path, code_files: list[Path]) -> list[SdkDependency]:
442
731
  packages = set(sdk_package_allowlist(repo_root).keys())
443
732
  if not packages:
@@ -8,7 +8,7 @@ from datetime import datetime, timezone
8
8
  from pathlib import Path
9
9
  from typing import Iterable
10
10
 
11
- from graphify.repo_registry import repo_metadata_by_name
11
+ from graphify.repo_registry import repo_id_for, repo_metadata_by_name
12
12
 
13
13
 
14
14
  _ANCHOR_KINDS = {"mtop_api", "rest_api", "hsf_api", "hsf_method", "metaq_producer", "metaq_consumer"}
@@ -49,20 +49,24 @@ def _repo_meta_from_graph(data: dict, fallback_name: str) -> dict:
49
49
  raw = graph.get("repo", {}) if isinstance(graph, dict) else {}
50
50
  if isinstance(raw, dict):
51
51
  name = str(raw.get("name", "") or graph.get("repo_name", "") or fallback_name)
52
+ group = str(raw.get("group", "") or "")
53
+ repo_id = str(raw.get("id", "") or raw.get("repo_id", "") or repo_id_for(group, name))
52
54
  return {
55
+ "id": repo_id,
53
56
  "name": name,
54
- "group": str(raw.get("group", "") or ""),
57
+ "group": group,
55
58
  "git": str(raw.get("git", "") or ""),
56
59
  }
57
- return {"name": str(graph.get("repo_name", "") or fallback_name), "group": "", "git": ""}
60
+ name = str(graph.get("repo_name", "") or fallback_name)
61
+ return {"id": repo_id_for("", name), "name": name, "group": "", "git": ""}
58
62
 
59
63
 
60
64
  def _norm_key(v: object) -> str:
61
65
  return str(v or "").strip().lower()
62
66
 
63
67
 
64
- def _node_key(repo: str, node_id: object) -> str:
65
- return f"{repo}::{node_id}"
68
+ def _node_key(repo_id: str, node_id: object) -> str:
69
+ return f"{repo_id}::{node_id}"
66
70
 
67
71
 
68
72
  def _shared_node_id(anchor: str) -> str:
@@ -78,21 +82,22 @@ def _edge_key(edge: dict) -> tuple[str, str, str, str]:
78
82
  )
79
83
 
80
84
 
81
- def _copy_node(repo: str, node: dict) -> dict:
85
+ def _copy_node(repo_id: str, repo_name: str, node: dict) -> dict:
82
86
  out = dict(node)
83
87
  local_id = str(node.get("id", ""))
84
- out["id"] = _node_key(repo, local_id)
88
+ out["id"] = _node_key(repo_id, local_id)
85
89
  out["local_id"] = local_id
86
- out["repo"] = repo
90
+ out["repo"] = repo_id
91
+ out["repo_name"] = repo_name
87
92
  out.setdefault("main_kind", "unknown")
88
93
  return out
89
94
 
90
95
 
91
- def _copy_edge(repo: str, edge: dict) -> dict:
96
+ def _copy_edge(repo_id: str, edge: dict) -> dict:
92
97
  out = dict(edge)
93
- out["source"] = _node_key(repo, edge.get("source", ""))
94
- out["target"] = _node_key(repo, edge.get("target", ""))
95
- out["repo"] = repo
98
+ out["source"] = _node_key(repo_id, edge.get("source", ""))
99
+ out["target"] = _node_key(repo_id, edge.get("target", ""))
100
+ out["repo"] = repo_id
96
101
  out["edge_scope"] = "intra_repo"
97
102
  return out
98
103
 
@@ -197,8 +202,8 @@ def _load_graph(path: Path) -> dict:
197
202
 
198
203
  def merge_graph_main_files(graph_paths: Iterable[Path], out_path: Path) -> GraphMainMergeStats:
199
204
  inputs = [Path(p).resolve() for p in graph_paths]
200
- if len(inputs) < 2:
201
- raise ValueError("merge_graph_main_files requires at least two graph-main.json files")
205
+ if len(inputs) < 1:
206
+ raise ValueError("merge_graph_main_files requires at least one graph-main.json file")
202
207
 
203
208
  nodes: list[dict] = []
204
209
  edges: list[dict] = []
@@ -220,17 +225,21 @@ def merge_graph_main_files(graph_paths: Iterable[Path], out_path: Path) -> Graph
220
225
  data = _load_graph(path)
221
226
  path_repo = _repo_from_path(path)
222
227
  graph_repo_meta = _repo_meta_from_graph(data, path_repo)
223
- repo = str(graph_repo_meta.get("name", "") or path_repo)
224
- registry_meta = registry.get(repo, {})
228
+ repo_name = str(graph_repo_meta.get("name", "") or path_repo)
229
+ registry_meta = registry.get(repo_name, {})
225
230
  git = str(graph_repo_meta.get("git", "") or registry_meta.get("git", "") or "")
226
231
  group = str(graph_repo_meta.get("group", "") or registry_meta.get("group", "") or "")
232
+ branch = str(graph_repo_meta.get("branch", "") or registry_meta.get("branch", "") or "master")
233
+ repo_id = str(graph_repo_meta.get("id", "") or registry_meta.get("id", "") or repo_id_for(group, repo_name))
227
234
  repo_root_abs = path.parent.parent
228
- repo_roots[repo] = f"repos/{repo}"
235
+ repo_roots[repo_id] = f"repos/{repo_name}"
229
236
  repo_meta = {
230
- "repo": repo,
231
- "name": repo,
237
+ "repo": repo_id,
238
+ "id": repo_id,
239
+ "name": repo_name,
232
240
  "group": group,
233
241
  "git": git,
242
+ "branch": branch,
234
243
  "kind": data.get("graph", {}).get("kind", ""),
235
244
  "mode": data.get("graph", {}).get("mode", ""),
236
245
  "nodes": len(data.get("nodes", [])),
@@ -239,7 +248,7 @@ def merge_graph_main_files(graph_paths: Iterable[Path], out_path: Path) -> Graph
239
248
  repos.append(repo_meta)
240
249
 
241
250
  for node in data.get("nodes", []):
242
- copied = _copy_node(repo, node)
251
+ copied = _copy_node(repo_id, repo_name, node)
243
252
  nid = str(copied.get("id", ""))
244
253
  nodes.append(copied)
245
254
  id_to_node[nid] = copied
@@ -258,7 +267,7 @@ def merge_graph_main_files(graph_paths: Iterable[Path], out_path: Path) -> Graph
258
267
  sdk_dependency_index[match_key].append(nid)
259
268
 
260
269
  for edge in data.get("links", []):
261
- copied = _copy_edge(repo, edge)
270
+ copied = _copy_edge(repo_id, edge)
262
271
  key = _edge_key(copied)
263
272
  if key in edge_seen:
264
273
  continue
@@ -20,6 +20,10 @@ _TRACE_RELS = {
20
20
  "resolves_rest_api",
21
21
  "resolves_hsf_api",
22
22
  "resolves_hsf_method",
23
+ "uses_sdk_dependency",
24
+ "resolves_npm_export",
25
+ "resolves_sdk_export",
26
+ "uses_sdk_export",
23
27
  "produces_metaq",
24
28
  "consumes_metaq",
25
29
  "resolves_metaq_topic",
@@ -34,6 +38,8 @@ _START_KINDS = {
34
38
  "mtop_api",
35
39
  "dependency",
36
40
  "dependency_method",
41
+ "sdk_dependency",
42
+ "sdk_export",
37
43
  "metaq_consumer",
38
44
  "metaq_producer",
39
45
  }
@@ -45,6 +51,8 @@ _TARGET_KINDS = {
45
51
  "hsf_method",
46
52
  "dependency",
47
53
  "dependency_method",
54
+ "sdk_dependency",
55
+ "sdk_export",
48
56
  "metaq_consumer",
49
57
  "metaq_producer",
50
58
  }
@@ -19,9 +19,26 @@ class RepoMetadata:
19
19
  name: str
20
20
  group: str = ""
21
21
  git: str = ""
22
+ branch: str = "master"
23
+ id: str = ""
22
24
 
23
25
  def as_dict(self) -> dict:
24
- return {"name": self.name, "group": self.group, "git": self.git}
26
+ repo_id = self.id or repo_id_for(self.group, self.name)
27
+ return {"id": repo_id, "name": self.name, "group": self.group, "git": self.git, "branch": self.branch or "master"}
28
+
29
+
30
+ def repo_id_for(group: str, name: str) -> str:
31
+ group = str(group or "").strip().strip("/")
32
+ name = str(name or "").strip().strip("/")
33
+ return f"{group}/{name}" if group else name
34
+
35
+
36
+ def _split_repo_id(repo_id: str) -> tuple[str, str]:
37
+ raw = str(repo_id or "").strip().strip("/")
38
+ if "/" not in raw:
39
+ return "", raw
40
+ group, name = raw.rsplit("/", 1)
41
+ return group, name
25
42
 
26
43
 
27
44
  def resolve_repo_list_path(start: Path | None = None) -> Path:
@@ -97,14 +114,19 @@ def _read_repo_list(path: Path) -> list[RepoMetadata]:
97
114
  for raw in raw_entries:
98
115
  if not isinstance(raw, dict):
99
116
  continue
100
- name = str(raw.get("name", "") or raw.get("repo", "")).strip()
117
+ raw_id = str(raw.get("id", "") or raw.get("repo_id", "")).strip()
118
+ id_group, id_name = _split_repo_id(raw_id)
119
+ name = str(raw.get("name", "") or raw.get("repo", "") or id_name).strip()
101
120
  if not name:
102
121
  continue
122
+ group = str(raw.get("group", "") or id_group or "")
103
123
  repos.append(
104
124
  RepoMetadata(
105
125
  name=name,
106
- group=str(raw.get("group", "") or ""),
126
+ group=group,
107
127
  git=str(raw.get("git", "") or raw.get("url", "") or ""),
128
+ branch=str(raw.get("branch", "") or raw.get("default_branch", "") or raw.get("main_branch", "") or "master"),
129
+ id=raw_id or repo_id_for(group, name),
108
130
  )
109
131
  )
110
132
  return repos
@@ -112,10 +134,13 @@ def _read_repo_list(path: Path) -> list[RepoMetadata]:
112
134
 
113
135
  def _write_repo_list(path: Path, repos: Iterable[RepoMetadata]) -> None:
114
136
  path.parent.mkdir(parents=True, exist_ok=True)
115
- ordered = sorted({r.name: r for r in repos}.values(), key=lambda r: (r.group, r.name))
137
+ ordered = sorted(
138
+ {(r.id or repo_id_for(r.group, r.name)): r for r in repos}.values(),
139
+ key=lambda r: (r.group, r.name),
140
+ )
116
141
  data = {
117
142
  "version": 1,
118
- "description": "Editable ckgraphify repo registry. Edit name/group/git here; graph-main generation preserves non-empty manual values.",
143
+ "description": "Editable ckgraphify repo registry. Edit name/group/git/branch here; graph-main generation preserves non-empty manual values.",
119
144
  "repos": [r.as_dict() for r in ordered],
120
145
  }
121
146
  path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
@@ -186,10 +211,13 @@ def ensure_repo_list(start: Path | None = None) -> Path:
186
211
  by_name[scanned.name] = scanned
187
212
  changed = True
188
213
  elif not current.git and scanned.git:
214
+ group = current.group or scanned.group
189
215
  by_name[scanned.name] = RepoMetadata(
190
216
  name=current.name,
191
- group=current.group or scanned.group,
217
+ group=group,
192
218
  git=scanned.git,
219
+ branch=current.branch,
220
+ id=repo_id_for(group, current.name),
193
221
  )
194
222
  changed = True
195
223
  elif not current.group and scanned.group:
@@ -197,6 +225,8 @@ def ensure_repo_list(start: Path | None = None) -> Path:
197
225
  name=current.name,
198
226
  group=scanned.group,
199
227
  git=current.git,
228
+ branch=current.branch,
229
+ id=repo_id_for(scanned.group, current.name),
200
230
  )
201
231
  changed = True
202
232
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ckgraphify"
7
- version = "0.1.4"
7
+ version = "0.2.0"
8
8
  description = "AI coding assistant skill for Claude Code and Codex - graph-main boundary graphs, multi-repo call chains, business-map concepts, and business search"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -73,7 +73,7 @@ Show this API briefly.
73
73
  - If the current directory is the parent of a marked `kg-banks/` root, use `./kg-banks/graphify-out/business-map.json`.
74
74
  - A marked `kg-banks/` root must contain `__root__` or `graphify-out/business-map.json`; do not trust the directory name alone.
75
75
  - Default graph: use `--graph`, then a JSON passed via `--path`, then the graph bound in `business-map.json` (`graph.path`), then `<resolved-map-root>/graphify-out/graph*.json`, then `graphify-out/graph-main-merged.json`.
76
- - For this kg-banks workspace, the bound default graph is `graphify-out/graph-hc.json`; from the parent directory it is `./kg-banks/graphify-out/graph-hc.json`.
76
+ - For this kg-banks workspace, the bound default graph is `graphify-out/graph.json`; from the parent directory it is `./kg-banks/graphify-out/graph.json`.
77
77
  - Default depth: `10`.
78
78
  - Default answer: concise business result first, evidence second, gaps last.
79
79
  - If required inputs are missing and cannot be inferred from files, ask one short clarification.
@@ -97,7 +97,7 @@ Prefer existing deterministic commands:
97
97
 
98
98
  Treat the merged graph as business navigation, not as a complete execution trace. Its normal responsibility is to get from business wording to the right repo, page, MTop/REST API, HSF method, dependency boundary, and source filepath.
99
99
 
100
- Do not expect `graph-hc.json` or other merged graphs to describe every parameter, response field, extension point, or method-local branch. That level of detail would make the graph too large and noisy. When a question depends on field-level or parameter-level behavior, first use `main-trace` and source filepaths to land on the right code, then read the relevant implementation directly.
100
+ Do not expect `graph.json` or other merged graphs to describe every parameter, response field, extension point, or method-local branch. That level of detail would make the graph too large and noisy. When a question depends on field-level or parameter-level behavior, first use `main-trace` and source filepaths to land on the right code, then read the relevant implementation directly.
101
101
 
102
102
  Missing field-level graph evidence is not automatically a graph defect. Classify it as one of:
103
103
 
@@ -126,7 +126,7 @@ Search discipline:
126
126
  For `$ckgraphify learn`:
127
127
 
128
128
  1. Require concept, scenario, repos, graph, map, and out.
129
- 2. Read `ckgraphify/docs/business-map-policy.md`; every map iteration must follow it or explicitly report the conflict.
129
+ 2. Read the task-local `input/business-map-policy.md` when present; otherwise read `ckgraphify/docs/business-map-policy.md`. Every map iteration must follow it or explicitly report the conflict.
130
130
  3. Read existing map scenarios, flows, gaps, accepted gaps, verification scopes, and `search_constraints`.
131
131
  4. Search graph nodes with aliases, keywords, page/API names, HSF names, and repo hints.
132
132
  5. Run `main-trace` on promising graph matches.
@@ -153,7 +153,7 @@ Keep graph-main schema details, dependency extraction rules, and low-level branc
153
153
  ## Guardrails
154
154
 
155
155
  - Do not write business semantics into `graph-main.json`.
156
- - Before changing `business-map.json`, read `ckgraphify/docs/business-map-policy.md`; follow it or state the conflict.
156
+ - Before changing `business-map.json`, read the task-local `input/business-map-policy.md` when present; otherwise read `ckgraphify/docs/business-map-policy.md`. Follow it or state the conflict.
157
157
  - Use `scenario.flows[]` for sub-links inside one complex business scene, such as landing page purchase/refund/benefit branches.
158
158
  - Keep `business-map.json` as a search/trace index. Do not add long `evidence` arrays there; put audit proof in `graphify-out/business-map-evidence.json`.
159
159
  - Use `gaps` for active unknowns and `accepted_gaps` for known boundaries with a stop rule.
@@ -15,16 +15,12 @@ Before running any `graphify` command in the Claude plugin environment, ensure t
15
15
  export GRAPHIFY="${CLAUDE_PLUGIN_ROOT}/.graphify/bin/graphify"
16
16
  ```
17
17
 
18
+ The setup script also runs `${CLAUDE_PLUGIN_ROOT}/scripts/sync-artifacts.sh`. Before using the default graph or business map, this sync script queries the production kg-server (`KG_SERVER_URL`, default `https://kg-banks.alibaba.net`) for the lightweight latest manifest. If the remote graph/concept versions and checksums already match the local manifest, it does not download files.
19
+
18
20
  If `${CLAUDE_PLUGIN_ROOT}` is missing, report that the Claude plugin environment is not active. Do not use macOS `/usr/bin/python3` or `sudo pip`. Do not inline the setup script from this skill; run the bundled script so quoting, checksum validation, and Python selection stay deterministic.
19
21
 
20
22
  ## User API
21
23
 
22
- ```bash
23
- /ckgraphify init --path <workspace-or-repos-root> --concept <business-concept> [--graph <merged-graph.json>] [--map <business-map.json>]
24
- ```
25
-
26
- Prepare the environment: locate or build repo `graph-main.json`, merge graphs if needed, initialize `business-map.json`, and validate anchors.
27
-
28
24
  ```bash
29
25
  /ckgraphify query "<business question>" --path <workspace-or-graph.json> [--map <business-map.json>] [--sources]
30
26
  ```
@@ -37,19 +33,6 @@ Semantic business search. First try to hit a known business concept/scenario/flo
37
33
 
38
34
  Trace a known concept or scenario. If no scenario is supplied, trace all useful known branches unless the result would be too broad.
39
35
 
40
- ```bash
41
- /ckgraphify learn "<concept:scenario>" --path <workspace-or-repos-root> --graph <merged-graph.json> --map <business-map.json> --out <candidate.json> [--keywords "..."] [--docs <paths...>]
42
- ```
43
-
44
- Learn a missing scenario. This is an AI workflow, not a single CLI command. Write a candidate patch; do not overwrite `business-map.json` directly.
45
- For a complex scenario with many sub-links, keep one scenario and add `flows` under it instead of creating many top-level scenarios.
46
-
47
- ```bash
48
- /ckgraphify update --path <workspace-or-repos-root> [--graph <merged-graph.json>] [--map <business-map.json>]
49
- ```
50
-
51
- Refresh graph-main outputs, rebuild the merged graph, validate the business map, and report stale anchors or missing repos.
52
-
53
36
  ```bash
54
37
  /ckgraphify help
55
38
  ```
@@ -60,26 +43,17 @@ Show this API briefly.
60
43
 
61
44
  - `--path` can point to a workspace, repos root, repo list, or a merged graph JSON. Resolve it before running CLI commands.
62
45
  - Two root directories:
63
- - `${CLAUDE_PLUGIN_ROOT}` — installed plugin directory (graph data, map, evidence).
64
- - `${CLAUDE_PROJECT_DIR}` — the project that installed the plugin (repos, pypi source, docs).
65
- - Graph and business map (read/write):
46
+ - `${CLAUDE_PLUGIN_ROOT}` — installed plugin directory (graph data, business map, and optional evidence).
47
+ - `${CLAUDE_PROJECT_DIR}` — optional user project directory for code reading only.
48
+ - Graph and business map (read-only in the plugin):
66
49
  - Default business map is `${CLAUDE_PLUGIN_ROOT}/graphify-out/business-map.json`.
67
50
  - Default evidence shadow table is `${CLAUDE_PLUGIN_ROOT}/graphify-out/business-map-evidence.json`.
68
- - Default merged graph is `${CLAUDE_PLUGIN_ROOT}/graphify-out/graph-hc.json`.
69
- - Repos (code reading, main-graph generation):
51
+ - Default merged graph is `${CLAUDE_PLUGIN_ROOT}/graphify-out/graph.json`.
52
+ - Repos (optional code reading only):
70
53
  - Default repos root is `${CLAUDE_PROJECT_DIR}/repos`.
71
- - Each repo's graph-main output is `${CLAUDE_PROJECT_DIR}/repos/<repo>/graphify-out/graph-main.json`.
72
- - Docs and policies (read-only, bundled with pypi source):
73
- - `${CLAUDE_PROJECT_DIR}/ckgraphify/docs/business-map-policy.md`
74
- - Sync: after writing to `${CLAUDE_PLUGIN_ROOT}/graphify-out/`, sync changes back to the project source:
75
-
76
- ```bash
77
- cp "${CLAUDE_PLUGIN_ROOT}/graphify-out/business-map.json" "${CLAUDE_PROJECT_DIR}/claude-plugin/graphify-out/business-map.json"
78
- cp "${CLAUDE_PLUGIN_ROOT}/graphify-out/business-map-evidence.json" "${CLAUDE_PROJECT_DIR}/claude-plugin/graphify-out/business-map-evidence.json"
79
- ```
54
+ - If `${CLAUDE_PROJECT_DIR}` is missing or repos are unavailable, answer from graph and business-map evidence and report that source-file reading is unavailable.
80
55
 
81
56
  - If `${CLAUDE_PLUGIN_ROOT}` is missing, report that the Claude plugin environment is not active.
82
- - If `${CLAUDE_PROJECT_DIR}` is missing, report that repos and docs are not available for code reading.
83
57
  - Default depth: `10`.
84
58
  - Default answer: concise business result first, evidence second, gaps last.
85
59
  - If required inputs are missing and cannot be inferred from files, ask one short clarification.
@@ -91,28 +65,25 @@ Prefer existing deterministic commands:
91
65
  ```bash
92
66
  "${GRAPHIFY:-graphify}" business-show --map <map> --concept <concept>
93
67
  "${GRAPHIFY:-graphify}" business-query "<question>" --map <map> --graph <graph> --trace --format text
94
- "${GRAPHIFY:-graphify}" business-lint --map <map>
95
- "${GRAPHIFY:-graphify}" business-validate --map <map> --graph <graph>
96
68
  "${GRAPHIFY:-graphify}" business-trace --map <map> --graph <graph> --concept <concept> --scenario <scenario> --flow <flow> --max-depth <n> --sources
97
69
  "${GRAPHIFY:-graphify}" main-trace --graph <graph> --from <exact-node> --api <api> --max-depth <n> --sources --prefer-repo <repo> --exclude-repo <repo>
98
- "${GRAPHIFY:-graphify}" main-graph "${CLAUDE_PROJECT_DIR}/repos/<repo>" --out "${CLAUDE_PROJECT_DIR}/repos/<repo>/graphify-out/graph-main.json"
99
- "${GRAPHIFY:-graphify}" merge-main-graphs "${CLAUDE_PROJECT_DIR}/repos/<repo-a>/graphify-out/graph-main.json" "${CLAUDE_PROJECT_DIR}/repos/<repo-b>/graphify-out/graph-main.json" --out "${CLAUDE_PLUGIN_ROOT}/graphify-out/graph-hc.json"
100
70
  ```
101
71
 
102
72
  ## Graph Scope
103
73
 
104
74
  Treat the merged graph as business navigation, not as a complete execution trace. Its normal responsibility is to get from business wording to the right repo, page, MTop/REST API, HSF method, dependency boundary, and source filepath.
105
75
 
106
- Do not expect `graph-hc.json` or other merged graphs to describe every parameter, response field, extension point, or method-local branch. That level of detail would make the graph too large and noisy. When a question depends on field-level or parameter-level behavior, first use `main-trace` and source filepaths to land on the right code, then read the relevant implementation directly.
76
+ Do not expect `graph.json` or other merged graphs to describe every parameter, response field, extension point, or method-local branch. That level of detail would make the graph too large and noisy. When a question depends on field-level or parameter-level behavior, first use `main-trace` and source filepaths to land on the right code, then read the relevant implementation directly.
107
77
 
108
78
  ### File path convention
109
79
 
110
- Merged graphs store source paths as **relative paths** per repo. To resolve the full path for code reading:
80
+ Merged graphs store source paths as **relative paths** per repo. To resolve the full path for optional code reading:
111
81
 
112
82
  - `graph.repo_roots` maps each repo name to its relative root, e.g. `{"p": "repos/p", "ele-newretail-drug": "repos/ele-newretail-drug"}`.
113
83
  - Each node has `repo` (repo name) and `source_file` (relative to the repo root).
114
84
  - Full path = `${CLAUDE_PROJECT_DIR}` + `/` + `repo_roots[node.repo]` + `/` + `node.source_file`.
115
85
  - `main-trace --sources` already outputs resolved paths in `repos/<repo>/<file>` format.
86
+ - If `${CLAUDE_PROJECT_DIR}` or the target repo is not available, do not fail the query; cite the graph source path and report source reading as unavailable.
116
87
 
117
88
  Missing field-level graph evidence is not automatically a graph defect. Classify it as one of:
118
89
 
@@ -138,20 +109,6 @@ Search discipline:
138
109
  - If code reading is genuinely necessary, inspect only trace source files or a very small named set of files. Exclude generated graph output, HTML, caches, and unrelated repo directories.
139
110
  - Prefer additional graphify tool calls over raw shell searches when adjusting scope, parameters, or neighboring scenarios.
140
111
 
141
- For `/ckgraphify learn`:
142
-
143
- 1. Require concept, scenario, repos, graph, map, and out.
144
- 2. Read `${CLAUDE_PROJECT_DIR}/ckgraphify/docs/business-map-policy.md`; every map iteration must follow it or explicitly report the conflict.
145
- 3. Read existing map scenarios, flows, gaps, accepted gaps, verification scopes, and `search_constraints`.
146
- 4. Search graph nodes with aliases, keywords, page/API names, HSF names, and repo hints.
147
- 5. Run `main-trace` on promising graph matches.
148
- 6. Validate important links in code, especially when the answer depends on response fields, request parameters, extension points, or method-local branching.
149
- 7. For a simple branch, write a candidate patch to `--out` with scenario-level anchors, trace hints, verification scope, boundaries, and remaining gaps. Keep detailed source evidence in `${CLAUDE_PLUGIN_ROOT}/graphify-out/business-map-evidence.json`, not in `business-map.json`.
150
- 8. For a complex branch, write one scenario with `flows`; each flow may have `id`, `name`, `status`, `summary`, `verification_scope`, `anchors`, `trace_hints`, `gaps`, and `accepted_gaps`. Keep `business-map.json` compact; use the evidence shadow table for source proof.
151
- 9. Do not split a user's named complex scenario into multiple top-level scenarios unless the user asks for that model.
152
- 10. Run `business-lint` and `business-validate`; run `business-trace` if hints are usable.
153
- 11. After writing to `${CLAUDE_PLUGIN_ROOT}/graphify-out/`, sync changes back to `${CLAUDE_PROJECT_DIR}/claude-plugin/graphify-out/`.
154
-
155
112
  ## Answer Contract
156
113
 
157
114
  Include:
@@ -168,16 +125,8 @@ Keep graph-main schema details, dependency extraction rules, and low-level branc
168
125
 
169
126
  ## Guardrails
170
127
 
171
- - Do not write business semantics into `graph-main.json`.
172
- - Before changing `business-map.json`, read `${CLAUDE_PROJECT_DIR}/ckgraphify/docs/business-map-policy.md`; follow it or state the conflict.
173
- - Use `scenario.flows[]` for sub-links inside one complex business scene, such as landing page purchase/refund/benefit branches.
174
- - Keep `business-map.json` as a search/trace index. Do not add long `evidence` arrays there; put audit proof in `graphify-out/business-map-evidence.json`.
175
- - Use `gaps` for active unknowns and `accepted_gaps` for known boundaries with a stop rule.
176
- - New learned scenarios go to candidate patch files first.
128
+ - Treat the Claude plugin as a read-only consumer of graph and business-map artifacts.
129
+ - Do not edit `graph-main.json`, `graph.json`, `business-map.json`, or `business-map-evidence.json` from this skill.
130
+ - Do not run learn, update, lint, validate, main-graph, or merge-main-graphs as part of answering user queries.
177
131
  - Missing graph evidence is a coverage gap, not proof that code behavior does not exist.
178
- - After modifying only `business-map.json`, run `graphify business-lint --map <map>` and `graphify business-validate --map <map> --graph <graph>`; `graphify update .` is not required for a map-only semantic edit.
179
- - After modifying code, docs, skills, extraction, merge, trace, business-map tooling, or HTML output, run:
180
-
181
- ```bash
182
- graphify update .
183
- ```
132
+ - If the answer needs knowledge not covered by the current artifacts, report the gap and suggest that the server-side kg production/learning workflow refresh the graph or business map.
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes