@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
|
@@ -1,231 +1,318 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""
|
|
4
|
-
QODER EVA - PRD/原型 自动评估 (快且准的"准"侧自动指标)
|
|
5
|
-
|
|
6
|
-
Usage:
|
|
7
|
-
python eval_prd.py <prd.md路径> [原型.html路径]
|
|
8
|
-
|
|
9
|
-
自动评分项 (满分 100):
|
|
10
|
-
A1 现实锚定 (40分): PRD 中引用的字段/API 在知识图谱中真实存在的比例
|
|
11
|
-
-> 防止 AI 编造不存在的字段和接口
|
|
12
|
-
A2 风格保真 (30分): 原型中的颜色值来自真源(模板/风格参考)的比例
|
|
13
|
-
-> 防止 AI 发明项目里不存在的颜色
|
|
14
|
-
A3 模板完整 (30分): PRD 包含团队模板必需章节的比例
|
|
15
|
-
-> 保证团队章程的模板结构不丢
|
|
16
|
-
|
|
17
|
-
结果追加到 .qoder/learning/eval-history.jsonl (本机), 供 /wl-status 趋势展示。
|
|
18
|
-
Exit: 0 = 总分>=80, 1 = 不达标 (发布前应修复)
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
import os
|
|
22
|
-
import
|
|
23
|
-
import
|
|
24
|
-
import
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
'
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
# ============================================================
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
for
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if
|
|
231
|
-
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
QODER EVA - PRD/原型 自动评估 (快且准的"准"侧自动指标)
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
python eval_prd.py <prd.md路径> [原型.html路径]
|
|
8
|
+
|
|
9
|
+
自动评分项 (满分 100):
|
|
10
|
+
A1 现实锚定 (40分): PRD 中引用的字段/API 在知识图谱中真实存在的比例
|
|
11
|
+
-> 防止 AI 编造不存在的字段和接口
|
|
12
|
+
A2 风格保真 (30分): 原型中的颜色值来自真源(模板/风格参考)的比例
|
|
13
|
+
-> 防止 AI 发明项目里不存在的颜色
|
|
14
|
+
A3 模板完整 (30分): PRD 包含团队模板必需章节的比例
|
|
15
|
+
-> 保证团队章程的模板结构不丢
|
|
16
|
+
|
|
17
|
+
结果追加到 .qoder/learning/eval-history.jsonl (本机), 供 /wl-status 趋势展示。
|
|
18
|
+
Exit: 0 = 总分>=80, 1 = 不达标 (发布前应修复)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
from common.paths import BUILD_CACHE_DIR
|
|
23
|
+
import sys
|
|
24
|
+
import json
|
|
25
|
+
import re
|
|
26
|
+
from datetime import datetime
|
|
27
|
+
|
|
28
|
+
if sys.platform == 'win32':
|
|
29
|
+
try:
|
|
30
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
31
|
+
except (AttributeError, IOError):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
35
|
+
|
|
36
|
+
BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
37
|
+
INDEX_DIR = os.path.join(BASE, 'data', 'index')
|
|
38
|
+
TPL_DIR = os.path.join(BASE, '.qoder', 'templates')
|
|
39
|
+
HISTORY = os.path.join(BASE, '.qoder', 'learning', 'eval-history.jsonl')
|
|
40
|
+
|
|
41
|
+
# 完整模板必需章节 (prd-full-template.md) / Mini 模板必需章节
|
|
42
|
+
FULL_SECTIONS = ['版本信息', '变更日志', '名词解释', '需求背景', '需求来源', '产品现状',
|
|
43
|
+
'需求范围', '功能清单', '字段规格', '操作与校验', '非功能性需求',
|
|
44
|
+
'埋点', '版本计划']
|
|
45
|
+
QUICK_SECTIONS = ['功能入口', '需求背景', '需求说明', '影响范围', '验收标准', '不在本次范围']
|
|
46
|
+
|
|
47
|
+
# 中性色白名单 (不算"发明颜色")
|
|
48
|
+
NEUTRAL_COLORS = {'#fff', '#ffffff', '#000', '#000000', '#333', '#666', '#999',
|
|
49
|
+
'#1f1f1f', '#f0f0f0', '#fafafa', '#d9d9d9', '#e8e8e8', '#f5f5f5'}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def load_json(path):
|
|
53
|
+
"""加载索引数据。优先 DuckDB(kg.duckdb, 数据真正所在), 回退 JSON 文件。
|
|
54
|
+
|
|
55
|
+
修复背景: A1 现实锚定读 ui-style.json 的 field_map, 但该 JSON 在 BUILD_CACHE
|
|
56
|
+
里缺失 → field_map=0 → 所有 PRD 字段都被误判"编造" → A1 必然低分。
|
|
57
|
+
数据其实在 DuckDB 里(_duckdb_load 能读出 field_map), 必须走 DuckDB。
|
|
58
|
+
"""
|
|
59
|
+
# ① 优先 DuckDB (从文件名取数据)
|
|
60
|
+
try:
|
|
61
|
+
from search_index import _duckdb_load
|
|
62
|
+
result = _duckdb_load(os.path.basename(path))
|
|
63
|
+
if result is not None:
|
|
64
|
+
return result
|
|
65
|
+
except Exception:
|
|
66
|
+
pass
|
|
67
|
+
# ② 回退文件读取
|
|
68
|
+
try:
|
|
69
|
+
with open(path, encoding='utf-8') as f:
|
|
70
|
+
return json.load(f)
|
|
71
|
+
except Exception:
|
|
72
|
+
return {}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def read_text(path):
|
|
76
|
+
with open(path, encoding='utf-8', errors='ignore') as f:
|
|
77
|
+
return f.read()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ============================================================
|
|
81
|
+
# 数据库真实列名 (第二层锚定: 代码field_map + 数据库schema)
|
|
82
|
+
# 从 kg.duckdb 的 db_columns 表读 (由 kg_build_db.py 导入)。
|
|
83
|
+
# 没有/连不上 = 空集, 自动降级为纯代码层锚定 (不影响评分)。
|
|
84
|
+
# ============================================================
|
|
85
|
+
|
|
86
|
+
_DB_COLUMNS_CACHE = None # {column_name_lower} 集合
|
|
87
|
+
|
|
88
|
+
def _load_db_columns():
|
|
89
|
+
"""惰性加载数据库真实列名集合 (从 kg_db.duckdb 的 db_columns 表)。
|
|
90
|
+
失败/不存在 = 空集, 自动降级为纯代码层锚定 (不影响评分)。"""
|
|
91
|
+
global _DB_COLUMNS_CACHE
|
|
92
|
+
if _DB_COLUMNS_CACHE is not None:
|
|
93
|
+
return _DB_COLUMNS_CACHE
|
|
94
|
+
cols = set()
|
|
95
|
+
try:
|
|
96
|
+
import duckdb
|
|
97
|
+
db_path = os.path.join(BASE, 'data', 'index', 'kg_db.duckdb')
|
|
98
|
+
if not os.path.isfile(db_path):
|
|
99
|
+
_DB_COLUMNS_CACHE = cols
|
|
100
|
+
return cols # 文件不存在 = 还没跑过 kg_build_db, 降级纯代码层
|
|
101
|
+
con = duckdb.connect(db_path, read_only=True)
|
|
102
|
+
try:
|
|
103
|
+
rows = con.execute("SELECT DISTINCT column_name FROM db_columns").fetchall()
|
|
104
|
+
cols = {r[0].lower() for r in rows if r[0]}
|
|
105
|
+
except Exception:
|
|
106
|
+
pass # 表不存在 → 空集, 降级纯代码层
|
|
107
|
+
con.close()
|
|
108
|
+
except Exception:
|
|
109
|
+
pass # duckdb 连不上 → 空集
|
|
110
|
+
_DB_COLUMNS_CACHE = cols
|
|
111
|
+
return cols
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _camel_to_snake(name):
|
|
115
|
+
"""camelCase → snake_case (caseStatus → case_status)。用于匹配数据库列名。"""
|
|
116
|
+
s = re.sub(r'([A-Z0-9]+)([A-Z][a-z])', r'\1_\2', name)
|
|
117
|
+
s = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', s)
|
|
118
|
+
return s.lower()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ============================================================
|
|
122
|
+
# A1: 现实锚定 - PRD 引用的字段/API 是否真实存在 (双层: 代码 + 数据库)
|
|
123
|
+
# ============================================================
|
|
124
|
+
|
|
125
|
+
def score_reality(prd_text):
|
|
126
|
+
fm = load_json(os.path.join(BUILD_CACHE_DIR, 'ui-style.json')).get('field_map', {})
|
|
127
|
+
apis = load_json(os.path.join(BUILD_CACHE_DIR, 'code-api.json'))
|
|
128
|
+
fields_lower = {k.lower() for k in fm}
|
|
129
|
+
# 第二层: 数据库真实列名 (连不上/没导入 = 空集, 自动降级纯代码层)
|
|
130
|
+
db_cols = {c.lower() for c in _load_db_columns()}
|
|
131
|
+
api_paths = set()
|
|
132
|
+
for ep in apis:
|
|
133
|
+
api_paths.add(ep.lower())
|
|
134
|
+
for seg in ep.lower().split('/'):
|
|
135
|
+
if len(seg) >= 4:
|
|
136
|
+
api_paths.add(seg)
|
|
137
|
+
|
|
138
|
+
# 只检查表格单元格里的 camelCase 标识符 (字段规格表的内容)
|
|
139
|
+
# 跳过"新增/规划"上下文的行 —— 新需求规划的路径本来就不存在
|
|
140
|
+
NEW_MARKS = ('新增', '新建', '规划', '待开发', '本次新增', 'NEW')
|
|
141
|
+
candidates = set()
|
|
142
|
+
api_refs = set()
|
|
143
|
+
for line in prd_text.splitlines():
|
|
144
|
+
is_new = any(m in line for m in NEW_MARKS)
|
|
145
|
+
if '|' in line and not is_new:
|
|
146
|
+
for cell in line.split('|'):
|
|
147
|
+
for tok in re.findall(r'\b[a-z][a-zA-Z0-9]{3,30}\b', cell.strip()):
|
|
148
|
+
if any(c.isupper() for c in tok): # camelCase 才像字段名
|
|
149
|
+
candidates.add(tok)
|
|
150
|
+
if not is_new:
|
|
151
|
+
for ref in re.findall(r'/[a-zA-Z][a-zA-Z0-9/]{3,60}', line):
|
|
152
|
+
# 前端组件新路径以 /apps|/views|/components 开头的不检查
|
|
153
|
+
if not re.search(r'(apps|views|components|modules)/', ref):
|
|
154
|
+
api_refs.add(ref)
|
|
155
|
+
|
|
156
|
+
checked, hits, misses = 0, 0, []
|
|
157
|
+
db_bonus = 0 # 数据库层额外救回的字段数 (报告用)
|
|
158
|
+
for tok in sorted(candidates):
|
|
159
|
+
checked += 1
|
|
160
|
+
tl = tok.lower()
|
|
161
|
+
if tl in fields_lower or any(tl in f or f in tl for f in fields_lower):
|
|
162
|
+
hits += 1
|
|
163
|
+
else:
|
|
164
|
+
# 第二层: 代码没有 → 查数据库列 (camelCase → snake_case)
|
|
165
|
+
snake = _camel_to_snake(tok)
|
|
166
|
+
if db_cols and (snake in db_cols or tl in db_cols):
|
|
167
|
+
hits += 1
|
|
168
|
+
db_bonus += 1
|
|
169
|
+
else:
|
|
170
|
+
misses.append(tok)
|
|
171
|
+
for ref in sorted(api_refs):
|
|
172
|
+
checked += 1
|
|
173
|
+
rl = ref.lower()
|
|
174
|
+
# ① 精确匹配 或 段级匹配
|
|
175
|
+
if rl in api_paths or any(seg in api_paths for seg in rl.split('/') if len(seg) >= 4):
|
|
176
|
+
hits += 1
|
|
177
|
+
# ② 后缀匹配: PRD 常省略模块前缀 (写 /vehInspection/getInfo 而非
|
|
178
|
+
# /vehlife/vehInspection/getInfo), 图谱存的是完整路径。只要图谱里
|
|
179
|
+
# 有以 PRD 引用结尾的路径, 就算命中 (长度>=8 防误伤短路径)。
|
|
180
|
+
elif len(rl) >= 8 and any(full.endswith(rl) for full in api_paths):
|
|
181
|
+
hits += 1
|
|
182
|
+
else:
|
|
183
|
+
misses.append(ref)
|
|
184
|
+
|
|
185
|
+
if checked == 0:
|
|
186
|
+
return 1.0, '未引用具体字段/API (零星文案类需求正常)', []
|
|
187
|
+
ratio = hits / checked
|
|
188
|
+
db_note = (',其中 %d 个由数据库层确认' % db_bonus) if db_bonus else ''
|
|
189
|
+
layer_note = ('(双层: 代码+数据库)' if db_cols else '(代码层)')
|
|
190
|
+
return ratio, '{}/{} 个字段/API 引用可在图谱中找到{}{}'.format(
|
|
191
|
+
hits, checked, layer_note, db_note), misses[:8]
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ============================================================
|
|
195
|
+
# A2: 风格保真 - 原型颜色是否来自真源
|
|
196
|
+
# ============================================================
|
|
197
|
+
|
|
198
|
+
def allowed_colors():
|
|
199
|
+
"""真源颜色集合 = 平台模板 + 风格参考 JSON 中出现过的所有色值"""
|
|
200
|
+
allowed = set(NEUTRAL_COLORS)
|
|
201
|
+
sources = [
|
|
202
|
+
os.path.join(TPL_DIR, 'prototype-web.html'),
|
|
203
|
+
os.path.join(TPL_DIR, 'prototype-app.html'),
|
|
204
|
+
os.path.join(INDEX_DIR, 'ref-chart-style.json'),
|
|
205
|
+
os.path.join(INDEX_DIR, 'ref-vben-style.json'),
|
|
206
|
+
]
|
|
207
|
+
for p in sources:
|
|
208
|
+
if os.path.isfile(p):
|
|
209
|
+
text = read_text(p)
|
|
210
|
+
for c in re.findall(r'#[0-9a-fA-F]{3,8}\b', text):
|
|
211
|
+
allowed.add(c.lower())
|
|
212
|
+
return allowed
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def score_style(html_text):
|
|
216
|
+
# emoji = 没有使用项目真实图标体系, 直接重罚
|
|
217
|
+
emojis = re.findall(r'[\U0001F300-\U0001FAFF☀-➿⬀-⯿]', html_text)
|
|
218
|
+
allowed = allowed_colors()
|
|
219
|
+
used = [c.lower() for c in re.findall(r'#[0-9a-fA-F]{3,8}\b', html_text)]
|
|
220
|
+
if not used and not emojis:
|
|
221
|
+
return 1.0, '原型未使用硬编码颜色 (纯 CSS 变量, 最佳)', []
|
|
222
|
+
uniq = sorted(set(used))
|
|
223
|
+
unknown = [c for c in uniq if c not in allowed]
|
|
224
|
+
ratio = 1 - (len(unknown) / len(uniq) if uniq else 0)
|
|
225
|
+
notes = []
|
|
226
|
+
if emojis:
|
|
227
|
+
ratio = min(ratio, 0.3)
|
|
228
|
+
notes.append('发现 {} 个 emoji (必须用 ref-icon.json 的真实图标)'.format(len(set(emojis))))
|
|
229
|
+
msg = '{}/{} 种颜色来自真源'.format(len(uniq) - len(unknown), len(uniq))
|
|
230
|
+
if notes:
|
|
231
|
+
msg += '; ' + '; '.join(notes)
|
|
232
|
+
return ratio, msg, unknown[:10]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ============================================================
|
|
236
|
+
# A3: 模板完整 - 团队模板章节是否齐全
|
|
237
|
+
# ============================================================
|
|
238
|
+
|
|
239
|
+
def score_template(prd_text):
|
|
240
|
+
# 优先看 mode 标记 (HTML 注释, 头部 2KB 内); 无标记回退到内容自识别 (旧 PRD 兼容)
|
|
241
|
+
# 注意: 去掉了旧的 len<3000 启发式 —— 它会把短的 full PRD 误判为 quick
|
|
242
|
+
_m = re.search(r'<!--\s*mode:\s*(\w+)\s*-->', prd_text[:2048], re.IGNORECASE)
|
|
243
|
+
if _m:
|
|
244
|
+
is_quick = _m.group(1).lower() == 'quick'
|
|
245
|
+
else:
|
|
246
|
+
_low = prd_text.lower()
|
|
247
|
+
is_quick = ('mini-prd' in _low or '零星需求' in prd_text)
|
|
248
|
+
sections = QUICK_SECTIONS if is_quick else FULL_SECTIONS
|
|
249
|
+
kind = '零星模板' if is_quick else '完整模板'
|
|
250
|
+
missing = [s for s in sections if s not in prd_text]
|
|
251
|
+
ratio = 1 - len(missing) / len(sections)
|
|
252
|
+
return ratio, '{} {}/{} 章节齐全'.format(kind, len(sections) - len(missing), len(sections)), missing
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# ============================================================
|
|
256
|
+
# Main
|
|
257
|
+
# ============================================================
|
|
258
|
+
|
|
259
|
+
def main():
|
|
260
|
+
if len(sys.argv) < 2:
|
|
261
|
+
print(__doc__)
|
|
262
|
+
return 1
|
|
263
|
+
prd_path = sys.argv[1]
|
|
264
|
+
html_path = sys.argv[2] if len(sys.argv) > 2 else None
|
|
265
|
+
|
|
266
|
+
if not os.path.isfile(prd_path):
|
|
267
|
+
print('PRD 不存在: ' + prd_path)
|
|
268
|
+
return 1
|
|
269
|
+
|
|
270
|
+
# 用程序化 API 评估 (重构后 main 只是 CLI 包装)
|
|
271
|
+
from common.eval_api import evaluate, format_report
|
|
272
|
+
try:
|
|
273
|
+
result = evaluate(prd_path, html_path)
|
|
274
|
+
except FileNotFoundError as e:
|
|
275
|
+
print(str(e))
|
|
276
|
+
return 1
|
|
277
|
+
|
|
278
|
+
# 打印人类可读报告
|
|
279
|
+
print(format_report(result))
|
|
280
|
+
|
|
281
|
+
# 详细 misses (补充 format_report 之外的细节)
|
|
282
|
+
if result.get('msg2') and '非真源颜色' in str(result.get('msg2', '')):
|
|
283
|
+
# msg2 已含信息, 此处可补 unknown 列表 —— 但 evaluate 已合并进 misses
|
|
284
|
+
pass
|
|
285
|
+
|
|
286
|
+
# 记录历史 (原子追加, 防并发损坏)
|
|
287
|
+
try:
|
|
288
|
+
from common.atomicio import atomic_append_jsonl
|
|
289
|
+
os.makedirs(os.path.dirname(HISTORY), exist_ok=True)
|
|
290
|
+
atomic_append_jsonl(HISTORY, {
|
|
291
|
+
'time': datetime.now().isoformat(),
|
|
292
|
+
'prd': result['prd'],
|
|
293
|
+
'a1_reality': result['r1'],
|
|
294
|
+
'a2_style': result['r2'],
|
|
295
|
+
'a3_template': result['r3'],
|
|
296
|
+
'score_pct': result['pct'],
|
|
297
|
+
'passed': result['passed'],
|
|
298
|
+
}, timeout=10)
|
|
299
|
+
except Exception:
|
|
300
|
+
pass # 历史记录失败不影响评估结果
|
|
301
|
+
|
|
302
|
+
# 埋点: PRD 评估结果反馈给 learning 引擎 (prd_accepted / prd_rejected)
|
|
303
|
+
try:
|
|
304
|
+
from learn import record_feedback
|
|
305
|
+
event = 'prd_accepted' if result['passed'] else 'prd_rejected'
|
|
306
|
+
record_feedback(event, {
|
|
307
|
+
'prd': result.get('prd', os.path.basename(prd_path)),
|
|
308
|
+
'score': result.get('pct', 0),
|
|
309
|
+
'a1': result.get('r1'), 'a2': result.get('r2'), 'a3': result.get('r3'),
|
|
310
|
+
})
|
|
311
|
+
except Exception:
|
|
312
|
+
pass # 埋点失败不影响评估
|
|
313
|
+
|
|
314
|
+
return 0 if result['passed'] else 1
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
if __name__ == '__main__':
|
|
318
|
+
sys.exit(main())
|