@hupan56/wlkj 2.7.12 → 3.0.0

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