@hupan56/wlkj 2.6.0 → 2.7.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 (125) hide show
  1. package/bin/cli.js +261 -11
  2. package/package.json +1 -1
  3. package/templates/qoder/agents/insight-planning.md +67 -0
  4. package/templates/qoder/agents/insight-research.md +61 -0
  5. package/templates/qoder/agents/prd-reference.md +8 -2
  6. package/templates/qoder/commands/optional/wl-insight.md +275 -0
  7. package/templates/qoder/commands/{wl-report.md → optional/wl-report.md} +13 -5
  8. package/templates/qoder/commands/{wl-spec.md → optional/wl-spec.md} +1 -1
  9. package/templates/qoder/commands/{wl-status.md → optional/wl-status.md} +28 -2
  10. package/templates/qoder/commands/wl-code.md +10 -2
  11. package/templates/qoder/commands/wl-commit.md +1 -1
  12. package/templates/qoder/commands/wl-design-draw.md +78 -0
  13. package/templates/qoder/commands/wl-design-scan.md +108 -0
  14. package/templates/qoder/commands/wl-design-spec.md +154 -0
  15. package/templates/qoder/commands/wl-design.md +32 -0
  16. package/templates/qoder/commands/wl-init.md +24 -3
  17. package/templates/qoder/commands/wl-prd-full.md +226 -0
  18. package/templates/qoder/commands/wl-prd-quick.md +134 -0
  19. package/templates/qoder/commands/wl-prd-review.md +104 -0
  20. package/templates/qoder/commands/wl-prd.md +17 -311
  21. package/templates/qoder/commands/wl-search.md +66 -30
  22. package/templates/qoder/commands/wl-task.md +290 -59
  23. package/templates/qoder/commands/wl-test.md +92 -24
  24. package/templates/qoder/config.yaml +51 -15
  25. package/templates/qoder/hooks/inject-workflow-state.py +2 -2
  26. package/templates/qoder/hooks/session-start.py +82 -51
  27. package/templates/qoder/rules/wl-pipeline.md +216 -105
  28. package/templates/qoder/scripts/__pycache__/search_index.cpython-39.pyc +0 -0
  29. package/templates/qoder/scripts/archive_prd.py +377 -0
  30. package/templates/qoder/scripts/autotest.py +1715 -0
  31. package/templates/qoder/scripts/autotest_batch.py +224 -0
  32. package/templates/qoder/scripts/autotest_run.py +297 -0
  33. package/templates/qoder/scripts/benchmark.py +210 -209
  34. package/templates/qoder/scripts/build_style_index.py +444 -4
  35. package/templates/qoder/scripts/check_carriers.py +238 -0
  36. package/templates/qoder/scripts/check_mcp.py +298 -0
  37. package/templates/qoder/scripts/check_qoderwork_consistency.py +166 -0
  38. package/templates/qoder/scripts/common/events.py +46 -0
  39. package/templates/qoder/scripts/common/extract.py +419 -0
  40. package/templates/qoder/scripts/common/graph_traverse.py +533 -0
  41. package/templates/qoder/scripts/common/paths.py +89 -0
  42. package/templates/qoder/scripts/common/pip_install.py +144 -0
  43. package/templates/qoder/scripts/common/platform_guard.py +61 -0
  44. package/templates/qoder/scripts/common/search_engine.py +205 -205
  45. package/templates/qoder/scripts/common/terms.py +57 -0
  46. package/templates/qoder/scripts/common/ts_extract.py +536 -0
  47. package/templates/qoder/scripts/context_pack.py +73 -13
  48. package/templates/qoder/scripts/enrich_prompt.py +226 -0
  49. package/templates/qoder/scripts/eval_prd.py +318 -231
  50. package/templates/qoder/scripts/export.py +487 -487
  51. package/templates/qoder/scripts/extract_api_params.py +246 -0
  52. package/templates/qoder/scripts/extract_routes.py +54 -0
  53. package/templates/qoder/scripts/extract_routes_tree.py +78 -0
  54. package/templates/qoder/scripts/fill_prototype.py +707 -0
  55. package/templates/qoder/scripts/gen_design_doc.py +394 -0
  56. package/templates/qoder/scripts/git_sync.py +27 -15
  57. package/templates/qoder/scripts/init_doctor.py +292 -40
  58. package/templates/qoder/scripts/install_qoderwork.py +366 -9
  59. package/templates/qoder/scripts/kg.py +708 -0
  60. package/templates/qoder/scripts/kg_auto_login.py +196 -0
  61. package/templates/qoder/scripts/kg_build.py +612 -0
  62. package/templates/qoder/scripts/kg_build_db.py +327 -0
  63. package/templates/qoder/scripts/kg_duckdb.py +549 -0
  64. package/templates/qoder/scripts/kg_incremental.py +393 -0
  65. package/templates/qoder/scripts/kg_link_db.py +224 -0
  66. package/templates/qoder/scripts/kg_mcp_server.py +801 -0
  67. package/templates/qoder/scripts/kg_semantic.py +150 -0
  68. package/templates/qoder/scripts/kg_test_runner.py +241 -0
  69. package/templates/qoder/scripts/lanhu_stdio_wrapper.py +119 -0
  70. package/templates/qoder/scripts/learn.py +118 -39
  71. package/templates/qoder/scripts/learn_aggregate.py +201 -0
  72. package/templates/qoder/scripts/mcp_launcher.py +359 -0
  73. package/templates/qoder/scripts/mysql_mcp_server.py +396 -0
  74. package/templates/qoder/scripts/repo_root.py +106 -0
  75. package/templates/qoder/scripts/role.py +12 -0
  76. package/templates/qoder/scripts/run_weekly_update.bat +5 -0
  77. package/templates/qoder/scripts/run_weekly_update.sh +5 -0
  78. package/templates/qoder/scripts/search_index.py +307 -60
  79. package/templates/qoder/scripts/secure-ls.js +5640 -0
  80. package/templates/qoder/scripts/setup.py +706 -641
  81. package/templates/qoder/scripts/setup_lanhu.py +963 -0
  82. package/templates/qoder/scripts/status.py +250 -11
  83. package/templates/qoder/scripts/sync_carriers.py +259 -0
  84. package/templates/qoder/scripts/syncgate.py +2 -2
  85. package/templates/qoder/scripts/task.py +75 -0
  86. package/templates/qoder/scripts/team_sync.py +60 -4
  87. package/templates/qoder/scripts/workspace_init.py +1 -1
  88. package/templates/qoder/skills/design-import/SKILL.md +226 -0
  89. package/templates/qoder/skills/design-import/figma-workflow.md +81 -0
  90. package/templates/qoder/skills/design-review/SKILL.md +82 -25
  91. package/templates/qoder/skills/prd-generator/SKILL.md +184 -60
  92. package/templates/qoder/skills/prd-review/SKILL.md +18 -1
  93. package/templates/qoder/skills/prompt-enrich/SKILL.md +90 -0
  94. package/templates/qoder/skills/prototype-generator/SKILL.md +256 -141
  95. package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
  96. package/templates/qoder/skills/spec-coder/SKILL.md +18 -1
  97. package/templates/qoder/skills/spec-generator/SKILL.md +18 -1
  98. package/templates/qoder/skills/test-generator/SKILL.md +15 -2
  99. package/templates/qoder/skills/wl-code/SKILL.md +55 -36
  100. package/templates/qoder/skills/wl-commit/SKILL.md +89 -76
  101. package/templates/qoder/skills/wl-design/SKILL.md +55 -0
  102. package/templates/qoder/skills/wl-init/SKILL.md +76 -67
  103. package/templates/qoder/skills/wl-insight/SKILL.md +201 -81
  104. package/templates/qoder/skills/wl-prd-full/SKILL.md +69 -0
  105. package/templates/qoder/skills/wl-prd-quick/SKILL.md +49 -0
  106. package/templates/qoder/skills/wl-prd-review/SKILL.md +34 -0
  107. package/templates/qoder/skills/wl-report/SKILL.md +131 -107
  108. package/templates/qoder/skills/wl-search/SKILL.md +141 -75
  109. package/templates/qoder/skills/wl-spec/SKILL.md +49 -39
  110. package/templates/qoder/skills/wl-status/SKILL.md +83 -61
  111. package/templates/qoder/skills/wl-task/SKILL.md +132 -58
  112. package/templates/qoder/skills/wl-test/SKILL.md +406 -40
  113. package/templates/qoder/templates/prototype-app.html +13 -8
  114. package/templates/qoder/templates/prototype-web.html +376 -93
  115. package/templates/root/AGENTS.md +89 -34
  116. package/templates/root/requirements.txt +21 -0
  117. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +259 -259
  118. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +186 -186
  119. package/templates/qoder/agents/prd-planning.md +0 -57
  120. package/templates/qoder/agents/prd-research.md +0 -34
  121. package/templates/qoder/commands/wl-insight.md +0 -51
  122. package/templates/qoder/scripts/__pycache__/init_doctor.cpython-39.pyc +0 -0
  123. package/templates/qoder/scripts/__pycache__/setup.cpython-39.pyc +0 -0
  124. package/templates/qoder/scripts/common/__pycache__/developer.cpython-39.pyc +0 -0
  125. package/templates/qoder/skills/wl-prd/SKILL.md +0 -92
@@ -0,0 +1,612 @@
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))