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.
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/PKG-INFO +8 -8
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/README.md +7 -7
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/__main__.py +2 -2
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/business_map.py +6 -3
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/graph_main_frontend.py +32 -3
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/graph_main_frontend_sdk.py +311 -22
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/graph_main_merge.py +30 -21
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/graph_main_trace.py +8 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/repo_registry.py +36 -6
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/pyproject.toml +1 -1
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/skill/skill-codex.md +4 -4
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/skill/skill.md +15 -66
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/LICENSE +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/MANIFEST.in +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/ckgraphify.egg-info/SOURCES.txt +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/__init__.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/analyze.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/benchmark.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/bridge_mtop.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/build.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/cache.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/callflow_html.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/cluster.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/dedup.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/detect.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/export.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/extract.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/global_graph.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/google_workspace.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/graph_main_backend.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/graph_main_html.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/hooks.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/ingest.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/llm.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/manifest.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/report.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/security.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/serve.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/transcribe.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/tree_html.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/validate.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/watch.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/graphify/wiki.py +0 -0
- {ckgraphify-0.1.4 → ckgraphify-0.2.0}/setup.cfg +0 -0
- {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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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) <
|
|
1333
|
+
if len(graph_paths) < 1:
|
|
1334
1334
|
print(
|
|
1335
|
-
"Usage: graphify merge-main-graphs <graph-main1.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
|
-
|
|
349
|
-
if
|
|
350
|
-
return
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
261
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
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
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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 =
|
|
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":
|
|
57
|
+
"group": group,
|
|
55
58
|
"git": str(raw.get("git", "") or ""),
|
|
56
59
|
}
|
|
57
|
-
|
|
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(
|
|
65
|
-
return f"{
|
|
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(
|
|
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(
|
|
88
|
+
out["id"] = _node_key(repo_id, local_id)
|
|
85
89
|
out["local_id"] = local_id
|
|
86
|
-
out["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(
|
|
96
|
+
def _copy_edge(repo_id: str, edge: dict) -> dict:
|
|
92
97
|
out = dict(edge)
|
|
93
|
-
out["source"] = _node_key(
|
|
94
|
-
out["target"] = _node_key(
|
|
95
|
-
out["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) <
|
|
201
|
-
raise ValueError("merge_graph_main_files requires at least
|
|
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
|
-
|
|
224
|
-
registry_meta = registry.get(
|
|
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[
|
|
235
|
+
repo_roots[repo_id] = f"repos/{repo_name}"
|
|
229
236
|
repo_meta = {
|
|
230
|
-
"repo":
|
|
231
|
-
"
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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(
|
|
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=
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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}` —
|
|
65
|
-
- Graph and business map (read
|
|
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
|
|
69
|
-
- Repos (code reading
|
|
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
|
-
-
|
|
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
|
|
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
|
-
-
|
|
172
|
-
-
|
|
173
|
-
-
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|