@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,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
  # =============================================================================