@hupan56/wlkj 2.5.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.
- package/bin/cli.js +289 -12
- package/package.json +1 -1
- package/templates/qoder/agents/insight-planning.md +67 -0
- package/templates/qoder/agents/insight-research.md +61 -0
- package/templates/qoder/agents/prd-quick.md +1 -0
- package/templates/qoder/agents/prd-reference.md +10 -2
- package/templates/qoder/commands/optional/wl-insight.md +275 -0
- package/templates/qoder/commands/{wl-report.md → optional/wl-report.md} +13 -5
- package/templates/qoder/commands/{wl-spec.md → optional/wl-spec.md} +1 -1
- package/templates/qoder/commands/{wl-status.md → optional/wl-status.md} +28 -2
- package/templates/qoder/commands/wl-code.md +10 -2
- package/templates/qoder/commands/wl-commit.md +1 -1
- package/templates/qoder/commands/wl-design-draw.md +78 -0
- package/templates/qoder/commands/wl-design-scan.md +108 -0
- package/templates/qoder/commands/wl-design-spec.md +154 -0
- package/templates/qoder/commands/wl-design.md +32 -0
- package/templates/qoder/commands/wl-init.md +24 -3
- package/templates/qoder/commands/wl-prd-full.md +226 -0
- package/templates/qoder/commands/wl-prd-quick.md +134 -0
- package/templates/qoder/commands/wl-prd-review.md +104 -0
- package/templates/qoder/commands/wl-prd.md +17 -288
- package/templates/qoder/commands/wl-search.md +66 -30
- package/templates/qoder/commands/wl-task.md +290 -59
- package/templates/qoder/commands/wl-test.md +92 -24
- package/templates/qoder/config.yaml +59 -15
- package/templates/qoder/hooks/inject-workflow-state.py +35 -9
- package/templates/qoder/hooks/session-start.py +144 -62
- package/templates/qoder/rules/wl-pipeline.md +216 -105
- package/templates/qoder/scripts/__pycache__/search_index.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/archive_prd.py +377 -0
- package/templates/qoder/scripts/autotest.py +1715 -0
- package/templates/qoder/scripts/autotest_batch.py +224 -0
- package/templates/qoder/scripts/autotest_run.py +297 -0
- package/templates/qoder/scripts/benchmark.py +210 -209
- package/templates/qoder/scripts/build_style_index.py +444 -4
- package/templates/qoder/scripts/check_carriers.py +238 -0
- package/templates/qoder/scripts/check_mcp.py +298 -0
- package/templates/qoder/scripts/check_qoderwork_consistency.py +166 -0
- package/templates/qoder/scripts/common/developer.py +26 -19
- package/templates/qoder/scripts/common/events.py +46 -0
- package/templates/qoder/scripts/common/extract.py +419 -0
- package/templates/qoder/scripts/common/graph_traverse.py +533 -0
- package/templates/qoder/scripts/common/identity.py +6 -1
- package/templates/qoder/scripts/common/paths.py +89 -0
- package/templates/qoder/scripts/common/pip_install.py +144 -0
- package/templates/qoder/scripts/common/platform_guard.py +61 -0
- package/templates/qoder/scripts/common/search_engine.py +205 -205
- package/templates/qoder/scripts/common/terms.py +57 -0
- package/templates/qoder/scripts/common/ts_extract.py +536 -0
- package/templates/qoder/scripts/context_pack.py +73 -13
- package/templates/qoder/scripts/enrich_prompt.py +226 -0
- package/templates/qoder/scripts/eval_prd.py +318 -225
- package/templates/qoder/scripts/export.py +487 -487
- package/templates/qoder/scripts/extract_api_params.py +246 -0
- package/templates/qoder/scripts/extract_routes.py +54 -0
- package/templates/qoder/scripts/extract_routes_tree.py +78 -0
- package/templates/qoder/scripts/fill_prototype.py +707 -0
- package/templates/qoder/scripts/gen_design_doc.py +394 -0
- package/templates/qoder/scripts/git_sync.py +27 -15
- package/templates/qoder/scripts/init_doctor.py +306 -41
- package/templates/qoder/scripts/install_qoderwork.py +366 -9
- package/templates/qoder/scripts/kg.py +708 -0
- package/templates/qoder/scripts/kg_auto_login.py +196 -0
- package/templates/qoder/scripts/kg_build.py +612 -0
- package/templates/qoder/scripts/kg_build_db.py +327 -0
- package/templates/qoder/scripts/kg_duckdb.py +549 -0
- package/templates/qoder/scripts/kg_incremental.py +393 -0
- package/templates/qoder/scripts/kg_link_db.py +224 -0
- package/templates/qoder/scripts/kg_mcp_server.py +801 -0
- package/templates/qoder/scripts/kg_semantic.py +150 -0
- package/templates/qoder/scripts/kg_test_runner.py +241 -0
- package/templates/qoder/scripts/lanhu_stdio_wrapper.py +119 -0
- package/templates/qoder/scripts/learn.py +118 -39
- package/templates/qoder/scripts/learn_aggregate.py +201 -0
- package/templates/qoder/scripts/mcp_launcher.py +359 -0
- package/templates/qoder/scripts/mysql_mcp_server.py +396 -0
- package/templates/qoder/scripts/repo_root.py +106 -0
- package/templates/qoder/scripts/role.py +12 -0
- package/templates/qoder/scripts/run_weekly_update.bat +5 -0
- package/templates/qoder/scripts/run_weekly_update.sh +5 -0
- package/templates/qoder/scripts/search_index.py +307 -60
- package/templates/qoder/scripts/secure-ls.js +5640 -0
- package/templates/qoder/scripts/setup.py +706 -453
- package/templates/qoder/scripts/setup_lanhu.py +963 -0
- package/templates/qoder/scripts/status.py +250 -11
- package/templates/qoder/scripts/sync_carriers.py +259 -0
- package/templates/qoder/scripts/syncgate.py +5 -4
- package/templates/qoder/scripts/task.py +75 -0
- package/templates/qoder/scripts/team_sync.py +60 -4
- package/templates/qoder/scripts/workspace_init.py +1 -1
- package/templates/qoder/skills/design-import/SKILL.md +226 -0
- package/templates/qoder/skills/design-import/figma-workflow.md +81 -0
- package/templates/qoder/skills/design-review/SKILL.md +82 -25
- package/templates/qoder/skills/prd-generator/SKILL.md +185 -58
- package/templates/qoder/skills/prd-review/SKILL.md +18 -1
- package/templates/qoder/skills/prompt-enrich/SKILL.md +90 -0
- package/templates/qoder/skills/prototype-generator/SKILL.md +256 -141
- package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
- package/templates/qoder/skills/spec-coder/SKILL.md +18 -1
- package/templates/qoder/skills/spec-generator/SKILL.md +18 -1
- package/templates/qoder/skills/test-generator/SKILL.md +15 -2
- package/templates/qoder/skills/wl-code/SKILL.md +55 -36
- package/templates/qoder/skills/wl-commit/SKILL.md +89 -76
- package/templates/qoder/skills/wl-design/SKILL.md +55 -0
- package/templates/qoder/skills/wl-init/SKILL.md +76 -67
- package/templates/qoder/skills/wl-insight/SKILL.md +201 -81
- package/templates/qoder/skills/wl-prd-full/SKILL.md +69 -0
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +49 -0
- package/templates/qoder/skills/wl-prd-review/SKILL.md +34 -0
- package/templates/qoder/skills/wl-report/SKILL.md +131 -107
- package/templates/qoder/skills/wl-search/SKILL.md +141 -75
- package/templates/qoder/skills/wl-spec/SKILL.md +49 -39
- package/templates/qoder/skills/wl-status/SKILL.md +83 -61
- package/templates/qoder/skills/wl-task/SKILL.md +132 -58
- package/templates/qoder/skills/wl-test/SKILL.md +406 -40
- package/templates/qoder/templates/prd-full-template.md +2 -0
- package/templates/qoder/templates/prd-quick-template.md +1 -0
- package/templates/qoder/templates/prototype-app.html +13 -8
- package/templates/qoder/templates/prototype-web.html +376 -93
- package/templates/root/AGENTS.md +89 -34
- package/templates/root/requirements.txt +21 -0
- package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +259 -259
- package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +186 -186
- package/templates/qoder/agents/prd-planning.md +0 -56
- package/templates/qoder/agents/prd-research.md +0 -33
- package/templates/qoder/commands/wl-insight.md +0 -51
- package/templates/qoder/skills/wl-prd/SKILL.md +0 -89
|
@@ -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))
|