@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.
- package/bin/cli.js +261 -11
- 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-reference.md +8 -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 -311
- 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 +51 -15
- package/templates/qoder/hooks/inject-workflow-state.py +2 -2
- package/templates/qoder/hooks/session-start.py +82 -51
- 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/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/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 -231
- 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 +292 -40
- 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 -641
- 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 +2 -2
- 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 +184 -60
- 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/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 -57
- package/templates/qoder/agents/prd-research.md +0 -34
- package/templates/qoder/commands/wl-insight.md +0 -51
- package/templates/qoder/scripts/__pycache__/init_doctor.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/setup.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/common/__pycache__/developer.cpython-39.pyc +0 -0
- package/templates/qoder/skills/wl-prd/SKILL.md +0 -92
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
In-memory graph traversal engine for the QODER knowledge graph.
|
|
4
|
+
|
|
5
|
+
Loads the existing JSON indexes into an adjacency structure and supports
|
|
6
|
+
multi-hop BFS queries that no single index can answer alone:
|
|
7
|
+
|
|
8
|
+
- impact(endpoint) → "改这个接口影响哪些页面/按钮/模块?"
|
|
9
|
+
- context360(symbol) → "这个符号的360度视图: 谁调用它/它调用谁/属于哪"
|
|
10
|
+
|
|
11
|
+
This is the "上层工程 U1" piece: turning write-only data (trace-chain,
|
|
12
|
+
code-api, prd-index) into live queryable knowledge. No graph database needed
|
|
13
|
+
— a few thousand entities traverse in milliseconds in pure Python dicts.
|
|
14
|
+
|
|
15
|
+
Design constraints (from the deep-thinking rounds):
|
|
16
|
+
- relations loaded on-demand, never slowing existing index loads
|
|
17
|
+
- degrades gracefully when indexes are missing (new project / partial build)
|
|
18
|
+
- output is AI-readable compact text, not raw JSON
|
|
19
|
+
"""
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
from collections import defaultdict
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _load(path):
|
|
27
|
+
"""Load JSON if exists, else return None."""
|
|
28
|
+
if path and os.path.isfile(path):
|
|
29
|
+
try:
|
|
30
|
+
return json.load(open(path, encoding='utf-8'))
|
|
31
|
+
except Exception:
|
|
32
|
+
return None
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CodeGraph:
|
|
37
|
+
"""In-memory graph built from the existing JSON indexes.
|
|
38
|
+
|
|
39
|
+
Indexes consumed:
|
|
40
|
+
trace-chain.json → edges: button @click → API endpoint
|
|
41
|
+
code-api.json → endpoint → Java controller file
|
|
42
|
+
ui-modules.json → module names (Chinese labels)
|
|
43
|
+
prd-index.json → PRD features + related code
|
|
44
|
+
ui-style.json → page → module (via vue_file path)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, index_dir):
|
|
48
|
+
self.index_dir = index_dir
|
|
49
|
+
self._loaded = False
|
|
50
|
+
self._duckdb_con = None
|
|
51
|
+
# Adjacency / lookup structures (built lazily on first query)
|
|
52
|
+
self.endpoint_to_traces = {} # {endpoint: [trace, ...]}
|
|
53
|
+
self.endpoint_to_controller = {} # {endpoint: controller_file}
|
|
54
|
+
self.vue_to_module = {} # {vue_path: module_cn}
|
|
55
|
+
self.prd_features = {} # {feature_kw: prd_info}
|
|
56
|
+
self.tests_by_package = {} # {package_prefix: [test, ...]} (E1)
|
|
57
|
+
self.tests_by_dep = {} # {dep_class: [test, ...]} (E1)
|
|
58
|
+
# Try DuckDB backend (preferred over JSON)
|
|
59
|
+
self._try_duckdb()
|
|
60
|
+
|
|
61
|
+
def _try_duckdb(self):
|
|
62
|
+
"""Try to connect to kg.duckdb as preferred backend."""
|
|
63
|
+
db_path = os.path.join(self.index_dir, 'kg.duckdb')
|
|
64
|
+
if not os.path.isfile(db_path):
|
|
65
|
+
return
|
|
66
|
+
try:
|
|
67
|
+
import duckdb
|
|
68
|
+
con = duckdb.connect(db_path, read_only=True)
|
|
69
|
+
tables = con.execute(
|
|
70
|
+
"SELECT name FROM sqlite_master WHERE type='table'"
|
|
71
|
+
).fetchall()
|
|
72
|
+
if 'entities' in {t[0] for t in tables}:
|
|
73
|
+
self._duckdb_con = con
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
def _ensure_loaded(self):
|
|
78
|
+
if self._loaded:
|
|
79
|
+
return
|
|
80
|
+
self._loaded = True
|
|
81
|
+
|
|
82
|
+
# 1. trace-chain → reverse map (endpoint → who calls it)
|
|
83
|
+
tc = _load(os.path.join(self.index_dir, 'trace-chain.json'))
|
|
84
|
+
if tc:
|
|
85
|
+
for t in tc.get('traces', []):
|
|
86
|
+
ep = t.get('endpoint')
|
|
87
|
+
if ep and ep != 'None':
|
|
88
|
+
self.endpoint_to_traces.setdefault(ep, []).append(t)
|
|
89
|
+
|
|
90
|
+
# 2. code-api → endpoint → controller
|
|
91
|
+
api = _load(os.path.join(self.index_dir, 'code-api.json'))
|
|
92
|
+
if api:
|
|
93
|
+
self.endpoint_to_controller = dict(api)
|
|
94
|
+
|
|
95
|
+
# 3. ui-modules → module dir → cn name (for page→module resolution)
|
|
96
|
+
mi = _load(os.path.join(self.index_dir, 'ui-modules.json'))
|
|
97
|
+
self._module_dirs = {}
|
|
98
|
+
if mi:
|
|
99
|
+
for proj, mods in mi.get('projects', {}).items():
|
|
100
|
+
for m in mods:
|
|
101
|
+
self._module_dirs[m['dir'].lower()] = m['cn']
|
|
102
|
+
|
|
103
|
+
# 4. prd-index → feature keywords
|
|
104
|
+
pi = _load(os.path.join(self.index_dir, 'prd-index.json'))
|
|
105
|
+
if pi:
|
|
106
|
+
for prd_file, info in pi.items():
|
|
107
|
+
for feat in (info.get('features') or []):
|
|
108
|
+
self.prd_features[feat.lower()] = {
|
|
109
|
+
'prd': prd_file, 'title': info.get('title', ''),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# 5. test-assertions → index by injected dependency + package (E1 layer)
|
|
113
|
+
ta = _load(os.path.join(self.index_dir, 'test-assertions.json'))
|
|
114
|
+
if ta:
|
|
115
|
+
for t in ta.get('tests', []):
|
|
116
|
+
# by dependency class (for JOIN to production code)
|
|
117
|
+
for dep in t.get('injected_deps', []):
|
|
118
|
+
self.tests_by_dep.setdefault(dep, []).append(t)
|
|
119
|
+
# by package (for module-level JOIN)
|
|
120
|
+
pkg = t.get('package', '')
|
|
121
|
+
if pkg:
|
|
122
|
+
# group by last 2 package segments (e.g. asset.asset)
|
|
123
|
+
parts = pkg.split('.')
|
|
124
|
+
key = '.'.join(parts[-2:]) if len(parts) >= 2 else pkg
|
|
125
|
+
self.tests_by_package.setdefault(key, []).append(t)
|
|
126
|
+
|
|
127
|
+
# 6. relations.json → unified edge table (E3: connects all 6 layers)
|
|
128
|
+
self.relations = _load(os.path.join(self.index_dir, 'relations.json')) or {}
|
|
129
|
+
self.rel_edges = self.relations.get('edges', {}) if self.relations else {}
|
|
130
|
+
# Pre-compute controller → tests lookup from relations
|
|
131
|
+
self.controller_to_tests = self.rel_edges.get('controller_to_tests', {})
|
|
132
|
+
|
|
133
|
+
def _resolve_module(self, vue_file):
|
|
134
|
+
"""Infer the Chinese module name from a Vue file path."""
|
|
135
|
+
parts = vue_file.replace('\\', '/').lower().split('/views/')
|
|
136
|
+
if len(parts) < 2:
|
|
137
|
+
return None
|
|
138
|
+
after_views = parts[1].split('/')[0] # e.g. "veh", "hr", "maintenance"
|
|
139
|
+
return self._module_dirs.get(after_views, after_views)
|
|
140
|
+
|
|
141
|
+
def _match_endpoint(self, query):
|
|
142
|
+
"""Find all known endpoints that match the query (exact or prefix)."""
|
|
143
|
+
self._ensure_loaded()
|
|
144
|
+
# Exact match
|
|
145
|
+
if query in self.endpoint_to_traces:
|
|
146
|
+
return [query]
|
|
147
|
+
if query in self.endpoint_to_controller:
|
|
148
|
+
return [query]
|
|
149
|
+
# Prefix / substring match
|
|
150
|
+
matches = set()
|
|
151
|
+
for ep in self.endpoint_to_traces:
|
|
152
|
+
if query in ep or ep in query:
|
|
153
|
+
matches.add(ep)
|
|
154
|
+
for ep in self.endpoint_to_controller:
|
|
155
|
+
if query in ep or ep in query:
|
|
156
|
+
matches.add(ep)
|
|
157
|
+
return sorted(matches)
|
|
158
|
+
|
|
159
|
+
def impact(self, query, depth=2):
|
|
160
|
+
"""'改 query 影响谁?' — BFS from endpoint outward.
|
|
161
|
+
|
|
162
|
+
Uses DuckDB (preferred) or JSON fallback.
|
|
163
|
+
"""
|
|
164
|
+
# DuckDB fast path
|
|
165
|
+
if self._duckdb_con:
|
|
166
|
+
return self._impact_duckdb(query)
|
|
167
|
+
|
|
168
|
+
self._ensure_loaded()
|
|
169
|
+
endpoints = self._match_endpoint(query)
|
|
170
|
+
if not endpoints:
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
lines = []
|
|
174
|
+
lines.append('## 影响分析: {}'.format(query))
|
|
175
|
+
lines.append('匹配到 {} 个端点:'.format(len(endpoints)))
|
|
176
|
+
|
|
177
|
+
all_pages = set()
|
|
178
|
+
all_modules = set()
|
|
179
|
+
all_buttons = []
|
|
180
|
+
|
|
181
|
+
for ep in endpoints:
|
|
182
|
+
lines.append('')
|
|
183
|
+
lines.append('### {}'.format(ep))
|
|
184
|
+
|
|
185
|
+
# Controller
|
|
186
|
+
ctrl = self.endpoint_to_controller.get(ep)
|
|
187
|
+
if ctrl:
|
|
188
|
+
ctrl_name = ctrl.split('/')[-1]
|
|
189
|
+
lines.append(' 后端: {}'.format(ctrl_name))
|
|
190
|
+
|
|
191
|
+
# Traces (who calls this endpoint)
|
|
192
|
+
traces = self.endpoint_to_traces.get(ep, [])
|
|
193
|
+
if traces:
|
|
194
|
+
lines.append(' 前端调用 ({}处):'.format(len(traces)))
|
|
195
|
+
for t in traces[:8]:
|
|
196
|
+
vue = t.get('vue_file', '').split('src/views/')[-1] if 'src/views/' in t.get('vue_file', '') else t.get('vue_file', '')
|
|
197
|
+
btn = t.get('button', '?')
|
|
198
|
+
handler = t.get('handler', '?')
|
|
199
|
+
conf = t.get('confidence', '?')
|
|
200
|
+
lines.append(' [{}] {} → {} ({})'.format(btn, vue, handler, conf))
|
|
201
|
+
all_pages.add(vue)
|
|
202
|
+
all_buttons.append(btn)
|
|
203
|
+
mod = self._resolve_module(t.get('vue_file', ''))
|
|
204
|
+
if mod:
|
|
205
|
+
all_modules.add(mod)
|
|
206
|
+
else:
|
|
207
|
+
lines.append(' 前端调用: 无 trace 记录 (可能未被前端直接调用, 或 trace 未覆盖)')
|
|
208
|
+
|
|
209
|
+
# Summary
|
|
210
|
+
lines.append('')
|
|
211
|
+
lines.append('### 影响摘要')
|
|
212
|
+
lines.append(' 受影响页面: {} 个'.format(len(all_pages)))
|
|
213
|
+
lines.append(' 受影响模块: {}'.format(', '.join(sorted(all_modules)) if all_modules else '未识别'))
|
|
214
|
+
lines.append(' 涉及操作: {}'.format(', '.join(sorted(set(all_buttons))) if all_buttons else '无'))
|
|
215
|
+
|
|
216
|
+
# Existing test coverage (E3: controller→tests edge from relations.json)
|
|
217
|
+
affected_controllers = set()
|
|
218
|
+
for ep in endpoints:
|
|
219
|
+
ctrl = self.endpoint_to_controller.get(ep, '')
|
|
220
|
+
if ctrl:
|
|
221
|
+
affected_controllers.add(ctrl.split('/')[-1].replace('.java', ''))
|
|
222
|
+
|
|
223
|
+
relevant_tests = []
|
|
224
|
+
# E3 precise path: controller → tests (from relations.json)
|
|
225
|
+
for ctrl in affected_controllers:
|
|
226
|
+
for t in self.controller_to_tests.get(ctrl, []):
|
|
227
|
+
relevant_tests.append(t)
|
|
228
|
+
# Fallback: module/package matching if relations didn't connect
|
|
229
|
+
if not relevant_tests:
|
|
230
|
+
for mod in all_modules:
|
|
231
|
+
for pkg_key, tests in self.tests_by_package.items():
|
|
232
|
+
if mod and mod in pkg_key.lower():
|
|
233
|
+
relevant_tests.extend(tests)
|
|
234
|
+
if relevant_tests:
|
|
235
|
+
seen = set()
|
|
236
|
+
unique_tests = []
|
|
237
|
+
for t in relevant_tests:
|
|
238
|
+
key = t.get('test', '') or t.get('method', '') or str(t)
|
|
239
|
+
if key not in seen:
|
|
240
|
+
seen.add(key)
|
|
241
|
+
unique_tests.append(t)
|
|
242
|
+
lines.append('')
|
|
243
|
+
lines.append('### 已有测试覆盖 ({}个)'.format(len(unique_tests)))
|
|
244
|
+
for t in unique_tests[:5]:
|
|
245
|
+
# Handle both relations format ({test, dep, assertions}) and
|
|
246
|
+
# test-assertions format ({class, method, injected_deps, assertions})
|
|
247
|
+
name = t.get('test', '') or '%s.%s' % (t.get('class', ''), t.get('method', ''))
|
|
248
|
+
deps = t.get('dep', '') or ', '.join(t.get('injected_deps', [])[:3])
|
|
249
|
+
asserts = ', '.join(t.get('assertions', [])[:3])
|
|
250
|
+
lines.append(' {} — 依赖[{}] 断言[{}]'.format(name, deps, asserts))
|
|
251
|
+
lines.append(' → 改完后可跑这些测试做回归验证')
|
|
252
|
+
else:
|
|
253
|
+
lines.append('')
|
|
254
|
+
lines.append('### 已有测试覆盖: 无 (该模块暂无自动化测试, 改动需手动验证)')
|
|
255
|
+
|
|
256
|
+
lines.append('')
|
|
257
|
+
lines.append(' ⚠️ 开发建议: 改完上述接口后, 重点回归以上页面的按钮功能')
|
|
258
|
+
|
|
259
|
+
return '\n'.join(lines)
|
|
260
|
+
|
|
261
|
+
def context360(self, query):
|
|
262
|
+
"""'query 的360度视图' — 聚合所有索引中关于它的信息。
|
|
263
|
+
|
|
264
|
+
Works for: endpoint, api function name, or Vue file path fragment.
|
|
265
|
+
"""
|
|
266
|
+
self._ensure_loaded()
|
|
267
|
+
lines = []
|
|
268
|
+
lines.append('## 360° 视图: {}'.format(query))
|
|
269
|
+
|
|
270
|
+
# Endpoint matches
|
|
271
|
+
endpoints = self._match_endpoint(query)
|
|
272
|
+
if endpoints:
|
|
273
|
+
lines.append('')
|
|
274
|
+
lines.append('### 端点 ({}个)'.format(len(endpoints)))
|
|
275
|
+
for ep in endpoints[:5]:
|
|
276
|
+
ctrl = self.endpoint_to_controller.get(ep, '?')
|
|
277
|
+
traces = self.endpoint_to_traces.get(ep, [])
|
|
278
|
+
lines.append(' {} → {} (前端{}处调用)'.format(
|
|
279
|
+
ep, ctrl.split('/')[-1] if ctrl != '?' else '?', len(traces)))
|
|
280
|
+
|
|
281
|
+
# API function matches (from trace-chain api_functions)
|
|
282
|
+
tc = _load(os.path.join(self.index_dir, 'trace-chain.json'))
|
|
283
|
+
if tc:
|
|
284
|
+
api_fns = tc.get('api_functions', {})
|
|
285
|
+
fn_matches = {k: v for k, v in api_fns.items() if query.lower() in k.lower()}
|
|
286
|
+
if fn_matches:
|
|
287
|
+
lines.append('')
|
|
288
|
+
lines.append('### API 函数 ({}个)'.format(len(fn_matches)))
|
|
289
|
+
for fn, info in list(fn_matches.items())[:5]:
|
|
290
|
+
lines.append(' {} → {} {}'.format(fn, info.get('url', '?'), info.get('verb', '?')))
|
|
291
|
+
|
|
292
|
+
# Trace matches (by handler name)
|
|
293
|
+
if tc:
|
|
294
|
+
handler_traces = [t for t in tc.get('traces', [])
|
|
295
|
+
if query.lower() in t.get('handler', '').lower()]
|
|
296
|
+
if handler_traces:
|
|
297
|
+
lines.append('')
|
|
298
|
+
lines.append('### 前端处理器 ({}处)'.format(len(handler_traces)))
|
|
299
|
+
for t in handler_traces[:5]:
|
|
300
|
+
vue = t.get('vue_file', '').split('src/views/')[-1] if 'src/views/' in t.get('vue_file', '') else '?'
|
|
301
|
+
lines.append(' {} @ {} → {} ({})'.format(
|
|
302
|
+
t.get('handler'), vue, t.get('endpoint', '?'), t.get('confidence', '?')))
|
|
303
|
+
|
|
304
|
+
# PRD references
|
|
305
|
+
prd_hits = {k: v for k, v in self.prd_features.items() if query.lower() in k}
|
|
306
|
+
if prd_hits:
|
|
307
|
+
lines.append('')
|
|
308
|
+
lines.append('### 关联PRD ({}个)'.format(len(prd_hits)))
|
|
309
|
+
for feat, info in list(prd_hits.items())[:3]:
|
|
310
|
+
lines.append(' {} — {} ({})'.format(feat, info['title'], info['prd']))
|
|
311
|
+
|
|
312
|
+
if len(lines) <= 2:
|
|
313
|
+
return None
|
|
314
|
+
return '\n'.join(lines)
|
|
315
|
+
|
|
316
|
+
def coverage_matrix(self):
|
|
317
|
+
"""功能×测试覆盖矩阵 — 哪些功能有测试, 哪些是空白?
|
|
318
|
+
|
|
319
|
+
Uses DuckDB SQL (preferred) or entity-registry JSON (fallback).
|
|
320
|
+
Returns AI-readable text showing each feature's coverage status.
|
|
321
|
+
"""
|
|
322
|
+
# DuckDB fast path: one SQL query
|
|
323
|
+
if self._duckdb_con:
|
|
324
|
+
return self._coverage_matrix_duckdb()
|
|
325
|
+
self._ensure_loaded()
|
|
326
|
+
er = _load(os.path.join(self.index_dir, 'entity-registry.json'))
|
|
327
|
+
if not er:
|
|
328
|
+
return 'entity-registry.json 不存在, 请先运行 build_entity_registry.py'
|
|
329
|
+
|
|
330
|
+
features = er.get('feature_adjacency', {})
|
|
331
|
+
entities = er.get('entities', {})
|
|
332
|
+
|
|
333
|
+
lines = ['## 功能×测试覆盖矩阵', '']
|
|
334
|
+
lines.append('| 功能 | 端点 | 按钮操作 | 测试 | 覆盖状态 |')
|
|
335
|
+
lines.append('|------|------|---------|------|---------|')
|
|
336
|
+
|
|
337
|
+
# Sort by endpoint count descending
|
|
338
|
+
sorted_feats = sorted(features.items(),
|
|
339
|
+
key=lambda x: -len(x[1].get('endpoints', [])))
|
|
340
|
+
|
|
341
|
+
for feat_id, adj in sorted_feats:
|
|
342
|
+
cn = entities.get(feat_id, {}).get('cn', feat_id)
|
|
343
|
+
ep_count = len(adj.get('endpoints', []))
|
|
344
|
+
btn_count = len(adj.get('buttons', []))
|
|
345
|
+
test_count = len(adj.get('tests', []))
|
|
346
|
+
|
|
347
|
+
if ep_count == 0 and btn_count == 0:
|
|
348
|
+
status = '⚠️ 无端点无按钮(可能是纯展示页)'
|
|
349
|
+
elif test_count == 0:
|
|
350
|
+
status = '🔴 无测试覆盖'
|
|
351
|
+
elif test_count < 3:
|
|
352
|
+
status = '🟡 测试不足(%d)' % test_count
|
|
353
|
+
else:
|
|
354
|
+
status = '🟢 有测试(%d)' % test_count
|
|
355
|
+
|
|
356
|
+
lines.append('| %s | %d | %d | %d | %s |' % (
|
|
357
|
+
cn, ep_count, btn_count, test_count, status))
|
|
358
|
+
|
|
359
|
+
# Summary
|
|
360
|
+
total_feats = len(sorted_feats)
|
|
361
|
+
no_test = sum(1 for adj in features.values()
|
|
362
|
+
if len(adj.get('tests', [])) == 0
|
|
363
|
+
and len(adj.get('endpoints', [])) > 0)
|
|
364
|
+
lines.append('')
|
|
365
|
+
lines.append('**总计**: %d 功能, %d 有端点但无测试 (🔴 空白)' % (total_feats, no_test))
|
|
366
|
+
lines.append('')
|
|
367
|
+
lines.append('⚠️ 无测试的功能是回归测试的盲区, 优先补测试')
|
|
368
|
+
|
|
369
|
+
return '\n'.join(lines)
|
|
370
|
+
|
|
371
|
+
def feature_overview(self, query=''):
|
|
372
|
+
"""功能概览: 一个功能的完整画像 (端点+按钮+测试+页面)."""
|
|
373
|
+
self._ensure_loaded()
|
|
374
|
+
er = _load(os.path.join(self.index_dir, 'entity-registry.json'))
|
|
375
|
+
if not er:
|
|
376
|
+
return 'entity-registry.json 不存在'
|
|
377
|
+
|
|
378
|
+
entities = er.get('entities', {})
|
|
379
|
+
features = er.get('feature_adjacency', {})
|
|
380
|
+
|
|
381
|
+
# Resolve query to feature entity ID (via alias resolution)
|
|
382
|
+
q_lower = query.lower()
|
|
383
|
+
target_feat = None
|
|
384
|
+
for fid, ent in entities.items():
|
|
385
|
+
if ent['type'] != 'FEATURE':
|
|
386
|
+
continue
|
|
387
|
+
if q_lower in [a.lower() for a in ent['aliases']]:
|
|
388
|
+
target_feat = fid
|
|
389
|
+
break
|
|
390
|
+
|
|
391
|
+
if not target_feat:
|
|
392
|
+
# Fuzzy match
|
|
393
|
+
for fid, ent in entities.items():
|
|
394
|
+
if ent['type'] != 'FEATURE':
|
|
395
|
+
continue
|
|
396
|
+
if q_lower in ent.get('canonical', '').lower() or q_lower in ent.get('cn', '').lower():
|
|
397
|
+
target_feat = fid
|
|
398
|
+
break
|
|
399
|
+
|
|
400
|
+
if not target_feat:
|
|
401
|
+
return '未找到功能 "%s"。可用功能: %s' % (
|
|
402
|
+
query, ', '.join(ent['cn'] for ent in entities.values()
|
|
403
|
+
if ent['type'] == 'FEATURE')[:200])
|
|
404
|
+
|
|
405
|
+
ent = entities[target_feat]
|
|
406
|
+
adj = features.get(target_feat, {})
|
|
407
|
+
lines = ['## 功能画像: %s (%s)' % (ent['cn'], ent['canonical'])]
|
|
408
|
+
lines.append(' 别名: %s' % ', '.join(ent['aliases'][:8]))
|
|
409
|
+
lines.append('')
|
|
410
|
+
lines.append('### 端点 (%d)' % len(adj.get('endpoints', [])))
|
|
411
|
+
for eid in adj.get('endpoints', [])[:8]:
|
|
412
|
+
ep = entities.get(eid, {})
|
|
413
|
+
lines.append(' %s → %s' % (ep.get('canonical', eid),
|
|
414
|
+
ep.get('properties', {}).get('controller', '?')))
|
|
415
|
+
lines.append('')
|
|
416
|
+
lines.append('### 按钮操作 (%d)' % len(adj.get('buttons', [])))
|
|
417
|
+
btn_texts = set()
|
|
418
|
+
for eid in adj.get('buttons', [])[:20]:
|
|
419
|
+
btn = entities.get(eid, {})
|
|
420
|
+
txt = btn.get('properties', {}).get('button_text', '')
|
|
421
|
+
if txt:
|
|
422
|
+
btn_texts.add(txt)
|
|
423
|
+
lines.append(' 操作: %s' % ', '.join(sorted(btn_texts)[:15]))
|
|
424
|
+
lines.append('')
|
|
425
|
+
lines.append('### 测试 (%d)' % len(adj.get('tests', [])))
|
|
426
|
+
for eid in adj.get('tests', [])[:5]:
|
|
427
|
+
test = entities.get(eid, {})
|
|
428
|
+
lines.append(' %s — 断言[%s]' % (
|
|
429
|
+
test.get('canonical', eid),
|
|
430
|
+
', '.join(test.get('properties', {}).get('assertions', [])[:3])))
|
|
431
|
+
|
|
432
|
+
return '\n'.join(lines)
|
|
433
|
+
|
|
434
|
+
# ── DuckDB backend methods (preferred when kg.duckdb exists) ──
|
|
435
|
+
|
|
436
|
+
def _coverage_matrix_duckdb(self):
|
|
437
|
+
"""Coverage matrix via single SQL query on DuckDB."""
|
|
438
|
+
con = self._duckdb_con
|
|
439
|
+
rows = con.execute("""
|
|
440
|
+
SELECT e.cn,
|
|
441
|
+
COUNT(DISTINCT CASE WHEN ed.edge_type='has_endpoint' THEN ed.to_id END) as eps,
|
|
442
|
+
COUNT(DISTINCT CASE WHEN ed.edge_type='has_button' THEN ed.to_id END) as btns,
|
|
443
|
+
COUNT(DISTINCT CASE WHEN ed.edge_type='has_test' THEN ed.to_id END) as tests
|
|
444
|
+
FROM entities e
|
|
445
|
+
LEFT JOIN edges ed ON ed.from_id = e.id
|
|
446
|
+
WHERE e.type = 'FEATURE'
|
|
447
|
+
GROUP BY e.id, e.cn
|
|
448
|
+
HAVING COUNT(DISTINCT CASE WHEN ed.edge_type='has_endpoint' THEN ed.to_id END) > 0
|
|
449
|
+
OR COUNT(DISTINCT CASE WHEN ed.edge_type='has_button' THEN ed.to_id END) > 0
|
|
450
|
+
ORDER BY eps DESC
|
|
451
|
+
""").fetchall()
|
|
452
|
+
|
|
453
|
+
lines = ['## 功能x测试覆盖矩阵', '']
|
|
454
|
+
lines.append('| 功能 | 端点 | 按钮 | 测试 | 状态 |')
|
|
455
|
+
lines.append('|------|------|------|------|------|')
|
|
456
|
+
no_test = 0
|
|
457
|
+
for cn, eps, btns, tests in rows:
|
|
458
|
+
status = '无测试' if tests == 0 else '有%d' % tests
|
|
459
|
+
if tests == 0:
|
|
460
|
+
no_test += 1
|
|
461
|
+
lines.append('| %s | %d | %d | %d | %s |' % (cn, eps, btns, tests, status))
|
|
462
|
+
lines.append('')
|
|
463
|
+
lines.append('总计: %d 功能, %d 无测试' % (len(rows), no_test))
|
|
464
|
+
return '\n'.join(lines)
|
|
465
|
+
|
|
466
|
+
def _impact_duckdb(self, query):
|
|
467
|
+
"""Impact analysis via DuckDB: endpoint -> buttons + controller + tests."""
|
|
468
|
+
con = self._duckdb_con
|
|
469
|
+
rows = con.execute("""
|
|
470
|
+
SELECT id FROM entities
|
|
471
|
+
WHERE type = 'ENDPOINT'
|
|
472
|
+
AND (LOWER(canonical) LIKE ? OR LOWER(id) LIKE ?)
|
|
473
|
+
LIMIT 30
|
|
474
|
+
""", ['%' + query.lower() + '%', '%' + query.lower() + '%']).fetchall()
|
|
475
|
+
if not rows:
|
|
476
|
+
return None
|
|
477
|
+
|
|
478
|
+
endpoint_ids = [r[0] for r in rows]
|
|
479
|
+
lines = ['## 影响分析: %s (%d端点)' % (query, len(endpoint_ids))]
|
|
480
|
+
all_pages = set()
|
|
481
|
+
all_buttons = []
|
|
482
|
+
|
|
483
|
+
for eid in endpoint_ids:
|
|
484
|
+
ep = eid.replace('E:', '')
|
|
485
|
+
lines.append('')
|
|
486
|
+
lines.append('### %s' % ep)
|
|
487
|
+
ctrl = con.execute(
|
|
488
|
+
"SELECT t.canonical FROM edges e JOIN entities t ON e.to_id=t.id WHERE e.from_id=? AND e.edge_type='handled_by'", [eid]
|
|
489
|
+
).fetchone()
|
|
490
|
+
if ctrl:
|
|
491
|
+
lines.append(' 后端: %s' % ctrl[0])
|
|
492
|
+
btns = con.execute(
|
|
493
|
+
"SELECT b.canonical, b.props, b.id FROM edges e JOIN entities b ON e.from_id=b.id WHERE e.to_id=? AND e.edge_type='clicks'", [eid]
|
|
494
|
+
).fetchall()
|
|
495
|
+
if btns:
|
|
496
|
+
lines.append(' 前端调用(%d):' % len(btns))
|
|
497
|
+
for bc, bp, bid in btns[:6]:
|
|
498
|
+
import json as _j
|
|
499
|
+
p = _j.loads(bp) if bp else {}
|
|
500
|
+
vue = bid.split('B:')[1].split(':')[0] if 'B:' in bid else '?'
|
|
501
|
+
lines.append(' [%s] %s -> %s (%s)' % (p.get('button','?'), vue, bc, p.get('confidence','?')))
|
|
502
|
+
all_pages.add(vue)
|
|
503
|
+
all_buttons.append(p.get('button', ''))
|
|
504
|
+
else:
|
|
505
|
+
lines.append(' 前端调用: 无trace')
|
|
506
|
+
|
|
507
|
+
# Test coverage: endpoint -> controller -> tests (2-hop)
|
|
508
|
+
placeholders = ','.join(['?'] * len(endpoint_ids))
|
|
509
|
+
tests = con.execute("""
|
|
510
|
+
SELECT DISTINCT t.id FROM edges e1
|
|
511
|
+
JOIN edges e2 ON e2.from_id = e1.to_id
|
|
512
|
+
JOIN entities t ON e2.to_id = t.id
|
|
513
|
+
WHERE e1.from_id IN (%s) AND e1.edge_type='handled_by'
|
|
514
|
+
AND e2.edge_type='tested_by' AND t.type='TEST'
|
|
515
|
+
LIMIT 10
|
|
516
|
+
""" % placeholders, endpoint_ids).fetchall()
|
|
517
|
+
|
|
518
|
+
lines.append('')
|
|
519
|
+
lines.append('### 影响摘要')
|
|
520
|
+
lines.append(' 受影响页面: %d' % len(all_pages))
|
|
521
|
+
lines.append(' 涉及操作: %s' % ', '.join(sorted(set(b for b in all_buttons if b))[:15]))
|
|
522
|
+
if tests:
|
|
523
|
+
lines.append('')
|
|
524
|
+
lines.append('### 已有测试(%d)' % len(tests))
|
|
525
|
+
for tr in tests[:5]:
|
|
526
|
+
lines.append(' %s' % tr[0].replace('T:', ''))
|
|
527
|
+
lines.append(' -> 改完可跑这些测试做回归')
|
|
528
|
+
else:
|
|
529
|
+
lines.append('')
|
|
530
|
+
lines.append('### 已有测试: 无')
|
|
531
|
+
lines.append('')
|
|
532
|
+
lines.append(' 改完上述接口后重点回归以上页面')
|
|
533
|
+
return '\n'.join(lines)
|
|
@@ -78,6 +78,95 @@ FILE_DEVELOPER = ".developer"
|
|
|
78
78
|
FILE_JOURNAL_PREFIX = "journal-"
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
# =============================================================================
|
|
82
|
+
# Index file registry — SINGLE SOURCE OF TRUTH for data/index/ filenames
|
|
83
|
+
# =============================================================================
|
|
84
|
+
# Why this exists: 17 index files used to be flat-named in data/index/ with
|
|
85
|
+
# 4 different naming styles, and two (module-map vs module-index) differed by
|
|
86
|
+
# one word but meant totally different things. Every consumer hardcoded the
|
|
87
|
+
# filename as a string literal. Now all names live here — rename = change once.
|
|
88
|
+
#
|
|
89
|
+
# Naming convention: <category>-<subject>.json
|
|
90
|
+
# code- source code layer (search, APIs, stats, dependency graph)
|
|
91
|
+
# ui- UI/style layer (page patterns, module names, design fingerprint)
|
|
92
|
+
# trace- call-chain (button click → API → controller)
|
|
93
|
+
# prd- product requirements
|
|
94
|
+
# wiki- semantic module docs
|
|
95
|
+
# ref- hand-curated static reference (NOT auto-built)
|
|
96
|
+
# Dotfiles (.meta, .cache) are machine-local bookkeeping.
|
|
97
|
+
|
|
98
|
+
DATA_INDEX_DIR = PROJECT_ROOT / "data" / "index"
|
|
99
|
+
BUILD_CACHE_DIR = DATA_INDEX_DIR / "_build_cache" # JSON 中间产物 (构建器写→kg_duckdb读→DuckDB)
|
|
100
|
+
KG_DB_PATH = DATA_INDEX_DIR / "kg.duckdb" # 知识图谱单文件存储 (消费层唯一查询入口)
|
|
101
|
+
|
|
102
|
+
INDEX_FILES = {
|
|
103
|
+
# ── code layer (built by git_sync.py) ──
|
|
104
|
+
'code_keyword': 'code-keyword.json', # was code-keyword.json
|
|
105
|
+
'code_api': 'code-api.json', # was code-api.json
|
|
106
|
+
'code_stats': 'code-stats.json', # was code-stats.json (PROJECT FILE COUNTS)
|
|
107
|
+
'code_callgraph': 'code-code-callgraph.json', # was code-callgraph.json
|
|
108
|
+
# ── UI layer (built by build_style_index.py) ──
|
|
109
|
+
'ui_style': 'ui-style.json', # was ui-style.json
|
|
110
|
+
'ui_style_meta': 'ui-ui-style-meta.json', # was ui-style-meta.json
|
|
111
|
+
'ui_modules': 'ui-modules.json', # was ui-modules.json (BUSINESS MODULE NAMES)
|
|
112
|
+
# ── call-chain (built by build_style_index.py) ──
|
|
113
|
+
'trace_chain': 'trace-chain.json', # was trace-chain.json
|
|
114
|
+
# ── 浏览器测试锚点 (built/updated by autotest.py learn) ──
|
|
115
|
+
'test_pages': 'test-pages.json', # 页面稳定锚点(role/name/placeholder), 团队共享
|
|
116
|
+
# ── PRD layer (built by git_sync.py) ──
|
|
117
|
+
'prd_index': 'prd-index.json', # (name unchanged, already clear)
|
|
118
|
+
'prd_code_map': 'prd-code-map.json', # (name unchanged)
|
|
119
|
+
# ── semantic ──
|
|
120
|
+
'wiki_index': 'wiki-index.json', # (name unchanged)
|
|
121
|
+
# ── static reference (hand-curated, committed to git) ──
|
|
122
|
+
'ref_vben': 'ref-vben-style.json', # was ref-vben-style.json
|
|
123
|
+
'ref_chart': 'ref-chart-style.json', # was ref-chart-style.json
|
|
124
|
+
'ref_icon': 'ref-icon.json', # was ref-icon.json
|
|
125
|
+
# ── meta / cache (dotfiles, machine-local) ──
|
|
126
|
+
'meta_index': '.index-meta.json', # (name unchanged)
|
|
127
|
+
'meta_prd_collected': '.prd-collected.json', # (name unchanged)
|
|
128
|
+
'meta_last_sync': '.last-sync', # (name unchanged)
|
|
129
|
+
'cache_inverted': '.inverted-cache.json', # (name unchanged)
|
|
130
|
+
'cache_file_keys': '.file-keys.json', # (name unchanged)
|
|
131
|
+
'meta_req_counter': 'req-counter.json', # (name unchanged)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# Legacy → new name mapping (for migration + backward-compat lookups)
|
|
135
|
+
INDEX_NAME_MIGRATION = {
|
|
136
|
+
'code-keyword.json': 'code-keyword.json',
|
|
137
|
+
'code-api.json': 'code-api.json',
|
|
138
|
+
'code-stats.json': 'code-stats.json',
|
|
139
|
+
'code-callgraph.json': 'code-code-callgraph.json',
|
|
140
|
+
'ui-style.json': 'ui-style.json',
|
|
141
|
+
'ui-style-meta.json': 'ui-ui-style-meta.json',
|
|
142
|
+
'ui-modules.json': 'ui-modules.json',
|
|
143
|
+
'trace-chain.json': 'trace-chain.json',
|
|
144
|
+
'ref-vben-style.json': 'ref-vben-style.json',
|
|
145
|
+
'ref-chart-style.json': 'ref-chart-style.json',
|
|
146
|
+
'ref-icon.json': 'ref-icon.json',
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def index_path(key: str) -> Path:
|
|
151
|
+
"""Get the full path for an index file by its registry key.
|
|
152
|
+
|
|
153
|
+
Usage: index_path('code_keyword') → data/index/code-keyword.json
|
|
154
|
+
"""
|
|
155
|
+
name = INDEX_FILES.get(key)
|
|
156
|
+
if name is None:
|
|
157
|
+
raise KeyError('Unknown index key: %r (valid: %s)' % (key, sorted(INDEX_FILES)))
|
|
158
|
+
return DATA_INDEX_DIR / name
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def resolve_index_name(filename: str) -> str:
|
|
162
|
+
"""Resolve a (possibly legacy) index filename to its current name.
|
|
163
|
+
|
|
164
|
+
Falls back to the input unchanged if not in the migration map.
|
|
165
|
+
Used by load helpers for backward compatibility.
|
|
166
|
+
"""
|
|
167
|
+
return INDEX_NAME_MIGRATION.get(filename, filename)
|
|
168
|
+
|
|
169
|
+
|
|
81
170
|
# =============================================================================
|
|
82
171
|
# Core functions
|
|
83
172
|
# =============================================================================
|