@hupan56/wlkj 2.7.12 → 3.1.2

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 (253) hide show
  1. package/bin/cli.js +1435 -1107
  2. package/package.json +29 -29
  3. package/templates/qoder/agents/insight-planning.md +1 -1
  4. package/templates/qoder/agents/insight-research.md +28 -15
  5. package/templates/qoder/commands/optional/wl-insight.md +161 -161
  6. package/templates/qoder/commands/optional/wl-report.md +20 -39
  7. package/templates/qoder/commands/optional/wl-status.md +4 -4
  8. package/templates/qoder/commands/wl-code.md +2 -2
  9. package/templates/qoder/commands/wl-design.md +6 -6
  10. package/templates/qoder/commands/wl-init.md +3 -3
  11. package/templates/qoder/commands/wl-prd.md +26 -13
  12. package/templates/qoder/commands/wl-req.md +43 -0
  13. package/templates/qoder/commands/wl-search.md +78 -47
  14. package/templates/qoder/commands/wl-task.md +343 -40
  15. package/templates/qoder/commands/wl-test.md +43 -17
  16. package/templates/qoder/config.yaml +52 -14
  17. package/templates/qoder/hooks/post-tool-use.py +181 -0
  18. package/templates/qoder/hooks/session-start.py +94 -23
  19. package/templates/qoder/hooks/stop-eval.py +207 -0
  20. package/templates/qoder/hooks/user-prompt-submit.py +139 -0
  21. package/templates/qoder/rules/wl-pipeline.md +85 -49
  22. package/templates/qoder/scripts/README.md +102 -0
  23. package/templates/qoder/scripts/capability/__init__.py +26 -0
  24. package/templates/qoder/scripts/capability/__main__.py +72 -0
  25. package/templates/qoder/scripts/capability/adapters/__init__.py +29 -0
  26. package/templates/qoder/scripts/capability/adapters/cli.py +316 -0
  27. package/templates/qoder/scripts/capability/adapters/mcp.py +334 -0
  28. package/templates/qoder/scripts/capability/adapters/qw.py +271 -0
  29. package/templates/qoder/scripts/capability/caps/__init__.py +27 -0
  30. package/templates/qoder/scripts/capability/caps/context.py +36 -0
  31. package/templates/qoder/scripts/capability/caps/cron.py +50 -0
  32. package/templates/qoder/scripts/capability/caps/identity.py +43 -0
  33. package/templates/qoder/scripts/capability/caps/memory.py +71 -0
  34. package/templates/qoder/scripts/capability/caps/notify.py +41 -0
  35. package/templates/qoder/scripts/capability/caps/present.py +48 -0
  36. package/templates/qoder/scripts/capability/caps/repo.py +31 -0
  37. package/templates/qoder/scripts/capability/caps/sandbox.py +43 -0
  38. package/templates/qoder/scripts/capability/chain.py +92 -0
  39. package/templates/qoder/scripts/capability/memory_chain.py +41 -0
  40. package/templates/qoder/scripts/capability/registry.py +246 -0
  41. package/templates/qoder/scripts/capability/registry_mcp.py +250 -0
  42. package/templates/qoder/scripts/capability/smoke_test.py +211 -0
  43. package/templates/qoder/scripts/capability/smoke_test_report.json +94 -0
  44. package/templates/qoder/scripts/deployment/setup/__init__.py +11 -0
  45. package/templates/qoder/scripts/deployment/setup/carriers.py +669 -0
  46. package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
  47. package/templates/qoder/scripts/{init_doctor.py → deployment/setup/init_doctor.py} +100 -39
  48. package/templates/qoder/scripts/{install_qoderwork.py → deployment/setup/install_qoderwork.py} +48 -30
  49. package/templates/qoder/scripts/{platform_doctor.py → deployment/setup/platform_doctor.py} +271 -259
  50. package/templates/qoder/scripts/{repo_root.py → deployment/setup/repo_root.py} +9 -2
  51. package/templates/qoder/scripts/{setup.py → deployment/setup/setup.py} +143 -14
  52. package/templates/qoder/scripts/{setup_lanhu.py → deployment/setup/setup_lanhu.py} +979 -963
  53. package/templates/qoder/scripts/domain/design/__init__.py +0 -0
  54. package/templates/qoder/scripts/{fill_prototype.py → domain/design/fill_prototype.py} +17 -5
  55. package/templates/qoder/scripts/{gen_design_doc.py → domain/design/gen_design_doc.py} +406 -394
  56. package/templates/qoder/scripts/domain/kg/__init__.py +11 -0
  57. package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
  58. package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +201 -0
  59. package/templates/qoder/scripts/domain/kg/build/build_relations.py +132 -0
  60. package/templates/qoder/scripts/{build_style_index.py → domain/kg/build/build_style_index.py} +50 -15
  61. package/templates/qoder/scripts/domain/kg/build/build_workflows.py +149 -0
  62. package/templates/qoder/scripts/{kg_build.py → domain/kg/build/kg_build.py} +685 -612
  63. package/templates/qoder/scripts/{kg_build_db.py → domain/kg/build/kg_build_db.py} +338 -327
  64. package/templates/qoder/scripts/{kg_incremental.py → domain/kg/build/kg_incremental.py} +425 -393
  65. package/templates/qoder/scripts/{learn_aggregate.py → domain/kg/build/learn_aggregate.py} +212 -201
  66. package/templates/qoder/scripts/domain/kg/extract/__init__.py +0 -0
  67. package/templates/qoder/scripts/{common → domain/kg/extract}/extract.py +430 -419
  68. package/templates/qoder/scripts/domain/kg/extract/test_extract.py +115 -0
  69. package/templates/qoder/scripts/{common → domain/kg/extract}/ts_extract.py +614 -536
  70. package/templates/qoder/scripts/domain/kg/graph/__init__.py +0 -0
  71. package/templates/qoder/scripts/{kg_link_db.py → domain/kg/graph/kg_link_db.py} +235 -224
  72. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
  73. package/templates/qoder/scripts/{kg.py → domain/kg/kg.py} +871 -708
  74. package/templates/qoder/scripts/domain/kg/kg_capabilities.py +182 -0
  75. package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
  76. package/templates/qoder/scripts/{context_pack.py → domain/kg/search/context_pack.py} +45 -17
  77. package/templates/qoder/scripts/{enrich_prompt.py → domain/kg/search/enrich_prompt.py} +238 -226
  78. package/templates/qoder/scripts/domain/kg/search/prefetch.py +384 -0
  79. package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
  80. package/templates/qoder/scripts/{search_index.py → domain/kg/search/search_index.py} +210 -88
  81. package/templates/qoder/scripts/domain/kg/server/__init__.py +0 -0
  82. package/templates/qoder/scripts/domain/kg/server/kgd.py +549 -0
  83. package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
  84. package/templates/qoder/scripts/domain/kg/storage/__init__.py +0 -0
  85. package/templates/qoder/scripts/{kg_duckdb.py → domain/kg/storage/kg_duckdb.py} +70 -45
  86. package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
  87. package/templates/qoder/scripts/{learn.py → domain/learning/learn.py} +157 -146
  88. package/templates/qoder/scripts/domain/report/__init__.py +11 -0
  89. package/templates/qoder/scripts/{add_session.py → domain/report/add_session.py} +256 -244
  90. package/templates/qoder/scripts/{export.py → domain/report/export.py} +72 -4
  91. package/templates/qoder/scripts/{report.py → domain/report/report.py} +292 -281
  92. package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
  93. package/templates/qoder/scripts/domain/report/role.py +39 -0
  94. package/templates/qoder/scripts/{status.py → domain/report/status.py} +737 -628
  95. package/templates/qoder/scripts/domain/report/status_snapshot.py +191 -0
  96. package/templates/qoder/scripts/domain/requirement/__init__.py +0 -0
  97. package/templates/qoder/scripts/{archive_prd.py → domain/requirement/archive_prd.py} +389 -377
  98. package/templates/qoder/scripts/domain/requirement/req.py +228 -0
  99. package/templates/qoder/scripts/domain/task/__init__.py +11 -0
  100. package/templates/qoder/scripts/{git_sync.py → domain/task/git_sync.py} +90 -50
  101. package/templates/qoder/scripts/{syncgate.py → domain/task/syncgate.py} +18 -7
  102. package/templates/qoder/scripts/domain/task/task.py +229 -0
  103. package/templates/qoder/scripts/domain/task/task_lifecycle.py +606 -0
  104. package/templates/qoder/scripts/domain/task/task_query.py +162 -0
  105. package/templates/qoder/scripts/domain/task/task_relations.py +425 -0
  106. package/templates/qoder/scripts/{team_sync.py → domain/task/team_sync.py} +101 -23
  107. package/templates/qoder/scripts/domain/task/zentao_sync.py +370 -0
  108. package/templates/qoder/scripts/foundation/__init__.py +0 -0
  109. package/templates/qoder/scripts/foundation/bootstrap.py +145 -0
  110. package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
  111. package/templates/qoder/scripts/foundation/core/cmd_registry.py +112 -0
  112. package/templates/qoder/scripts/foundation/core/config.py +187 -0
  113. package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -400
  114. package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
  115. package/templates/qoder/scripts/foundation/data/contract.py +317 -0
  116. package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
  117. package/templates/qoder/scripts/foundation/data/result.py +223 -0
  118. package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -392
  119. package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
  120. package/templates/qoder/scripts/foundation/identity/check_publish.py +103 -0
  121. package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -238
  122. package/templates/qoder/scripts/foundation/identity/guard.py +159 -0
  123. package/templates/qoder/scripts/{common → foundation/identity}/identity.py +132 -9
  124. package/templates/qoder/scripts/foundation/identity/roles.py +60 -0
  125. package/templates/qoder/scripts/foundation/integrations/__init__.py +0 -0
  126. package/templates/qoder/scripts/{common → foundation/integrations}/active_task.py +30 -15
  127. package/templates/qoder/scripts/{common → foundation/integrations}/feishu.py +11 -10
  128. package/templates/qoder/scripts/{common → foundation/integrations}/terms.py +3 -3
  129. package/templates/qoder/scripts/foundation/integrations/zentao_client.py +220 -0
  130. package/templates/qoder/scripts/foundation/io/__init__.py +0 -0
  131. package/templates/qoder/scripts/{common → foundation/io}/filelock.py +1 -1
  132. package/templates/qoder/scripts/{common → foundation/io}/reqid.py +1 -1
  133. package/templates/qoder/scripts/foundation/protocol/__init__.py +0 -0
  134. package/templates/qoder/scripts/foundation/protocol/duckdb_conn.py +39 -0
  135. package/templates/qoder/scripts/foundation/protocol/mcp_base.py +268 -0
  136. package/templates/qoder/scripts/foundation/utils/__init__.py +0 -0
  137. package/templates/qoder/scripts/{common → foundation/utils}/eval_api.py +12 -1
  138. package/templates/qoder/scripts/{common → foundation/utils}/pip_install.py +23 -6
  139. package/templates/qoder/scripts/{common → foundation/utils}/platform_guard.py +1 -1
  140. package/templates/qoder/scripts/orchestration/__init__.py +0 -0
  141. package/templates/qoder/scripts/orchestration/wlkj.py +185 -0
  142. package/templates/qoder/scripts/protocol/__init__.py +0 -0
  143. package/templates/qoder/scripts/protocol/browser/README.md +207 -0
  144. package/templates/qoder/scripts/protocol/browser/SKILL.md +265 -0
  145. package/templates/qoder/scripts/protocol/browser/config.env +9 -0
  146. package/templates/qoder/scripts/protocol/browser/references/cdp-api.md +110 -0
  147. package/templates/qoder/scripts/protocol/browser/references/migration-2.5.3.md +72 -0
  148. package/templates/qoder/scripts/protocol/browser/references/site-patterns/.gitkeep +0 -0
  149. package/templates/qoder/scripts/protocol/browser/scripts/browser-discovery.mjs +138 -0
  150. package/templates/qoder/scripts/protocol/browser/scripts/cdp-proxy.mjs +672 -0
  151. package/templates/qoder/scripts/protocol/browser/scripts/check-deps.mjs +206 -0
  152. package/templates/qoder/scripts/protocol/browser/scripts/find-url.mjs +253 -0
  153. package/templates/qoder/scripts/protocol/browser/scripts/match-site.mjs +46 -0
  154. package/templates/qoder/scripts/protocol/browser/templates/config.env.template +9 -0
  155. package/templates/qoder/scripts/protocol/mcp/__init__.py +11 -0
  156. package/templates/qoder/scripts/protocol/mcp/kg_mcp_server.py +478 -0
  157. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → protocol/mcp/lanhu_stdio_wrapper.py} +131 -119
  158. package/templates/qoder/scripts/protocol/mcp/mcp_doctor.py +548 -0
  159. package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
  160. package/templates/qoder/scripts/protocol/mcp/mysql_mcp_server.py +461 -0
  161. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1920 -0
  162. package/templates/qoder/scripts/protocol/transports/__init__.py +56 -0
  163. package/templates/qoder/scripts/protocol/transports/base.py +87 -0
  164. package/templates/qoder/scripts/protocol/transports/cli.py +132 -0
  165. package/templates/qoder/scripts/protocol/transports/http.py +141 -0
  166. package/templates/qoder/scripts/protocol/transports/stdio.py +293 -0
  167. package/templates/qoder/scripts/run_weekly_update.bat +27 -18
  168. package/templates/qoder/scripts/run_weekly_update.sh +22 -13
  169. package/templates/qoder/scripts/validation/__init__.py +0 -0
  170. package/templates/qoder/scripts/validation/eval/__init__.py +0 -0
  171. package/templates/qoder/scripts/validation/eval/perf-report-2026-06-24.md +139 -0
  172. package/templates/qoder/scripts/validation/eval/qwork_harness.py +617 -0
  173. package/templates/qoder/scripts/validation/eval/report-commands.md +220 -0
  174. package/templates/qoder/scripts/validation/eval/transcript_timing.py +386 -0
  175. package/templates/qoder/scripts/validation/metrics/__init__.py +0 -0
  176. package/templates/qoder/scripts/{eval_prd.py → validation/metrics/eval_prd.py} +84 -15
  177. package/templates/qoder/scripts/validation/metrics/usability_score.py +750 -0
  178. package/templates/qoder/scripts/validation/test/__init__.py +11 -0
  179. package/templates/qoder/scripts/validation/test/assertion_gen.py +551 -0
  180. package/templates/qoder/scripts/{autotest.py → validation/test/autotest.py} +1234 -1751
  181. package/templates/qoder/scripts/validation/test/autotest_auth.py +109 -0
  182. package/templates/qoder/scripts/{autotest_batch.py → validation/test/autotest_batch.py} +255 -224
  183. package/templates/qoder/scripts/validation/test/autotest_data.py +680 -0
  184. package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
  185. package/templates/qoder/scripts/{autotest_run.py → validation/test/autotest_run.py} +323 -297
  186. package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
  187. package/templates/qoder/scripts/{benchmark.py → validation/test/benchmark.py} +17 -5
  188. package/templates/qoder/scripts/{kg_auto_login.py → validation/test/kg_auto_login.py} +208 -196
  189. package/templates/qoder/scripts/{kg_test_runner.py → validation/test/kg_test_runner.py} +13 -2
  190. package/templates/qoder/scripts/{page_probe.py → validation/test/page_probe.py} +470 -459
  191. package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
  192. package/templates/qoder/settings.json +23 -2
  193. package/templates/qoder/skills/design-import/SKILL.md +235 -226
  194. package/templates/qoder/skills/design-review/SKILL.md +91 -82
  195. package/templates/qoder/skills/prd-generator/SKILL.md +38 -24
  196. package/templates/qoder/skills/prd-review/SKILL.md +8 -6
  197. package/templates/qoder/skills/prototype-generator/SKILL.md +296 -247
  198. package/templates/qoder/skills/spec-coder/SKILL.md +24 -15
  199. package/templates/qoder/skills/spec-generator/SKILL.md +24 -12
  200. package/templates/qoder/skills/test-generator/SKILL.md +9 -7
  201. package/templates/qoder/skills/wl-code/SKILL.md +25 -13
  202. package/templates/qoder/skills/wl-commit/SKILL.md +10 -9
  203. package/templates/qoder/skills/wl-design/SKILL.md +7 -5
  204. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  205. package/templates/qoder/skills/wl-insight/SKILL.md +25 -12
  206. package/templates/qoder/skills/wl-prd-full/SKILL.md +59 -8
  207. package/templates/qoder/skills/wl-prd-quick/SKILL.md +7 -7
  208. package/templates/qoder/skills/wl-prd-review/SKILL.md +18 -6
  209. package/templates/qoder/skills/wl-report/SKILL.md +27 -25
  210. package/templates/qoder/skills/wl-search/SKILL.md +151 -80
  211. package/templates/qoder/skills/wl-spec/SKILL.md +9 -7
  212. package/templates/qoder/skills/wl-status/SKILL.md +50 -18
  213. package/templates/qoder/skills/wl-task/SKILL.md +94 -12
  214. package/templates/qoder/skills/wl-test/SKILL.md +154 -45
  215. package/templates/qoder/templates/prd-full-template.md +7 -0
  216. package/templates/root/AGENTS.md +275 -237
  217. package/templates/root/requirements.txt +3 -0
  218. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +3 -3
  219. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +2 -2
  220. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  221. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  222. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  223. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  224. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  225. package/templates/qoder/scripts/check_carriers.py +0 -238
  226. package/templates/qoder/scripts/check_mcp.py +0 -298
  227. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  228. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  229. package/templates/qoder/scripts/collect_prds.py +0 -31
  230. package/templates/qoder/scripts/common/mentions.py +0 -134
  231. package/templates/qoder/scripts/common/utf8.py +0 -38
  232. package/templates/qoder/scripts/extract_api_params.py +0 -246
  233. package/templates/qoder/scripts/extract_routes.py +0 -54
  234. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  235. package/templates/qoder/scripts/handoff.py +0 -22
  236. package/templates/qoder/scripts/init_developer.py +0 -76
  237. package/templates/qoder/scripts/kg_mcp_server.py +0 -801
  238. package/templates/qoder/scripts/kg_semantic.py +0 -150
  239. package/templates/qoder/scripts/mcp_launcher.py +0 -414
  240. package/templates/qoder/scripts/mysql_mcp_server.py +0 -396
  241. package/templates/qoder/scripts/parse_prds.py +0 -33
  242. package/templates/qoder/scripts/role.py +0 -51
  243. package/templates/qoder/scripts/sync_carriers.py +0 -259
  244. package/templates/qoder/scripts/task.py +0 -1261
  245. package/templates/qoder/scripts/workspace_init.py +0 -102
  246. package/templates/qoder/scripts/zentao_mcp_server.py +0 -424
  247. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  248. /package/templates/qoder/scripts/{common → deployment}/__init__.py +0 -0
  249. /package/templates/qoder/{skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp → scripts/domain/__init__.py} +0 -0
  250. /package/templates/qoder/scripts/{common → domain/kg/graph}/graph_traverse.py +0 -0
  251. /package/templates/qoder/scripts/{common → domain/kg/server}/repowiki.py +0 -0
  252. /package/templates/qoder/scripts/{common → foundation/io}/atomicio.py +0 -0
  253. /package/templates/qoder/scripts/{secure-ls.js → validation/test/secure-ls.js} +0 -0
@@ -1,612 +1,685 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- Unified knowledge graph builder code → DuckDB directly, no JSON.
4
- PROJECT-AGNOSTIC: auto-discovers project structure, no hardcoded names.
5
-
6
- Discovers for each project under data/code/:
7
- - Language/framework (Vue/React/Vant/Element/Ant/jQuery...)
8
- - Module directories (apps/*/src/api, src/modules, src/views, pages/, ...)
9
- - API definition patterns (requestClient, axios, fetch, @RequestMapping...)
10
- - Test files (*Test.java, *.test.ts, *.spec.js...)
11
- - Layout config (preferences.ts, vue.config.js, package.json...)
12
- """
13
- import json
14
- import os
15
- import re
16
- import sys
17
- import time
18
-
19
- SCRIPTS = os.path.dirname(os.path.abspath(__file__))
20
- sys.path.insert(0, SCRIPTS)
21
-
22
- from common.paths import DATA_INDEX_DIR, PROJECT_ROOT
23
- from kg_duckdb import get_db, init_schema, _batch_insert
24
-
25
- CODE_DIR = str(PROJECT_ROOT / 'data' / 'code')
26
- DB_PATH = str(DATA_INDEX_DIR / 'kg.duckdb')
27
-
28
-
29
- # ── Project Discovery (zero hardcoded assumptions) ──────────────────
30
-
31
- def discover_projects():
32
- """Scan data/code/ and discover each project's structure dynamically.
33
-
34
- Returns list of ProjectConfig dicts:
35
- {name, path, framework, lang, api_dirs, view_dirs, module_dirs,
36
- test_globs, layout_file, has_vue, has_java}
37
- """
38
- projects = []
39
- if not os.path.isdir(CODE_DIR):
40
- return projects
41
-
42
- for name in sorted(os.listdir(CODE_DIR)):
43
- path = os.path.join(CODE_DIR, name)
44
- if not os.path.isdir(path):
45
- continue
46
-
47
- cfg = {'name': name, 'path': path}
48
- _discover_structure(cfg)
49
- if cfg.get('framework') or cfg.get('has_vue') or cfg.get('has_java'):
50
- projects.append(cfg)
51
-
52
- return projects
53
-
54
-
55
- def _discover_structure(cfg):
56
- """Auto-detect project framework, directories, patterns."""
57
- path = cfg['path']
58
- cfg['has_vue'] = False
59
- cfg['has_java'] = False
60
- cfg['has_ts'] = False
61
- cfg['framework'] = 'unknown'
62
- cfg['api_dirs'] = [] # directories containing API definitions
63
- cfg['view_dirs'] = [] # directories containing page components
64
- cfg['module_dirs'] = [] # directories representing business modules
65
- cfg['test_globs'] = [] # patterns for test files
66
- cfg['layout_file'] = None # preferences.ts / vue.config.js / etc
67
- cfg['package_json'] = None
68
-
69
- # Scan for markers (limit depth for speed)
70
- vue_count = 0
71
- java_count = 0
72
- ts_count = 0
73
- found_dirs = {'src/views': [], 'src/api': [], 'src/modules': [],
74
- 'pages': [], 'app': [], 'src/components': []}
75
-
76
- for root, dirs, files in os.walk(path):
77
- dirs[:] = [d for d in dirs if d not in ('node_modules', '.git', 'dist',
78
- '__pycache__', '.svn', 'target', 'build')]
79
- # Limit depth
80
- depth = root[len(path):].count(os.sep)
81
- if depth > 6:
82
- dirs[:] = []
83
- continue
84
-
85
- rel_root = os.path.relpath(root, path).replace('\\', '/')
86
-
87
- # Detect framework from package.json
88
- for f in files:
89
- if f == 'package.json':
90
- cfg['package_json'] = os.path.join(root, f)
91
- _detect_framework_from_pkgjson(cfg)
92
- if f == 'preferences.ts':
93
- cfg['layout_file'] = os.path.join(root, f)
94
- if f == 'vue.config.js':
95
- if not cfg['layout_file']:
96
- cfg['layout_file'] = os.path.join(root, f)
97
-
98
- # Count file types
99
- for f in files:
100
- if f.endswith('.vue'):
101
- vue_count += 1
102
- elif f.endswith('.java'):
103
- java_count += 1
104
- elif f.endswith('.ts') or f.endswith('.tsx'):
105
- ts_count += 1
106
-
107
- # Detect known directory patterns (relative to project root)
108
- for pattern in list(found_dirs.keys()):
109
- if rel_root.endswith(pattern) or rel_root == pattern:
110
- found_dirs[pattern].append(root)
111
- # Also check monorepo: apps/*/src/views
112
- if '/apps/' in rel_root:
113
- for sub_pat in ['src/views', 'src/api']:
114
- if rel_root.endswith(sub_pat):
115
- found_dirs[sub_pat].append(root)
116
-
117
- cfg['has_vue'] = vue_count > 0
118
- cfg['has_java'] = java_count > 0
119
- cfg['has_ts'] = ts_count > 0
120
-
121
- # Assign discovered dirs
122
- cfg['view_dirs'] = found_dirs['src/views'] + found_dirs['pages']
123
- cfg['api_dirs'] = found_dirs['src/api']
124
- cfg['module_dirs'] = found_dirs['src/modules'] + found_dirs['src/api']
125
-
126
- # Test file patterns (auto-detect)
127
- if cfg['has_java']:
128
- cfg['test_globs'].append('*Test.java')
129
- if cfg['has_ts'] or cfg['has_vue']:
130
- cfg['test_globs'].extend(['*.test.ts', '*.spec.ts', '*.test.js'])
131
-
132
- # If no framework detected from package.json, infer from file types
133
- if cfg['framework'] == 'unknown':
134
- if vue_count > 10:
135
- cfg['framework'] = 'vue'
136
- elif java_count > 10:
137
- cfg['framework'] = 'java'
138
- elif ts_count > 10:
139
- cfg['framework'] = 'typescript'
140
-
141
-
142
- def _detect_framework_from_pkgjson(cfg):
143
- """Detect UI framework from package.json dependencies."""
144
- try:
145
- pkg = json.load(open(cfg['package_json'], encoding='utf-8'))
146
- except:
147
- return
148
- deps = set((pkg.get('dependencies') or {}).keys())
149
- deps |= set((pkg.get('devDependencies') or {}).keys())
150
- if not deps:
151
- return
152
-
153
- # Also check monorepo apps/*/package.json
154
- apps_dir = os.path.join(os.path.dirname(cfg['package_json']), 'apps')
155
- if os.path.isdir(apps_dir):
156
- for app in os.listdir(apps_dir):
157
- app_pkg = os.path.join(apps_dir, app, 'package.json')
158
- if os.path.isfile(app_pkg):
159
- try:
160
- ap = json.load(open(app_pkg, encoding='utf-8'))
161
- deps |= set((ap.get('dependencies') or {}).keys())
162
- deps |= set((ap.get('devDependencies') or {}).keys())
163
- except:
164
- pass
165
-
166
- if any(d.startswith('@vben/') for d in deps):
167
- cfg['framework'] = 'vben-admin'
168
- elif 'vant' in deps:
169
- cfg['framework'] = 'vant-mobile'
170
- elif 'react' in deps and 'react-dom' in deps:
171
- cfg['framework'] = 'react'
172
- elif 'ant-design-vue' in deps:
173
- cfg['framework'] = 'ant-design-vue'
174
- elif 'element-plus' in deps:
175
- cfg['framework'] = 'element-plus'
176
- elif 'vue' in deps:
177
- cfg['framework'] = 'vue'
178
-
179
-
180
- # ── Extractors (work on any project, parameterized by ProjectConfig) ──
181
-
182
- def extract_keywords_and_apis(cfg):
183
- """Extract keyword→file index + API endpoints for one project."""
184
- kw_index = {}
185
- api_index = {}
186
- file_count = 0
187
-
188
- exts = ['.vue', '.js', '.ts', '.jsx', '.tsx', '.java', '.xml', '.yml', '.yaml', '.properties']
189
- for root, dirs, files in os.walk(cfg['path']):
190
- dirs[:] = [d for d in dirs if d not in ('node_modules', '.git', 'dist', '__pycache__', '.svn', 'target', 'build')]
191
- for f in files:
192
- if not any(f.endswith(e) for e in exts):
193
- continue
194
- filepath = os.path.join(root, f)
195
- rel = cfg['name'] + '/' + os.path.relpath(filepath, cfg['path']).replace('\\', '/')
196
- file_count += 1
197
- try:
198
- content = open(filepath, encoding='utf-8', errors='ignore').read()
199
- except:
200
- continue
201
-
202
- if f.endswith('.java'):
203
- for m in re.finditer(r'(?:class|interface|enum)\s+(\w+)', content):
204
- for kw in [m.group(1).lower()] + re.findall(r'[A-Z]?[a-z]+', m.group(1)):
205
- kw_index.setdefault(kw.lower(), []).append(rel)
206
- for m in re.finditer(r'@(?:Get|Post|Put|Delete|Request)Mapping\s*\(?["\']([^"\']+)["\']', content):
207
- api_index[m.group(1)] = rel
208
- else:
209
- name_m = re.search(r'name:\s*["\'](\w+)["\']', content)
210
- if name_m:
211
- kw_index.setdefault(name_m.group(1).lower(), []).append(rel)
212
- for pat in [r'requestClient\.\w+\s*\(?["\']([^"\']+)',
213
- r'(?:axios|request|fetch|http)\.\w+\s*\(?["\']([^"\']+)',
214
- r'path:\s*["\']([^"\']+)["\']']:
215
- for m in re.finditer(pat, content):
216
- kw_index.setdefault('api:' + m.group(1), []).append(rel)
217
- for m in re.finditer(r'[A-Z]?[a-z]+', f.rsplit('.', 1)[0]):
218
- kw = m.group().lower()
219
- if len(kw) >= 3:
220
- kw_index.setdefault(kw, []).append(rel)
221
-
222
- return kw_index, api_index, file_count
223
-
224
-
225
- def extract_ui_layer(cfg):
226
- """Extract pages + fields + modules for one project (framework-aware)."""
227
- pages = []
228
- field_map = {}
229
- modules = []
230
-
231
- # Modules: auto-discover from api_dirs and module_dirs
232
- seen_mods = set()
233
- for d in cfg.get('module_dirs', []):
234
- try:
235
- for sub in sorted(os.listdir(d)):
236
- if sub.startswith('.') or sub.startswith('_'):
237
- continue
238
- sub_path = os.path.join(d, sub)
239
- if os.path.isdir(sub_path):
240
- mod_key = sub.lower()
241
- if mod_key not in seen_mods:
242
- seen_mods.add(mod_key)
243
- rel = os.path.relpath(sub_path, cfg['path']).replace('\\', '/')
244
- modules.append({'dir': sub, 'cn': sub, 'project': cfg['name'], 'path': rel})
245
- except:
246
- pass
247
-
248
- # Pages: scan view_dirs
249
- table_marks = {
250
- 'vben-admin': ['VxeGrid', 'useVbenVxeGrid', '<a-table'],
251
- 'ant-design-vue': ['<a-table', 'a-table'],
252
- 'element-plus': ['<el-table', 'el-table'],
253
- 'vant-mobile': ['<van-list', 'van-list'],
254
- 'vue': ['<a-table', '<el-table', '<vxe-table'],
255
- }.get(cfg.get('framework', ''), ['<table'])
256
- table_marks_lower = [m.lower() for m in table_marks]
257
-
258
- form_marks = {
259
- 'vben-admin': ['useVbenForm', '<a-form'],
260
- 'ant-design-vue': ['<a-form', 'a-form'],
261
- 'element-plus': ['<el-form', 'el-form'],
262
- 'vant-mobile': ['<van-form', 'van-form'],
263
- 'vue': ['<a-form', '<el-form'],
264
- }.get(cfg.get('framework', ''), ['<form'])
265
-
266
- for view_dir in cfg.get('view_dirs', []):
267
- for root, dirs, files in os.walk(view_dir):
268
- dirs[:] = [d for d in dirs if d not in ('node_modules', 'dist', '.git')]
269
- for f in files:
270
- if not f.endswith('.vue') and not f.endswith('.tsx') and not f.endswith('.jsx'):
271
- continue
272
- filepath = os.path.join(root, f)
273
- try:
274
- content = open(filepath, encoding='utf-8', errors='ignore').read()
275
- except:
276
- continue
277
-
278
- # Page type detection
279
- ptype = 'unknown'
280
- if any(m.lower() in content.lower() for m in table_marks_lower):
281
- ptype = 'table-page'
282
- elif any(m in content for m in form_marks):
283
- ptype = 'form-page'
284
- elif 'detail' in filepath.lower() or 'info' in filepath.lower():
285
- ptype = 'detail-page'
286
- if ptype == 'unknown':
287
- continue
288
-
289
- # Fields (generic patterns, framework-agnostic)
290
- for m in re.finditer(r"title:\s*['\"]([^'\"]+)['\"]\s*,\s*field:\s*['\"]([^'\"]+)['\"]", content):
291
- field_map.setdefault(m.group(2), {'titles': set(), 'files': []})
292
- field_map[m.group(2)]['titles'].add(m.group(1))
293
- field_map[m.group(2)]['files'].append(filepath)
294
- for m in re.finditer(r"fieldName:\s*['\"]([^'\"]+)['\"]\s*,\s*label:\s*['\"]([^'\"]+)['\"]", content):
295
- field_map.setdefault(m.group(1), {'titles': set(), 'files': []})
296
- field_map[m.group(1)]['titles'].add(m.group(2))
297
- field_map[m.group(1)]['files'].append(filepath)
298
-
299
- rel = filepath.replace('\\', '/')
300
- file_key = rel.split('/views/')[-1] if '/views/' in rel else \
301
- rel.split('/pages/')[-1] if '/pages/' in rel else os.path.basename(rel)
302
- pages.append({'file': file_key, 'project': cfg['name'], 'page_type': ptype})
303
-
304
- return pages, field_map, modules
305
-
306
-
307
- def extract_traces(cfg, api_table):
308
- """Extract button→API traces for one Vue project."""
309
- traces = []
310
- if not cfg.get('has_vue') or not api_table:
311
- return traces
312
- try:
313
- from common.ts_extract import ts_trace_clicks
314
- except:
315
- return traces
316
-
317
- for view_dir in cfg.get('view_dirs', []):
318
- for root, dirs, files in os.walk(view_dir):
319
- dirs[:] = [d for d in dirs if d not in ('node_modules', 'dist', '.git')]
320
- for f in files:
321
- if not f.endswith('.vue'):
322
- continue
323
- filepath = os.path.join(root, f)
324
- try:
325
- content = open(filepath, encoding='utf-8', errors='ignore').read()
326
- except:
327
- continue
328
- if '@click' not in content:
329
- continue
330
- traces.extend(ts_trace_clicks(filepath, content, api_table))
331
- return traces
332
-
333
-
334
- def extract_tests(cfg):
335
- """Extract test assertions for one project."""
336
- results = []
337
- try:
338
- from common.test_extract import extract_test_assertions
339
- results = extract_test_assertions(cfg['path'])
340
- except:
341
- pass
342
- return results
343
-
344
-
345
- def extract_layout_fingerprint(cfg):
346
- """Extract layout/design fingerprint from project config."""
347
- fp = {}
348
- layout_file = cfg.get('layout_file')
349
- if layout_file and layout_file.endswith('preferences.ts'):
350
- try:
351
- from build_style_index import extract_layout_fingerprint as _elf
352
- fp = _elf(cfg['path'])
353
- except:
354
- pass
355
- elif cfg.get('package_json'):
356
- # Infer from package.json
357
- fp['stack'] = cfg.get('framework', 'unknown')
358
- fp['stack'] = fp.get('stack', cfg.get('framework', 'unknown'))
359
- return fp
360
-
361
-
362
- # ── Main build (project-agnostic) ───────────────────────────────────
363
-
364
- def build_all():
365
- """Full rebuild: discover projects → extract → DuckDB. Zero hardcoded names."""
366
- t0 = time.time()
367
- con = get_db()
368
- init_schema(con)
369
-
370
- # Clear all
371
- for table in ['entities', 'aliases', 'edges', 'keywords', 'endpoints', 'prds',
372
- 'wiki', 'pages', 'fields', 'modules', 'traces', 'api_functions',
373
- 'tests', 'workflows', 'style_meta', 'build_meta']:
374
- con.execute('DELETE FROM %s' % table)
375
-
376
- # 1. Discover projects
377
- print(' [0/7] Discovering projects...')
378
- projects = discover_projects()
379
- print(' Found %d projects:' % len(projects))
380
- for p in projects:
381
- print(' %s: %s (%s, %d api_dirs, %d view_dirs)' % (
382
- p['name'], p['framework'], 'vue' if p['has_vue'] else 'java' if p['has_java'] else '?',
383
- len(p['api_dirs']), len(p['view_dirs'])))
384
-
385
- stats = {}
386
-
387
- # 2. Keywords + APIs (all projects)
388
- print(' [1/7] keywords + APIs...')
389
- all_kw_rows = []
390
- all_ep_rows = []
391
- for cfg in projects:
392
- kw_idx, api_idx, fc = extract_keywords_and_apis(cfg)
393
- for k, files in kw_idx.items():
394
- for f in files:
395
- all_kw_rows.append((k, f))
396
- for ep, f in api_idx.items():
397
- all_ep_rows.append((ep, f))
398
- _batch_insert(con, 'keywords', all_kw_rows)
399
- _batch_insert(con, 'endpoints', all_ep_rows, ignore=True)
400
- stats['keywords'] = len(all_kw_rows)
401
- stats['endpoints'] = len(all_ep_rows)
402
-
403
- # 3. UI layer (Vue/React projects only)
404
- print(' [2/7] UI pages + fields + modules...')
405
- all_pages = []
406
- all_fields = []
407
- all_modules = []
408
- style_meta = {}
409
- for cfg in projects:
410
- if not cfg.get('has_vue') and cfg['framework'] not in ('react',):
411
- continue
412
- pages, field_map, modules = extract_ui_layer(cfg)
413
- all_pages.extend(pages)
414
- for k, info in field_map.items():
415
- all_fields.append((k, '/'.join(sorted(info['titles'])[:2]), len(info['files']), '[]'))
416
- all_modules.extend(modules)
417
- fp = extract_layout_fingerprint(cfg)
418
- if fp:
419
- style_meta[cfg['name']] = fp
420
- page_rows = [(p['file'], p['project'], p['page_type'], '[]', '[]', '[]', '{}') for p in all_pages]
421
- _batch_insert(con, 'pages', page_rows, ignore=True)
422
- _batch_insert(con, 'fields', all_fields)
423
- mod_rows = [(m['dir'], m['cn'], m['project'], m['path']) for m in all_modules]
424
- _batch_insert(con, 'modules', mod_rows)
425
- for proj, fp in style_meta.items():
426
- con.execute("INSERT OR REPLACE INTO style_meta VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", [
427
- proj, fp.get('stack', ''), fp.get('layout', ''), fp.get('access_mode', ''),
428
- fp.get('sidebar_width'), fp.get('color_primary'), fp.get('color_success'),
429
- fp.get('color_warning'), fp.get('color_destructive'), fp.get('radius'), 0, '{}', '{}'])
430
- stats['pages'] = len(page_rows)
431
- stats['fields'] = len(all_fields)
432
- stats['modules'] = len(all_modules)
433
-
434
- # 4. API functions + traces (Vue projects with api_dirs)
435
- print(' [3/7] API functions...')
436
- api_table = {}
437
- for cfg in projects:
438
- for api_dir in cfg.get('api_dirs', []):
439
- try:
440
- from common.ts_extract import ts_build_api_fn_table
441
- t = ts_build_api_fn_table(api_dir)
442
- api_table.update(t)
443
- except:
444
- pass
445
- fn_rows = [(fn, info.get('url', ''), info.get('verb', ''), info.get('file', ''))
446
- for fn, info in api_table.items()]
447
- _batch_insert(con, 'api_functions', fn_rows, ignore=True)
448
- stats['api_functions'] = len(fn_rows)
449
-
450
- print(' [4/7] trace chain...')
451
- all_traces = []
452
- for cfg in projects:
453
- all_traces.extend(extract_traces(cfg, api_table))
454
- trace_rows = [(t.get('vue_file', '').replace('\\', '/'), t.get('button', ''),
455
- t.get('handler', ''), t.get('api_fn', ''), t.get('endpoint', ''),
456
- t.get('verb', ''), t.get('confidence', ''))
457
- for t in all_traces]
458
- _batch_insert(con, 'traces', trace_rows)
459
- stats['traces'] = len(trace_rows)
460
-
461
- # 5. Tests
462
- print(' [5/7] test assertions...')
463
- all_tests = []
464
- for cfg in projects:
465
- all_tests.extend(extract_tests(cfg))
466
- test_rows = [(t.get('class', ''), t.get('method', ''), t.get('package', ''),
467
- t.get('display_name', ''),
468
- json.dumps(t.get('injected_deps', []), ensure_ascii=False),
469
- json.dumps(t.get('assertions', []), ensure_ascii=False),
470
- t.get('is_integration', False))
471
- for t in all_tests]
472
- _batch_insert(con, 'tests', test_rows)
473
- stats['tests'] = len(test_rows)
474
-
475
- # 6. Entities + edges
476
- print(' [6/7] entity resolution + edges...')
477
- _build_entities_and_edges(con, all_modules, dict(all_ep_rows and [(r[0], r[1]) for r in all_ep_rows] or []),
478
- all_traces, all_tests)
479
- stats['entities'] = con.execute("SELECT COUNT(*) FROM entities").fetchone()[0]
480
- stats['edges'] = con.execute("SELECT COUNT(*) FROM edges").fetchone()[0]
481
-
482
- # 7. Workflows
483
- print(' [7/7] workflows...')
484
- wf_count = _build_workflows(con, all_traces)
485
- stats['workflows'] = wf_count
486
-
487
- con.execute("INSERT OR REPLACE INTO build_meta VALUES ('version', '2.0')")
488
- con.execute("INSERT OR REPLACE INTO build_meta VALUES ('built_at', ?)", [str(int(time.time()))])
489
- con.close()
490
-
491
- stats['elapsed'] = '%.1fs' % (time.time() - t0)
492
- return stats
493
-
494
-
495
- def _build_entities_and_edges(con, modules, api_idx, traces, tests):
496
- """Build entities + edges (project-agnostic)."""
497
- entity_rows = []
498
- alias_rows = []
499
- edge_rows = []
500
-
501
- # FEATURE entities
502
- feature_dirs = {}
503
- for m in modules:
504
- eid = 'F:%s:%s' % (m['project'], m['dir'])
505
- entity_rows.append((eid, 'FEATURE', m['dir'], m['cn'], '{}'))
506
- alias_rows.append((m['dir'].lower(), eid))
507
- alias_rows.append((m['cn'].lower(), eid))
508
- feature_dirs[m['dir'].lower()] = eid
509
-
510
- # ENDPOINT + CONTROLLER entities + edges
511
- seen_ctrl = set()
512
- for ep, ctrl_file in api_idx.items():
513
- eid = 'E:%s' % ep
514
- entity_rows.append((eid, 'ENDPOINT', ep, '', '{}'))
515
- ctrl_name = ctrl_file.split('/')[-1].replace('.java', '')
516
- ctrl_eid = 'C:%s' % ctrl_name
517
- if ctrl_name not in seen_ctrl:
518
- entity_rows.append((ctrl_eid, 'CONTROLLER', ctrl_name, '', '{}'))
519
- alias_rows.append((ctrl_name.lower(), ctrl_eid))
520
- seen_ctrl.add(ctrl_name)
521
- edge_rows.append((eid, ctrl_eid, 'handled_by', 0, '{}'))
522
-
523
- # BUTTON entities + click edges
524
- for t in traces:
525
- vue = (t.get('vue_file') or '').replace('\\', '/')
526
- handler = t.get('handler', '')
527
- vue_short = vue.split('/views/')[-1] if '/views/' in vue else vue.split('/pages/')[-1] if '/pages/' in vue else vue
528
- eid = 'B:%s:%s' % (vue_short, handler)
529
- entity_rows.append((eid, 'BUTTON', handler, '',
530
- json.dumps({'button_text': t.get('button', ''),
531
- 'confidence': t.get('confidence', '')}, ensure_ascii=False)))
532
- ep = t.get('endpoint')
533
- if ep and ep != 'None':
534
- edge_rows.append((eid, 'E:%s' % ep, 'clicks', 0,
535
- json.dumps({'button': t.get('button', ''),
536
- 'confidence': t.get('confidence', '')}, ensure_ascii=False)))
537
-
538
- # TEST entities + controller→test edges
539
- for t in tests:
540
- name = '%s.%s' % (t.get('class', ''), t.get('method', ''))
541
- eid = 'T:%s' % name
542
- entity_rows.append((eid, 'TEST', name, '',
543
- json.dumps({'deps': t.get('injected_deps', []),
544
- 'assertions': t.get('assertions', [])}, ensure_ascii=False)))
545
- for dep in t.get('injected_deps', []):
546
- dep_base = dep.replace('I', '', 1).replace('Impl', '').replace('Service', '')
547
- for ctrl_name in seen_ctrl:
548
- if dep_base.lower() in ctrl_name.lower():
549
- edge_rows.append(('C:%s' % ctrl_name, eid, 'tested_by', 0, '{}'))
550
-
551
- # FEATURE endpoints/buttons edges
552
- for m in modules:
553
- feat_eid = 'F:%s:%s' % (m['project'], m['dir'])
554
- mdir = m['dir'].lower()
555
- for ep in api_idx:
556
- if mdir in ep.lower():
557
- edge_rows.append((feat_eid, 'E:%s' % ep, 'has_endpoint', 0, '{}'))
558
- for t in traces:
559
- vue = (t.get('vue_file') or '').lower()
560
- if mdir in vue:
561
- vue_short = vue.split('/views/')[-1] if '/views/' in vue else vue
562
- btn_eid = 'B:%s:%s' % (vue_short, t.get('handler', ''))
563
- edge_rows.append((feat_eid, btn_eid, 'has_button', 0, '{}'))
564
-
565
- _batch_insert(con, 'entities', entity_rows, ignore=True)
566
- _batch_insert(con, 'aliases', alias_rows, ignore=True)
567
- _batch_insert(con, 'edges', edge_rows)
568
-
569
-
570
- def _build_workflows(con, traces):
571
- """Build workflow chains (project-agnostic)."""
572
- STATE_KW = {
573
- 'create': ['add', 'create', 'save', 'insert'],
574
- 'submit': ['submit', 'commit', 'apply'],
575
- 'execute': ['execute', 'start', 'begin'],
576
- 'complete': ['complete', 'finish', 'done', 'end'],
577
- 'archive': ['archive', 'close', 'settle'],
578
- 'delete': ['delete', 'remove', 'destroy'],
579
- 'export': ['export', 'download'],
580
- 'query': ['list', 'get', 'query', 'page', 'detail'],
581
- }
582
- from collections import defaultdict
583
- mod_ops = defaultdict(list)
584
- for t in traces:
585
- vue = (t.get('vue_file') or '').replace('\\', '/')
586
- mod = vue.split('/views/')[-1].split('/')[0] if '/views/' in vue else \
587
- vue.split('/pages/')[-1].split('/')[0] if '/pages/' in vue else 'unknown'
588
- text = ((t.get('button') or '') + ' ' + (t.get('endpoint') or '')).lower()
589
- for state, kws in STATE_KW.items():
590
- if any(k in text for k in kws):
591
- mod_ops[mod].append((state, t.get('button', ''), t.get('endpoint', '')))
592
- break
593
-
594
- chain_order = ['query', 'create', 'submit', 'execute', 'complete', 'archive', 'export', 'delete']
595
- wf_rows = []
596
- for mod, ops in mod_ops.items():
597
- for state in chain_order:
598
- matching = [o for o in ops if o[0] == state]
599
- for op in matching[:3]:
600
- wf_rows.append((mod, state, chain_order.index(state), op[1], op[2]))
601
- _batch_insert(con, 'workflows', wf_rows)
602
- return len(wf_rows)
603
-
604
-
605
- if __name__ == '__main__':
606
- print('=== kg_build.py: auto-discover → DuckDB (no JSON, no hardcode) ===')
607
- stats = build_all()
608
- print()
609
- print('Done in %s' % stats.pop('elapsed', '?'))
610
- for k, v in sorted(stats.items()):
611
- print(' %-16s: %s' % (k, v))
612
- print(' file: %s (%.1f KB)' % (DB_PATH, os.path.getsize(DB_PATH) / 1024))
1
+ # -*- coding: utf-8 -*-
2
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
3
+ import os as _o, sys as _s
4
+ _f = _o.path.abspath(__file__)
5
+ for _ in range(10):
6
+ _f = _o.path.dirname(_f)
7
+ _cp = _o.path.join(_f, 'foundation')
8
+ if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
9
+ break
10
+ if _cp not in _s.path: _s.path.insert(0, _cp)
11
+ from bootstrap import setup; setup()
12
+
13
+ """
14
+ Unified knowledge graph builder — code → DuckDB directly, no JSON.
15
+ PROJECT-AGNOSTIC: auto-discovers project structure, no hardcoded names.
16
+
17
+ Discovers for each project under data/code/:
18
+ - Language/framework (Vue/React/Vant/Element/Ant/jQuery...)
19
+ - Module directories (apps/*/src/api, src/modules, src/views, pages/, ...)
20
+ - API definition patterns (requestClient, axios, fetch, @RequestMapping...)
21
+ - Test files (*Test.java, *.test.ts, *.spec.js...)
22
+ - Layout config (preferences.ts, vue.config.js, package.json...)
23
+ """
24
+ import json
25
+ import os
26
+ import re
27
+ import sys
28
+ import time
29
+
30
+ SCRIPTS = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 子包→scripts/根
31
+ sys.path.insert(0, SCRIPTS)
32
+
33
+ from foundation.core.paths import DATA_INDEX_DIR, PROJECT_ROOT
34
+ from domain.kg.storage.kg_duckdb import get_db, init_schema, _batch_insert
35
+
36
+ CODE_DIR = str(PROJECT_ROOT / 'data' / 'code')
37
+ DB_PATH = str(DATA_INDEX_DIR / 'kg.duckdb')
38
+
39
+
40
+ # ── Project Discovery (zero hardcoded assumptions) ──────────────────
41
+
42
+ def discover_projects():
43
+ """Scan data/code/ and discover each project's structure dynamically.
44
+
45
+ Returns list of ProjectConfig dicts:
46
+ {name, path, framework, lang, api_dirs, view_dirs, module_dirs,
47
+ test_globs, layout_file, has_vue, has_java}
48
+ """
49
+ projects = []
50
+ if not os.path.isdir(CODE_DIR):
51
+ return projects
52
+
53
+ for name in sorted(os.listdir(CODE_DIR)):
54
+ path = os.path.join(CODE_DIR, name)
55
+ if not os.path.isdir(path):
56
+ continue
57
+
58
+ cfg = {'name': name, 'path': path}
59
+ _discover_structure(cfg)
60
+ if cfg.get('framework') or cfg.get('has_vue') or cfg.get('has_java'):
61
+ projects.append(cfg)
62
+
63
+ return projects
64
+
65
+
66
+ def _discover_structure(cfg):
67
+ """Auto-detect project framework, directories, patterns."""
68
+ path = cfg['path']
69
+ cfg['has_vue'] = False
70
+ cfg['has_java'] = False
71
+ cfg['has_ts'] = False
72
+ cfg['framework'] = 'unknown'
73
+ cfg['api_dirs'] = [] # directories containing API definitions
74
+ cfg['view_dirs'] = [] # directories containing page components
75
+ cfg['module_dirs'] = [] # directories representing business modules
76
+ cfg['test_globs'] = [] # patterns for test files
77
+ cfg['layout_file'] = None # preferences.ts / vue.config.js / etc
78
+ cfg['package_json'] = None
79
+
80
+ # Scan for markers (limit depth for speed)
81
+ vue_count = 0
82
+ java_count = 0
83
+ ts_count = 0
84
+ found_dirs = {'src/views': [], 'src/api': [], 'src/modules': [],
85
+ 'pages': [], 'app': [], 'src/components': []}
86
+
87
+ for root, dirs, files in os.walk(path):
88
+ dirs[:] = [d for d in dirs if d not in ('node_modules', '.git', 'dist',
89
+ '__pycache__', '.svn', 'target', 'build')]
90
+ # Limit depth
91
+ depth = root[len(path):].count(os.sep)
92
+ if depth > 6:
93
+ dirs[:] = []
94
+ continue
95
+
96
+ rel_root = os.path.relpath(root, path).replace('\\', '/')
97
+
98
+ # Detect framework from package.json
99
+ for f in files:
100
+ if f == 'package.json':
101
+ cfg['package_json'] = os.path.join(root, f)
102
+ _detect_framework_from_pkgjson(cfg)
103
+ if f == 'preferences.ts':
104
+ cfg['layout_file'] = os.path.join(root, f)
105
+ if f == 'vue.config.js':
106
+ if not cfg['layout_file']:
107
+ cfg['layout_file'] = os.path.join(root, f)
108
+
109
+ # Count file types
110
+ for f in files:
111
+ if f.endswith('.vue'):
112
+ vue_count += 1
113
+ elif f.endswith('.java'):
114
+ java_count += 1
115
+ elif f.endswith('.ts') or f.endswith('.tsx'):
116
+ ts_count += 1
117
+
118
+ # Detect known directory patterns (relative to project root)
119
+ for pattern in list(found_dirs.keys()):
120
+ if rel_root.endswith(pattern) or rel_root == pattern:
121
+ found_dirs[pattern].append(root)
122
+ # Also check monorepo: apps/*/src/views
123
+ if '/apps/' in rel_root:
124
+ for sub_pat in ['src/views', 'src/api']:
125
+ if rel_root.endswith(sub_pat):
126
+ found_dirs[sub_pat].append(root)
127
+
128
+ cfg['has_vue'] = vue_count > 0
129
+ cfg['has_java'] = java_count > 0
130
+ cfg['has_ts'] = ts_count > 0
131
+
132
+ # Assign discovered dirs
133
+ cfg['view_dirs'] = found_dirs['src/views'] + found_dirs['pages']
134
+ cfg['api_dirs'] = found_dirs['src/api']
135
+ cfg['module_dirs'] = found_dirs['src/modules'] + found_dirs['src/api']
136
+
137
+ # Test file patterns (auto-detect)
138
+ if cfg['has_java']:
139
+ cfg['test_globs'].append('*Test.java')
140
+ if cfg['has_ts'] or cfg['has_vue']:
141
+ cfg['test_globs'].extend(['*.test.ts', '*.spec.ts', '*.test.js'])
142
+
143
+ # If no framework detected from package.json, infer from file types
144
+ if cfg['framework'] == 'unknown':
145
+ if vue_count > 10:
146
+ cfg['framework'] = 'vue'
147
+ elif java_count > 10:
148
+ cfg['framework'] = 'java'
149
+ elif ts_count > 10:
150
+ cfg['framework'] = 'typescript'
151
+
152
+
153
+ def _detect_framework_from_pkgjson(cfg):
154
+ """Detect UI framework from package.json dependencies."""
155
+ try:
156
+ pkg = json.load(open(cfg['package_json'], encoding='utf-8'))
157
+ except Exception:
158
+ return
159
+ deps = set((pkg.get('dependencies') or {}).keys())
160
+ deps |= set((pkg.get('devDependencies') or {}).keys())
161
+ if not deps:
162
+ return
163
+
164
+ # Also check monorepo apps/*/package.json
165
+ apps_dir = os.path.join(os.path.dirname(cfg['package_json']), 'apps')
166
+ if os.path.isdir(apps_dir):
167
+ for app in os.listdir(apps_dir):
168
+ app_pkg = os.path.join(apps_dir, app, 'package.json')
169
+ if os.path.isfile(app_pkg):
170
+ try:
171
+ ap = json.load(open(app_pkg, encoding='utf-8'))
172
+ deps |= set((ap.get('dependencies') or {}).keys())
173
+ deps |= set((ap.get('devDependencies') or {}).keys())
174
+ except Exception:
175
+ pass
176
+
177
+ if any(d.startswith('@vben/') for d in deps):
178
+ cfg['framework'] = 'vben-admin'
179
+ elif 'vant' in deps:
180
+ cfg['framework'] = 'vant-mobile'
181
+ elif 'react' in deps and 'react-dom' in deps:
182
+ cfg['framework'] = 'react'
183
+ elif 'ant-design-vue' in deps:
184
+ cfg['framework'] = 'ant-design-vue'
185
+ elif 'element-plus' in deps:
186
+ cfg['framework'] = 'element-plus'
187
+ elif 'vue' in deps:
188
+ cfg['framework'] = 'vue'
189
+
190
+
191
+ # ── Extractors (work on any project, parameterized by ProjectConfig) ──
192
+
193
+ def extract_keywords_and_apis(cfg):
194
+ """Extract keyword→file index + API endpoints for one project."""
195
+ kw_index = {}
196
+ api_index = {}
197
+ file_count = 0
198
+
199
+ exts = ['.vue', '.js', '.ts', '.jsx', '.tsx', '.java', '.xml', '.yml', '.yaml', '.properties']
200
+ for root, dirs, files in os.walk(cfg['path']):
201
+ dirs[:] = [d for d in dirs if d not in ('node_modules', '.git', 'dist', '__pycache__', '.svn', 'target', 'build')]
202
+ for f in files:
203
+ if not any(f.endswith(e) for e in exts):
204
+ continue
205
+ filepath = os.path.join(root, f)
206
+ rel = cfg['name'] + '/' + os.path.relpath(filepath, cfg['path']).replace('\\', '/')
207
+ file_count += 1
208
+ try:
209
+ content = open(filepath, encoding='utf-8', errors='ignore').read()
210
+ except Exception:
211
+ continue
212
+
213
+ if f.endswith('.java'):
214
+ for m in re.finditer(r'(?:class|interface|enum)\s+(\w+)', content):
215
+ for kw in [m.group(1).lower()] + re.findall(r'[A-Z]?[a-z]+', m.group(1)):
216
+ kw_index.setdefault(kw.lower(), []).append(rel)
217
+ for m in re.finditer(r'@(?:Get|Post|Put|Delete|Request)Mapping\s*\(?["\']([^"\']+)["\']', content):
218
+ api_index[m.group(1)] = rel
219
+ else:
220
+ name_m = re.search(r'name:\s*["\'](\w+)["\']', content)
221
+ if name_m:
222
+ kw_index.setdefault(name_m.group(1).lower(), []).append(rel)
223
+ for pat in [r'requestClient\.\w+\s*\(?["\']([^"\']+)',
224
+ r'(?:axios|request|fetch|http)\.\w+\s*\(?["\']([^"\']+)',
225
+ r'path:\s*["\']([^"\']+)["\']']:
226
+ for m in re.finditer(pat, content):
227
+ kw_index.setdefault('api:' + m.group(1), []).append(rel)
228
+ for m in re.finditer(r'[A-Z]?[a-z]+', f.rsplit('.', 1)[0]):
229
+ kw = m.group().lower()
230
+ if len(kw) >= 3:
231
+ kw_index.setdefault(kw, []).append(rel)
232
+
233
+ return kw_index, api_index, file_count
234
+
235
+
236
+ def extract_ui_layer(cfg):
237
+ """Extract pages + fields + modules for one project (framework-aware)."""
238
+ pages = []
239
+ field_map = {}
240
+ modules = []
241
+
242
+ # Modules: auto-discover from api_dirs and module_dirs
243
+ seen_mods = set()
244
+ for d in cfg.get('module_dirs', []):
245
+ try:
246
+ for sub in sorted(os.listdir(d)):
247
+ if sub.startswith('.') or sub.startswith('_'):
248
+ continue
249
+ sub_path = os.path.join(d, sub)
250
+ if os.path.isdir(sub_path):
251
+ mod_key = sub.lower()
252
+ if mod_key not in seen_mods:
253
+ seen_mods.add(mod_key)
254
+ rel = os.path.relpath(sub_path, cfg['path']).replace('\\', '/')
255
+ modules.append({'dir': sub, 'cn': sub, 'project': cfg['name'], 'path': rel})
256
+ except Exception:
257
+ pass
258
+
259
+ # Pages: scan view_dirs
260
+ table_marks = {
261
+ 'vben-admin': ['VxeGrid', 'useVbenVxeGrid', '<a-table'],
262
+ 'ant-design-vue': ['<a-table', 'a-table'],
263
+ 'element-plus': ['<el-table', 'el-table'],
264
+ 'vant-mobile': ['<van-list', 'van-list'],
265
+ 'vue': ['<a-table', '<el-table', '<vxe-table'],
266
+ }.get(cfg.get('framework', ''), ['<table'])
267
+ table_marks_lower = [m.lower() for m in table_marks]
268
+
269
+ form_marks = {
270
+ 'vben-admin': ['useVbenForm', '<a-form'],
271
+ 'ant-design-vue': ['<a-form', 'a-form'],
272
+ 'element-plus': ['<el-form', 'el-form'],
273
+ 'vant-mobile': ['<van-form', 'van-form'],
274
+ 'vue': ['<a-form', '<el-form'],
275
+ }.get(cfg.get('framework', ''), ['<form'])
276
+
277
+ for view_dir in cfg.get('view_dirs', []):
278
+ for root, dirs, files in os.walk(view_dir):
279
+ dirs[:] = [d for d in dirs if d not in ('node_modules', 'dist', '.git')]
280
+ for f in files:
281
+ if not f.endswith('.vue') and not f.endswith('.tsx') and not f.endswith('.jsx'):
282
+ continue
283
+ filepath = os.path.join(root, f)
284
+ try:
285
+ content = open(filepath, encoding='utf-8', errors='ignore').read()
286
+ except Exception:
287
+ continue
288
+
289
+ # Page type detection
290
+ ptype = 'unknown'
291
+ if any(m.lower() in content.lower() for m in table_marks_lower):
292
+ ptype = 'table-page'
293
+ elif any(m in content for m in form_marks):
294
+ ptype = 'form-page'
295
+ elif 'detail' in filepath.lower() or 'info' in filepath.lower():
296
+ ptype = 'detail-page'
297
+ if ptype == 'unknown':
298
+ continue
299
+
300
+ # Fields (generic patterns, framework-agnostic)
301
+ for m in re.finditer(r"title:\s*['\"]([^'\"]+)['\"]\s*,\s*field:\s*['\"]([^'\"]+)['\"]", content):
302
+ field_map.setdefault(m.group(2), {'titles': set(), 'files': []})
303
+ field_map[m.group(2)]['titles'].add(m.group(1))
304
+ field_map[m.group(2)]['files'].append(filepath)
305
+ for m in re.finditer(r"fieldName:\s*['\"]([^'\"]+)['\"]\s*,\s*label:\s*['\"]([^'\"]+)['\"]", content):
306
+ field_map.setdefault(m.group(1), {'titles': set(), 'files': []})
307
+ field_map[m.group(1)]['titles'].add(m.group(2))
308
+ field_map[m.group(1)]['files'].append(filepath)
309
+
310
+ rel = filepath.replace('\\', '/')
311
+ file_key = rel.split('/views/')[-1] if '/views/' in rel else \
312
+ rel.split('/pages/')[-1] if '/pages/' in rel else os.path.basename(rel)
313
+ pages.append({'file': file_key, 'project': cfg['name'], 'page_type': ptype})
314
+
315
+ return pages, field_map, modules
316
+
317
+
318
+ def extract_traces(cfg, api_table):
319
+ """Extract button→API traces for one Vue project."""
320
+ traces = []
321
+ if not cfg.get('has_vue') or not api_table:
322
+ return traces
323
+ try:
324
+ from domain.kg.extract.ts_extract import ts_trace_clicks
325
+ except Exception:
326
+ return traces
327
+
328
+ for view_dir in cfg.get('view_dirs', []):
329
+ for root, dirs, files in os.walk(view_dir):
330
+ dirs[:] = [d for d in dirs if d not in ('node_modules', 'dist', '.git')]
331
+ for f in files:
332
+ if not f.endswith('.vue'):
333
+ continue
334
+ filepath = os.path.join(root, f)
335
+ try:
336
+ content = open(filepath, encoding='utf-8', errors='ignore').read()
337
+ except Exception:
338
+ continue
339
+ if '@click' not in content:
340
+ continue
341
+ traces.extend(ts_trace_clicks(filepath, content, api_table))
342
+ return traces
343
+
344
+
345
+ def extract_tests(cfg):
346
+ """Extract test assertions for one project."""
347
+ results = []
348
+ try:
349
+ from domain.kg.extract.test_extract import extract_test_assertions
350
+ results = extract_test_assertions(cfg['path'])
351
+ except Exception:
352
+ pass
353
+ return results
354
+
355
+
356
+ def extract_layout_fingerprint(cfg):
357
+ """Extract layout/design fingerprint from project config."""
358
+ fp = {}
359
+ layout_file = cfg.get('layout_file')
360
+ if layout_file and layout_file.endswith('preferences.ts'):
361
+ try:
362
+ from domain.kg.build.build_style_index import extract_layout_fingerprint as _elf
363
+ fp = _elf(cfg['path'])
364
+ except Exception:
365
+ pass
366
+ elif cfg.get('package_json'):
367
+ # Infer from package.json
368
+ fp['stack'] = cfg.get('framework', 'unknown')
369
+ fp['stack'] = fp.get('stack', cfg.get('framework', 'unknown'))
370
+ return fp
371
+
372
+
373
+ # ── Main build (project-agnostic) ───────────────────────────────────
374
+
375
+ def build_all():
376
+ """Full rebuild: discover projects → extract → DuckDB. Zero hardcoded names."""
377
+ t0 = time.time()
378
+ con = get_db()
379
+ init_schema(con)
380
+
381
+ # Clear all
382
+ for table in ['entities', 'aliases', 'edges', 'keywords', 'endpoints', 'prds',
383
+ 'wiki', 'pages', 'fields', 'modules', 'traces', 'api_functions',
384
+ 'tests', 'workflows', 'style_meta', 'build_meta']:
385
+ con.execute('DELETE FROM %s' % table)
386
+
387
+ # 1. Discover projects
388
+ print(' [0/7] Discovering projects...')
389
+ projects = discover_projects()
390
+ print(' Found %d projects:' % len(projects))
391
+ for p in projects:
392
+ print(' %s: %s (%s, %d api_dirs, %d view_dirs)' % (
393
+ p['name'], p['framework'], 'vue' if p['has_vue'] else 'java' if p['has_java'] else '?',
394
+ len(p['api_dirs']), len(p['view_dirs'])))
395
+
396
+ stats = {}
397
+
398
+ # 2. Keywords + APIs (all projects)
399
+ print(' [1/7] keywords + APIs...')
400
+ all_kw_rows = []
401
+ all_ep_rows = []
402
+ for cfg in projects:
403
+ kw_idx, api_idx, fc = extract_keywords_and_apis(cfg)
404
+ for k, files in kw_idx.items():
405
+ for f in files:
406
+ all_kw_rows.append((k, f))
407
+ for ep, f in api_idx.items():
408
+ all_ep_rows.append((ep, f))
409
+ _batch_insert(con, 'keywords', all_kw_rows)
410
+ _batch_insert(con, 'endpoints', all_ep_rows, ignore=True)
411
+ stats['keywords'] = len(all_kw_rows)
412
+ stats['endpoints'] = len(all_ep_rows)
413
+
414
+ # 3. UI layer (Vue/React projects only)
415
+ print(' [2/7] UI pages + fields + modules...')
416
+ all_pages = []
417
+ all_fields = []
418
+ all_modules = []
419
+ style_meta = {}
420
+ for cfg in projects:
421
+ if not cfg.get('has_vue') and cfg['framework'] not in ('react',):
422
+ continue
423
+ pages, field_map, modules = extract_ui_layer(cfg)
424
+ all_pages.extend(pages)
425
+ for k, info in field_map.items():
426
+ all_fields.append((k, '/'.join(sorted(info['titles'])[:2]), len(info['files']), '[]'))
427
+ all_modules.extend(modules)
428
+ fp = extract_layout_fingerprint(cfg)
429
+ if fp:
430
+ style_meta[cfg['name']] = fp
431
+ page_rows = [(p['file'], p['project'], p['page_type'], '[]', '[]', '[]', '{}') for p in all_pages]
432
+ _batch_insert(con, 'pages', page_rows, ignore=True)
433
+ _batch_insert(con, 'fields', all_fields)
434
+ mod_rows = [(m['dir'], m['cn'], m['project'], m['path']) for m in all_modules]
435
+ _batch_insert(con, 'modules', mod_rows)
436
+ for proj, fp in style_meta.items():
437
+ con.execute("INSERT OR REPLACE INTO style_meta VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", [
438
+ proj, fp.get('stack', ''), fp.get('layout', ''), fp.get('access_mode', ''),
439
+ fp.get('sidebar_width'), fp.get('color_primary'), fp.get('color_success'),
440
+ fp.get('color_warning'), fp.get('color_destructive'), fp.get('radius'), 0, '{}', '{}'])
441
+ stats['pages'] = len(page_rows)
442
+ stats['fields'] = len(all_fields)
443
+ stats['modules'] = len(all_modules)
444
+
445
+ # 4. API functions + traces (Vue projects with api_dirs)
446
+ print(' [3/7] API functions...')
447
+ api_table = {}
448
+ for cfg in projects:
449
+ for api_dir in cfg.get('api_dirs', []):
450
+ try:
451
+ from domain.kg.extract.ts_extract import ts_build_api_fn_table
452
+ t = ts_build_api_fn_table(api_dir)
453
+ api_table.update(t)
454
+ except Exception:
455
+ pass
456
+ fn_rows = [(fn, info.get('url', ''), info.get('verb', ''), info.get('file', ''))
457
+ for fn, info in api_table.items()]
458
+ _batch_insert(con, 'api_functions', fn_rows, ignore=True)
459
+ stats['api_functions'] = len(fn_rows)
460
+
461
+ print(' [4/7] trace chain...')
462
+ all_traces = []
463
+ for cfg in projects:
464
+ all_traces.extend(extract_traces(cfg, api_table))
465
+ trace_rows = [(t.get('vue_file', '').replace('\\', '/'), t.get('button', ''),
466
+ t.get('handler', ''), t.get('api_fn', ''), t.get('endpoint', ''),
467
+ t.get('verb', ''), t.get('confidence', ''))
468
+ for t in all_traces]
469
+ _batch_insert(con, 'traces', trace_rows)
470
+ stats['traces'] = len(trace_rows)
471
+
472
+ # 5. Tests
473
+ print(' [5/7] test assertions...')
474
+ all_tests = []
475
+ for cfg in projects:
476
+ all_tests.extend(extract_tests(cfg))
477
+ test_rows = [(t.get('class', ''), t.get('method', ''), t.get('package', ''),
478
+ t.get('display_name', ''),
479
+ json.dumps(t.get('injected_deps', []), ensure_ascii=False),
480
+ json.dumps(t.get('assertions', []), ensure_ascii=False),
481
+ t.get('is_integration', False))
482
+ for t in all_tests]
483
+ _batch_insert(con, 'tests', test_rows)
484
+ stats['tests'] = len(test_rows)
485
+
486
+ # 6. Entities + edges
487
+ print(' [6/7] entity resolution + edges...')
488
+ _build_entities_and_edges(con, all_modules, dict(all_ep_rows and [(r[0], r[1]) for r in all_ep_rows] or []),
489
+ all_traces, all_tests)
490
+ stats['entities'] = con.execute("SELECT COUNT(*) FROM entities").fetchone()[0]
491
+ stats['edges'] = con.execute("SELECT COUNT(*) FROM edges").fetchone()[0]
492
+
493
+ # 7. Workflows
494
+ print(' [7/8] workflows...')
495
+ wf_count = _build_workflows(con, all_traces)
496
+ stats['workflows'] = wf_count
497
+
498
+ # 8. Import PRD + Wiki (from git_sync's JSON output)
499
+ print(' [8/8] PRD + Wiki...')
500
+ try:
501
+ prd_count, wiki_count = _import_prds_and_wiki(con)
502
+ stats['prds'] = prd_count
503
+ stats['wiki'] = wiki_count
504
+ except Exception as e:
505
+ print(' PRD/Wiki SKIP: %s' % str(e)[:60])
506
+ stats['prds'] = 0
507
+ stats['wiki'] = 0
508
+
509
+ con.execute("INSERT OR REPLACE INTO build_meta VALUES ('version', '2.0')")
510
+ con.execute("INSERT OR REPLACE INTO build_meta VALUES ('built_at', ?)", [str(int(time.time()))])
511
+ con.close()
512
+
513
+ stats['elapsed'] = '%.1fs' % (time.time() - t0)
514
+ return stats
515
+
516
+
517
+ def _build_entities_and_edges(con, modules, api_idx, traces, tests):
518
+ """Build entities + edges (project-agnostic)."""
519
+ entity_rows = []
520
+ alias_rows = []
521
+ edge_rows = []
522
+
523
+ # FEATURE entities
524
+ feature_dirs = {}
525
+ for m in modules:
526
+ eid = 'F:%s:%s' % (m['project'], m['dir'])
527
+ entity_rows.append((eid, 'FEATURE', m['dir'], m['cn'], '{}'))
528
+ alias_rows.append((m['dir'].lower(), eid))
529
+ alias_rows.append((m['cn'].lower(), eid))
530
+ feature_dirs[m['dir'].lower()] = eid
531
+
532
+ # ENDPOINT + CONTROLLER entities + edges
533
+ seen_ctrl = set()
534
+ for ep, ctrl_file in api_idx.items():
535
+ eid = 'E:%s' % ep
536
+ entity_rows.append((eid, 'ENDPOINT', ep, '', '{}'))
537
+ ctrl_name = ctrl_file.split('/')[-1].replace('.java', '')
538
+ ctrl_eid = 'C:%s' % ctrl_name
539
+ if ctrl_name not in seen_ctrl:
540
+ entity_rows.append((ctrl_eid, 'CONTROLLER', ctrl_name, '', '{}'))
541
+ alias_rows.append((ctrl_name.lower(), ctrl_eid))
542
+ seen_ctrl.add(ctrl_name)
543
+ edge_rows.append((eid, ctrl_eid, 'handled_by', 0, '{}'))
544
+
545
+ # BUTTON entities + click edges
546
+ for t in traces:
547
+ vue = (t.get('vue_file') or '').replace('\\', '/')
548
+ handler = t.get('handler', '')
549
+ vue_short = vue.split('/views/')[-1] if '/views/' in vue else vue.split('/pages/')[-1] if '/pages/' in vue else vue
550
+ eid = 'B:%s:%s' % (vue_short, handler)
551
+ # cn: 用按钮中文文本 (清洗 icon 字体 &#x...; 和噪音), 提升语义检索召回
552
+ btn_cn = re.sub(r'&#x[0-9a-fA-F]+;', '', t.get('button', '')).strip(' >\t\r\n')
553
+ entity_rows.append((eid, 'BUTTON', handler, btn_cn,
554
+ json.dumps({'button_text': t.get('button', ''),
555
+ 'confidence': t.get('confidence', '')}, ensure_ascii=False)))
556
+ # 别名: 按钮中文名 → entity (让 multi_hop 传中文名能找到按钮)
557
+ if btn_cn:
558
+ alias_rows.append((btn_cn.lower(), eid))
559
+ ep = t.get('endpoint')
560
+ if ep and ep != 'None':
561
+ edge_rows.append((eid, 'E:%s' % ep, 'clicks', 0,
562
+ json.dumps({'button': t.get('button', ''),
563
+ 'confidence': t.get('confidence', '')}, ensure_ascii=False)))
564
+
565
+ # TEST entities + controller→test edges
566
+ for t in tests:
567
+ name = '%s.%s' % (t.get('class', ''), t.get('method', ''))
568
+ eid = 'T:%s' % name
569
+ entity_rows.append((eid, 'TEST', name, '',
570
+ json.dumps({'deps': t.get('injected_deps', []),
571
+ 'assertions': t.get('assertions', [])}, ensure_ascii=False)))
572
+ for dep in t.get('injected_deps', []):
573
+ dep_base = dep.replace('I', '', 1).replace('Impl', '').replace('Service', '')
574
+ for ctrl_name in seen_ctrl:
575
+ if dep_base.lower() in ctrl_name.lower():
576
+ edge_rows.append(('C:%s' % ctrl_name, eid, 'tested_by', 0, '{}'))
577
+
578
+ # FEATURE endpoints/buttons edges
579
+ for m in modules:
580
+ feat_eid = 'F:%s:%s' % (m['project'], m['dir'])
581
+ mdir = m['dir'].lower()
582
+ for ep in api_idx:
583
+ if mdir in ep.lower():
584
+ edge_rows.append((feat_eid, 'E:%s' % ep, 'has_endpoint', 0, '{}'))
585
+ for t in traces:
586
+ vue = (t.get('vue_file') or '').lower()
587
+ if mdir in vue:
588
+ vue_short = vue.split('/views/')[-1] if '/views/' in vue else vue
589
+ btn_eid = 'B:%s:%s' % (vue_short, t.get('handler', ''))
590
+ edge_rows.append((feat_eid, btn_eid, 'has_button', 0, '{}'))
591
+
592
+ _batch_insert(con, 'entities', entity_rows, ignore=True)
593
+ _batch_insert(con, 'aliases', alias_rows, ignore=True)
594
+ _batch_insert(con, 'edges', edge_rows)
595
+
596
+
597
+ def _build_workflows(con, traces):
598
+ """Build workflow chains (project-agnostic)."""
599
+ STATE_KW = {
600
+ 'create': ['add', 'create', 'save', 'insert'],
601
+ 'submit': ['submit', 'commit', 'apply'],
602
+ 'execute': ['execute', 'start', 'begin'],
603
+ 'complete': ['complete', 'finish', 'done', 'end'],
604
+ 'archive': ['archive', 'close', 'settle'],
605
+ 'delete': ['delete', 'remove', 'destroy'],
606
+ 'export': ['export', 'download'],
607
+ 'query': ['list', 'get', 'query', 'page', 'detail'],
608
+ }
609
+ from collections import defaultdict
610
+ mod_ops = defaultdict(list)
611
+ for t in traces:
612
+ vue = (t.get('vue_file') or '').replace('\\', '/')
613
+ mod = vue.split('/views/')[-1].split('/')[0] if '/views/' in vue else \
614
+ vue.split('/pages/')[-1].split('/')[0] if '/pages/' in vue else 'unknown'
615
+ text = ((t.get('button') or '') + ' ' + (t.get('endpoint') or '')).lower()
616
+ for state, kws in STATE_KW.items():
617
+ if any(k in text for k in kws):
618
+ mod_ops[mod].append((state, t.get('button', ''), t.get('endpoint', '')))
619
+ break
620
+
621
+ chain_order = ['query', 'create', 'submit', 'execute', 'complete', 'archive', 'export', 'delete']
622
+ wf_rows = []
623
+ for mod, ops in mod_ops.items():
624
+ for state in chain_order:
625
+ matching = [o for o in ops if o[0] == state]
626
+ for op in matching[:3]:
627
+ wf_rows.append((mod, state, chain_order.index(state), op[1], op[2]))
628
+ _batch_insert(con, 'workflows', wf_rows)
629
+ return len(wf_rows)
630
+
631
+
632
+ def _import_prds_and_wiki(con):
633
+ """Import PRD index + Wiki from git_sync's JSON output into DuckDB.
634
+
635
+ Reads _build_cache/prd-index.json and data/index/wiki-index.json.
636
+ Returns (prd_count, wiki_count).
637
+ """
638
+ INDEX = str(DATA_INDEX_DIR)
639
+ prd_count = 0
640
+ wiki_count = 0
641
+
642
+ def _load_json(name):
643
+ for d in [os.path.join(INDEX, '_build_cache'), INDEX]:
644
+ p = os.path.join(d, name)
645
+ if os.path.isfile(p):
646
+ return json.load(open(p, encoding='utf-8'))
647
+ return None
648
+
649
+ # PRD
650
+ prds = _load_json('prd-index.json')
651
+ if prds:
652
+ con.execute("DELETE FROM prds")
653
+ rows = [(f, info.get('title', ''),
654
+ json.dumps(info.get('keywords', []), ensure_ascii=False),
655
+ json.dumps(info.get('features', []), ensure_ascii=False),
656
+ json.dumps(info.get('cn_terms', []), ensure_ascii=False))
657
+ for f, info in prds.items()]
658
+ if rows:
659
+ _batch_insert(con, 'prds', rows, ignore=True)
660
+ prd_count = len(rows)
661
+
662
+ # Wiki
663
+ wiki = _load_json('wiki-index.json')
664
+ if wiki and isinstance(wiki, dict):
665
+ con.execute("DELETE FROM wiki")
666
+ rows = []
667
+ for kw, entries in (wiki.get('index', {}) or {}).items():
668
+ for e in (entries if isinstance(entries, list) else []):
669
+ rows.append((kw, e.get('project', ''), e.get('title', ''),
670
+ e.get('md_path', ''), e.get('description', '')))
671
+ if rows:
672
+ _batch_insert(con, 'wiki', rows)
673
+ wiki_count = len(rows)
674
+
675
+ return prd_count, wiki_count
676
+
677
+
678
+ if __name__ == '__main__':
679
+ print('=== kg_build.py: auto-discover → DuckDB (no JSON, no hardcode) ===')
680
+ stats = build_all()
681
+ print()
682
+ print('Done in %s' % stats.pop('elapsed', '?'))
683
+ for k, v in sorted(stats.items()):
684
+ print(' %-16s: %s' % (k, v))
685
+ print(' file: %s (%.1f KB)' % (DB_PATH, os.path.getsize(DB_PATH) / 1024))