@hupan56/wlkj 2.7.12 → 3.1.2

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 (253) hide show
  1. package/bin/cli.js +1435 -1107
  2. package/package.json +29 -29
  3. package/templates/qoder/agents/insight-planning.md +1 -1
  4. package/templates/qoder/agents/insight-research.md +28 -15
  5. package/templates/qoder/commands/optional/wl-insight.md +161 -161
  6. package/templates/qoder/commands/optional/wl-report.md +20 -39
  7. package/templates/qoder/commands/optional/wl-status.md +4 -4
  8. package/templates/qoder/commands/wl-code.md +2 -2
  9. package/templates/qoder/commands/wl-design.md +6 -6
  10. package/templates/qoder/commands/wl-init.md +3 -3
  11. package/templates/qoder/commands/wl-prd.md +26 -13
  12. package/templates/qoder/commands/wl-req.md +43 -0
  13. package/templates/qoder/commands/wl-search.md +78 -47
  14. package/templates/qoder/commands/wl-task.md +343 -40
  15. package/templates/qoder/commands/wl-test.md +43 -17
  16. package/templates/qoder/config.yaml +52 -14
  17. package/templates/qoder/hooks/post-tool-use.py +181 -0
  18. package/templates/qoder/hooks/session-start.py +94 -23
  19. package/templates/qoder/hooks/stop-eval.py +207 -0
  20. package/templates/qoder/hooks/user-prompt-submit.py +139 -0
  21. package/templates/qoder/rules/wl-pipeline.md +85 -49
  22. package/templates/qoder/scripts/README.md +102 -0
  23. package/templates/qoder/scripts/capability/__init__.py +26 -0
  24. package/templates/qoder/scripts/capability/__main__.py +72 -0
  25. package/templates/qoder/scripts/capability/adapters/__init__.py +29 -0
  26. package/templates/qoder/scripts/capability/adapters/cli.py +316 -0
  27. package/templates/qoder/scripts/capability/adapters/mcp.py +334 -0
  28. package/templates/qoder/scripts/capability/adapters/qw.py +271 -0
  29. package/templates/qoder/scripts/capability/caps/__init__.py +27 -0
  30. package/templates/qoder/scripts/capability/caps/context.py +36 -0
  31. package/templates/qoder/scripts/capability/caps/cron.py +50 -0
  32. package/templates/qoder/scripts/capability/caps/identity.py +43 -0
  33. package/templates/qoder/scripts/capability/caps/memory.py +71 -0
  34. package/templates/qoder/scripts/capability/caps/notify.py +41 -0
  35. package/templates/qoder/scripts/capability/caps/present.py +48 -0
  36. package/templates/qoder/scripts/capability/caps/repo.py +31 -0
  37. package/templates/qoder/scripts/capability/caps/sandbox.py +43 -0
  38. package/templates/qoder/scripts/capability/chain.py +92 -0
  39. package/templates/qoder/scripts/capability/memory_chain.py +41 -0
  40. package/templates/qoder/scripts/capability/registry.py +246 -0
  41. package/templates/qoder/scripts/capability/registry_mcp.py +250 -0
  42. package/templates/qoder/scripts/capability/smoke_test.py +211 -0
  43. package/templates/qoder/scripts/capability/smoke_test_report.json +94 -0
  44. package/templates/qoder/scripts/deployment/setup/__init__.py +11 -0
  45. package/templates/qoder/scripts/deployment/setup/carriers.py +669 -0
  46. package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
  47. package/templates/qoder/scripts/{init_doctor.py → deployment/setup/init_doctor.py} +100 -39
  48. package/templates/qoder/scripts/{install_qoderwork.py → deployment/setup/install_qoderwork.py} +48 -30
  49. package/templates/qoder/scripts/{platform_doctor.py → deployment/setup/platform_doctor.py} +271 -259
  50. package/templates/qoder/scripts/{repo_root.py → deployment/setup/repo_root.py} +9 -2
  51. package/templates/qoder/scripts/{setup.py → deployment/setup/setup.py} +143 -14
  52. package/templates/qoder/scripts/{setup_lanhu.py → deployment/setup/setup_lanhu.py} +979 -963
  53. package/templates/qoder/scripts/domain/design/__init__.py +0 -0
  54. package/templates/qoder/scripts/{fill_prototype.py → domain/design/fill_prototype.py} +17 -5
  55. package/templates/qoder/scripts/{gen_design_doc.py → domain/design/gen_design_doc.py} +406 -394
  56. package/templates/qoder/scripts/domain/kg/__init__.py +11 -0
  57. package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
  58. package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +201 -0
  59. package/templates/qoder/scripts/domain/kg/build/build_relations.py +132 -0
  60. package/templates/qoder/scripts/{build_style_index.py → domain/kg/build/build_style_index.py} +50 -15
  61. package/templates/qoder/scripts/domain/kg/build/build_workflows.py +149 -0
  62. package/templates/qoder/scripts/{kg_build.py → domain/kg/build/kg_build.py} +685 -612
  63. package/templates/qoder/scripts/{kg_build_db.py → domain/kg/build/kg_build_db.py} +338 -327
  64. package/templates/qoder/scripts/{kg_incremental.py → domain/kg/build/kg_incremental.py} +425 -393
  65. package/templates/qoder/scripts/{learn_aggregate.py → domain/kg/build/learn_aggregate.py} +212 -201
  66. package/templates/qoder/scripts/domain/kg/extract/__init__.py +0 -0
  67. package/templates/qoder/scripts/{common → domain/kg/extract}/extract.py +430 -419
  68. package/templates/qoder/scripts/domain/kg/extract/test_extract.py +115 -0
  69. package/templates/qoder/scripts/{common → domain/kg/extract}/ts_extract.py +614 -536
  70. package/templates/qoder/scripts/domain/kg/graph/__init__.py +0 -0
  71. package/templates/qoder/scripts/{kg_link_db.py → domain/kg/graph/kg_link_db.py} +235 -224
  72. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
  73. package/templates/qoder/scripts/{kg.py → domain/kg/kg.py} +871 -708
  74. package/templates/qoder/scripts/domain/kg/kg_capabilities.py +182 -0
  75. package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
  76. package/templates/qoder/scripts/{context_pack.py → domain/kg/search/context_pack.py} +45 -17
  77. package/templates/qoder/scripts/{enrich_prompt.py → domain/kg/search/enrich_prompt.py} +238 -226
  78. package/templates/qoder/scripts/domain/kg/search/prefetch.py +384 -0
  79. package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
  80. package/templates/qoder/scripts/{search_index.py → domain/kg/search/search_index.py} +210 -88
  81. package/templates/qoder/scripts/domain/kg/server/__init__.py +0 -0
  82. package/templates/qoder/scripts/domain/kg/server/kgd.py +549 -0
  83. package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
  84. package/templates/qoder/scripts/domain/kg/storage/__init__.py +0 -0
  85. package/templates/qoder/scripts/{kg_duckdb.py → domain/kg/storage/kg_duckdb.py} +70 -45
  86. package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
  87. package/templates/qoder/scripts/{learn.py → domain/learning/learn.py} +157 -146
  88. package/templates/qoder/scripts/domain/report/__init__.py +11 -0
  89. package/templates/qoder/scripts/{add_session.py → domain/report/add_session.py} +256 -244
  90. package/templates/qoder/scripts/{export.py → domain/report/export.py} +72 -4
  91. package/templates/qoder/scripts/{report.py → domain/report/report.py} +292 -281
  92. package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
  93. package/templates/qoder/scripts/domain/report/role.py +39 -0
  94. package/templates/qoder/scripts/{status.py → domain/report/status.py} +737 -628
  95. package/templates/qoder/scripts/domain/report/status_snapshot.py +191 -0
  96. package/templates/qoder/scripts/domain/requirement/__init__.py +0 -0
  97. package/templates/qoder/scripts/{archive_prd.py → domain/requirement/archive_prd.py} +389 -377
  98. package/templates/qoder/scripts/domain/requirement/req.py +228 -0
  99. package/templates/qoder/scripts/domain/task/__init__.py +11 -0
  100. package/templates/qoder/scripts/{git_sync.py → domain/task/git_sync.py} +90 -50
  101. package/templates/qoder/scripts/{syncgate.py → domain/task/syncgate.py} +18 -7
  102. package/templates/qoder/scripts/domain/task/task.py +229 -0
  103. package/templates/qoder/scripts/domain/task/task_lifecycle.py +606 -0
  104. package/templates/qoder/scripts/domain/task/task_query.py +162 -0
  105. package/templates/qoder/scripts/domain/task/task_relations.py +425 -0
  106. package/templates/qoder/scripts/{team_sync.py → domain/task/team_sync.py} +101 -23
  107. package/templates/qoder/scripts/domain/task/zentao_sync.py +370 -0
  108. package/templates/qoder/scripts/foundation/__init__.py +0 -0
  109. package/templates/qoder/scripts/foundation/bootstrap.py +145 -0
  110. package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
  111. package/templates/qoder/scripts/foundation/core/cmd_registry.py +112 -0
  112. package/templates/qoder/scripts/foundation/core/config.py +187 -0
  113. package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -400
  114. package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
  115. package/templates/qoder/scripts/foundation/data/contract.py +317 -0
  116. package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
  117. package/templates/qoder/scripts/foundation/data/result.py +223 -0
  118. package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -392
  119. package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
  120. package/templates/qoder/scripts/foundation/identity/check_publish.py +103 -0
  121. package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -238
  122. package/templates/qoder/scripts/foundation/identity/guard.py +159 -0
  123. package/templates/qoder/scripts/{common → foundation/identity}/identity.py +132 -9
  124. package/templates/qoder/scripts/foundation/identity/roles.py +60 -0
  125. package/templates/qoder/scripts/foundation/integrations/__init__.py +0 -0
  126. package/templates/qoder/scripts/{common → foundation/integrations}/active_task.py +30 -15
  127. package/templates/qoder/scripts/{common → foundation/integrations}/feishu.py +11 -10
  128. package/templates/qoder/scripts/{common → foundation/integrations}/terms.py +3 -3
  129. package/templates/qoder/scripts/foundation/integrations/zentao_client.py +220 -0
  130. package/templates/qoder/scripts/foundation/io/__init__.py +0 -0
  131. package/templates/qoder/scripts/{common → foundation/io}/filelock.py +1 -1
  132. package/templates/qoder/scripts/{common → foundation/io}/reqid.py +1 -1
  133. package/templates/qoder/scripts/foundation/protocol/__init__.py +0 -0
  134. package/templates/qoder/scripts/foundation/protocol/duckdb_conn.py +39 -0
  135. package/templates/qoder/scripts/foundation/protocol/mcp_base.py +268 -0
  136. package/templates/qoder/scripts/foundation/utils/__init__.py +0 -0
  137. package/templates/qoder/scripts/{common → foundation/utils}/eval_api.py +12 -1
  138. package/templates/qoder/scripts/{common → foundation/utils}/pip_install.py +23 -6
  139. package/templates/qoder/scripts/{common → foundation/utils}/platform_guard.py +1 -1
  140. package/templates/qoder/scripts/orchestration/__init__.py +0 -0
  141. package/templates/qoder/scripts/orchestration/wlkj.py +185 -0
  142. package/templates/qoder/scripts/protocol/__init__.py +0 -0
  143. package/templates/qoder/scripts/protocol/browser/README.md +207 -0
  144. package/templates/qoder/scripts/protocol/browser/SKILL.md +265 -0
  145. package/templates/qoder/scripts/protocol/browser/config.env +9 -0
  146. package/templates/qoder/scripts/protocol/browser/references/cdp-api.md +110 -0
  147. package/templates/qoder/scripts/protocol/browser/references/migration-2.5.3.md +72 -0
  148. package/templates/qoder/scripts/protocol/browser/references/site-patterns/.gitkeep +0 -0
  149. package/templates/qoder/scripts/protocol/browser/scripts/browser-discovery.mjs +138 -0
  150. package/templates/qoder/scripts/protocol/browser/scripts/cdp-proxy.mjs +672 -0
  151. package/templates/qoder/scripts/protocol/browser/scripts/check-deps.mjs +206 -0
  152. package/templates/qoder/scripts/protocol/browser/scripts/find-url.mjs +253 -0
  153. package/templates/qoder/scripts/protocol/browser/scripts/match-site.mjs +46 -0
  154. package/templates/qoder/scripts/protocol/browser/templates/config.env.template +9 -0
  155. package/templates/qoder/scripts/protocol/mcp/__init__.py +11 -0
  156. package/templates/qoder/scripts/protocol/mcp/kg_mcp_server.py +478 -0
  157. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → protocol/mcp/lanhu_stdio_wrapper.py} +131 -119
  158. package/templates/qoder/scripts/protocol/mcp/mcp_doctor.py +548 -0
  159. package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
  160. package/templates/qoder/scripts/protocol/mcp/mysql_mcp_server.py +461 -0
  161. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1920 -0
  162. package/templates/qoder/scripts/protocol/transports/__init__.py +56 -0
  163. package/templates/qoder/scripts/protocol/transports/base.py +87 -0
  164. package/templates/qoder/scripts/protocol/transports/cli.py +132 -0
  165. package/templates/qoder/scripts/protocol/transports/http.py +141 -0
  166. package/templates/qoder/scripts/protocol/transports/stdio.py +293 -0
  167. package/templates/qoder/scripts/run_weekly_update.bat +27 -18
  168. package/templates/qoder/scripts/run_weekly_update.sh +22 -13
  169. package/templates/qoder/scripts/validation/__init__.py +0 -0
  170. package/templates/qoder/scripts/validation/eval/__init__.py +0 -0
  171. package/templates/qoder/scripts/validation/eval/perf-report-2026-06-24.md +139 -0
  172. package/templates/qoder/scripts/validation/eval/qwork_harness.py +617 -0
  173. package/templates/qoder/scripts/validation/eval/report-commands.md +220 -0
  174. package/templates/qoder/scripts/validation/eval/transcript_timing.py +386 -0
  175. package/templates/qoder/scripts/validation/metrics/__init__.py +0 -0
  176. package/templates/qoder/scripts/{eval_prd.py → validation/metrics/eval_prd.py} +84 -15
  177. package/templates/qoder/scripts/validation/metrics/usability_score.py +750 -0
  178. package/templates/qoder/scripts/validation/test/__init__.py +11 -0
  179. package/templates/qoder/scripts/validation/test/assertion_gen.py +551 -0
  180. package/templates/qoder/scripts/{autotest.py → validation/test/autotest.py} +1234 -1751
  181. package/templates/qoder/scripts/validation/test/autotest_auth.py +109 -0
  182. package/templates/qoder/scripts/{autotest_batch.py → validation/test/autotest_batch.py} +255 -224
  183. package/templates/qoder/scripts/validation/test/autotest_data.py +680 -0
  184. package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
  185. package/templates/qoder/scripts/{autotest_run.py → validation/test/autotest_run.py} +323 -297
  186. package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
  187. package/templates/qoder/scripts/{benchmark.py → validation/test/benchmark.py} +17 -5
  188. package/templates/qoder/scripts/{kg_auto_login.py → validation/test/kg_auto_login.py} +208 -196
  189. package/templates/qoder/scripts/{kg_test_runner.py → validation/test/kg_test_runner.py} +13 -2
  190. package/templates/qoder/scripts/{page_probe.py → validation/test/page_probe.py} +470 -459
  191. package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
  192. package/templates/qoder/settings.json +23 -2
  193. package/templates/qoder/skills/design-import/SKILL.md +235 -226
  194. package/templates/qoder/skills/design-review/SKILL.md +91 -82
  195. package/templates/qoder/skills/prd-generator/SKILL.md +38 -24
  196. package/templates/qoder/skills/prd-review/SKILL.md +8 -6
  197. package/templates/qoder/skills/prototype-generator/SKILL.md +296 -247
  198. package/templates/qoder/skills/spec-coder/SKILL.md +24 -15
  199. package/templates/qoder/skills/spec-generator/SKILL.md +24 -12
  200. package/templates/qoder/skills/test-generator/SKILL.md +9 -7
  201. package/templates/qoder/skills/wl-code/SKILL.md +25 -13
  202. package/templates/qoder/skills/wl-commit/SKILL.md +10 -9
  203. package/templates/qoder/skills/wl-design/SKILL.md +7 -5
  204. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  205. package/templates/qoder/skills/wl-insight/SKILL.md +25 -12
  206. package/templates/qoder/skills/wl-prd-full/SKILL.md +59 -8
  207. package/templates/qoder/skills/wl-prd-quick/SKILL.md +7 -7
  208. package/templates/qoder/skills/wl-prd-review/SKILL.md +18 -6
  209. package/templates/qoder/skills/wl-report/SKILL.md +27 -25
  210. package/templates/qoder/skills/wl-search/SKILL.md +151 -80
  211. package/templates/qoder/skills/wl-spec/SKILL.md +9 -7
  212. package/templates/qoder/skills/wl-status/SKILL.md +50 -18
  213. package/templates/qoder/skills/wl-task/SKILL.md +94 -12
  214. package/templates/qoder/skills/wl-test/SKILL.md +154 -45
  215. package/templates/qoder/templates/prd-full-template.md +7 -0
  216. package/templates/root/AGENTS.md +275 -237
  217. package/templates/root/requirements.txt +3 -0
  218. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +3 -3
  219. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +2 -2
  220. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  221. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  222. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  223. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  224. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  225. package/templates/qoder/scripts/check_carriers.py +0 -238
  226. package/templates/qoder/scripts/check_mcp.py +0 -298
  227. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  228. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  229. package/templates/qoder/scripts/collect_prds.py +0 -31
  230. package/templates/qoder/scripts/common/mentions.py +0 -134
  231. package/templates/qoder/scripts/common/utf8.py +0 -38
  232. package/templates/qoder/scripts/extract_api_params.py +0 -246
  233. package/templates/qoder/scripts/extract_routes.py +0 -54
  234. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  235. package/templates/qoder/scripts/handoff.py +0 -22
  236. package/templates/qoder/scripts/init_developer.py +0 -76
  237. package/templates/qoder/scripts/kg_mcp_server.py +0 -801
  238. package/templates/qoder/scripts/kg_semantic.py +0 -150
  239. package/templates/qoder/scripts/mcp_launcher.py +0 -414
  240. package/templates/qoder/scripts/mysql_mcp_server.py +0 -396
  241. package/templates/qoder/scripts/parse_prds.py +0 -33
  242. package/templates/qoder/scripts/role.py +0 -51
  243. package/templates/qoder/scripts/sync_carriers.py +0 -259
  244. package/templates/qoder/scripts/task.py +0 -1261
  245. package/templates/qoder/scripts/workspace_init.py +0 -102
  246. package/templates/qoder/scripts/zentao_mcp_server.py +0 -424
  247. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  248. /package/templates/qoder/scripts/{common → deployment}/__init__.py +0 -0
  249. /package/templates/qoder/{skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp → scripts/domain/__init__.py} +0 -0
  250. /package/templates/qoder/scripts/{common → domain/kg/graph}/graph_traverse.py +0 -0
  251. /package/templates/qoder/scripts/{common → domain/kg/server}/repowiki.py +0 -0
  252. /package/templates/qoder/scripts/{common → foundation/io}/atomicio.py +0 -0
  253. /package/templates/qoder/scripts/{secure-ls.js → validation/test/secure-ls.js} +0 -0
@@ -0,0 +1,750 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
4
+ import os as _o, sys as _s
5
+ _f = _o.path.abspath(__file__)
6
+ for _ in range(10):
7
+ _f = _o.path.dirname(_f)
8
+ _cp = _o.path.join(_f, 'foundation')
9
+ if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
10
+ break
11
+ if _cp not in _s.path: _s.path.insert(0, _cp)
12
+ from bootstrap import setup; setup()
13
+
14
+ """
15
+ usability_score.py — QODER 工作流可用性评测 (采纳优先 + 流程辅助)
16
+
17
+ 回答: 这套工作流对真实团队, 到底好不好用、用不用得起来?
18
+ 指标定义见 data/docs/constitution/workflow-usability-metrics.md
19
+
20
+ 读现有真实数据 (零外部依赖, 纯函数):
21
+ - data/learning/eval-history.jsonl PRD 质量评测历史 (自动)
22
+ - workspace/members/*/journal/feedback.jsonl 事件流 (rule_violation/task_completed/...)
23
+ - workspace/members/*/journal/learning.yaml 计数汇总
24
+ - data/docs/prd/*.md 已发布需求文档 (绕过率/采纳率)
25
+ - workspace/tasks/*/task.json 任务 (周期/孤岛)
26
+ - 复用 validation/eval/transcript_timing.py 迭代轮/工具调用 (有则读)
27
+
28
+ 诚实标注: 能算的算; 算不了的标 NEEDS_INSTRUMENTATION + 软估算; 绝不编造好看的数。
29
+
30
+ 用法:
31
+ python usability_score.py # 人读 (终端表格)
32
+ python usability_score.py --json # 机器读 (供 wl-status 拉)
33
+ python usability_score.py --days 14 # 评测最近 N 天 (默认 7)
34
+ """
35
+ import argparse
36
+ import json
37
+ import os
38
+ import re
39
+ import sys
40
+ from datetime import datetime, timedelta
41
+ from pathlib import Path
42
+
43
+ try:
44
+ sys.stdout.reconfigure(encoding='utf-8', errors='replace')
45
+ except (AttributeError, TypeError, OSError):
46
+ pass
47
+
48
+ from foundation.core.paths import get_repo_root
49
+
50
+ REPO = get_repo_root()
51
+ EVAL_HISTORY = REPO / 'data' / 'learning' / 'eval-history.jsonl'
52
+ PRD_DIR = REPO / 'data' / 'docs' / 'prd'
53
+ MEMBERS_DIR = REPO / 'workspace' / 'members'
54
+ TASKS_DIR = REPO / 'workspace' / 'tasks'
55
+
56
+ REQ_RE = re.compile(r'REQ-\d{2,4}(?:-\d{2,4})?', re.IGNORECASE)
57
+ REQ_FILENAME_RE = re.compile(r'REQ[-_]\d', re.IGNORECASE)
58
+
59
+
60
+ # ── 数据加载 (纯函数, 失败返回空, 绝不崩) ─────────────────────────────
61
+ def _read_jsonl(path):
62
+ """读 jsonl, 每行一条; 坏行跳过。返回 list[dict]。"""
63
+ if not path.is_file():
64
+ return []
65
+ out = []
66
+ with open(path, encoding='utf-8', errors='replace') as f:
67
+ for line in f:
68
+ line = line.strip()
69
+ if not line:
70
+ continue
71
+ try:
72
+ out.append(json.loads(line))
73
+ except (ValueError, TypeError):
74
+ continue
75
+ return out
76
+
77
+
78
+ def _collect_feedback():
79
+ """收集所有人的 feedback.jsonl, 合并成 list, 附 dev 来源。"""
80
+ events = []
81
+ if not MEMBERS_DIR.is_dir():
82
+ return events
83
+ for dev_dir in MEMBERS_DIR.iterdir():
84
+ fb = dev_dir / 'journal' / 'feedback.jsonl'
85
+ if not fb.is_file():
86
+ continue
87
+ for row in _read_jsonl(fb):
88
+ row.setdefault('dev', dev_dir.name)
89
+ events.append(row)
90
+ return events
91
+
92
+
93
+ def _collect_learning_yaml():
94
+ """汇总所有人的 learning.yaml 计数。返回 dict[stat->sum]。"""
95
+ totals = {}
96
+ if not MEMBERS_DIR.is_dir():
97
+ return totals
98
+ for dev_dir in MEMBERS_DIR.iterdir():
99
+ yml = dev_dir / 'journal' / 'learning.yaml'
100
+ if not yml.is_file():
101
+ continue
102
+ try:
103
+ import yaml
104
+ data = yaml.safe_load(yml.read_text(encoding='utf-8')) or {}
105
+ except Exception:
106
+ continue
107
+ if isinstance(data, dict):
108
+ for k, v in data.items():
109
+ if isinstance(v, (int, float)):
110
+ totals[k] = totals.get(k, 0) + v
111
+ return totals
112
+
113
+
114
+ def _collect_tasks():
115
+ """读所有 task.json。返回 list[dict]。"""
116
+ tasks = []
117
+ if not TASKS_DIR.is_dir():
118
+ return tasks
119
+ for td in TASKS_DIR.iterdir():
120
+ tj = td / 'task.json'
121
+ if not tj.is_file():
122
+ continue
123
+ try:
124
+ tasks.append(json.loads(tj.read_text(encoding='utf-8')))
125
+ except Exception:
126
+ continue
127
+ return tasks
128
+
129
+
130
+ def _published_prd_files():
131
+ """data/docs/prd 下 .md 需求文档 (排除模板/培训材料)。返回 list[Path]。"""
132
+ if not PRD_DIR.is_dir():
133
+ return []
134
+ md = []
135
+ for p in PRD_DIR.rglob('*.md'):
136
+ md.append(p)
137
+ return md
138
+
139
+
140
+ # ── 时间过滤 ───────────────────────────────────────────────────────────
141
+ def _within(ts_str, days):
142
+ """ts_str (ISO) 是否在最近 N 天内。解析失败返回 False。"""
143
+ if not ts_str:
144
+ return False
145
+ try:
146
+ t = datetime.fromisoformat(str(ts_str).split('.')[0].replace('Z', ''))
147
+ except (ValueError, TypeError):
148
+ return False
149
+ return t >= datetime.now() - timedelta(days=days)
150
+
151
+
152
+ # ═══════════════════════════════════════════════════════════════════════
153
+ # 指标计算 —— 每个 return dict 带 {value, unit, level(绿/黄/红/-), auto, note}
154
+ # ═══════════════════════════════════════════════════════════════════════
155
+
156
+ def _level(value, green, yellow_max):
157
+ """阈值判定: value>=green 绿; >=yellow_max 黄; 否则红。
158
+ green/yellow_max 是「达标边界」, value 越高越好。"""
159
+ if value is None:
160
+ return '-'
161
+ if value >= green:
162
+ return 'green'
163
+ if value >= yellow_max:
164
+ return 'yellow'
165
+ return 'red'
166
+
167
+
168
+ def _level_low(value, green_max, yellow_max):
169
+ """反向阈值: value 越低越好。value<=green_max 绿; <=yellow_max 黄; 否则红。"""
170
+ if value is None:
171
+ return '-'
172
+ if value <= green_max:
173
+ return 'green'
174
+ if value <= yellow_max:
175
+ return 'yellow'
176
+ return 'red'
177
+
178
+
179
+ # ── 采纳 (U 系列) ──────────────────────────────────────────────────────
180
+
181
+ def m_u2_first_pass_rate():
182
+ """U2 一次过审率: 首轮 eval 即 passed 的 PRD 占比。🟢自动"""
183
+ rows = _read_jsonl(EVAL_HISTORY)
184
+ if not rows:
185
+ return _empty('U2', '一次过审率', auto='auto')
186
+ by_prd = {}
187
+ for r in rows:
188
+ prd = r.get('prd')
189
+ if not prd:
190
+ continue
191
+ t = r.get('time', '')
192
+ cur = by_prd.get(prd)
193
+ if cur is None or (t and t < cur[0]):
194
+ by_prd[prd] = (t, r)
195
+ if not by_prd:
196
+ return _empty('U2', '一次过审率', auto='auto')
197
+ first_passed = sum(1 for (t, row) in by_prd.values() if row.get('passed'))
198
+ rate = 100.0 * first_passed / len(by_prd)
199
+ return {
200
+ 'id': 'U2', 'name': '一次过审率',
201
+ 'value': round(rate, 1), 'unit': '%', 'level': _level(rate, 60, 30),
202
+ 'auto': 'auto', 'note': '%d/%d 个 PRD 首轮即过门禁' % (first_passed, len(by_prd)),
203
+ 'data': {'passed_first': first_passed, 'total': len(by_prd)},
204
+ }
205
+
206
+
207
+ def m_u3_rework_rounds():
208
+ """U3 返工率: 单 PRD 平均 eval 次数 - 1。🟢自动"""
209
+ rows = _read_jsonl(EVAL_HISTORY)
210
+ if not rows:
211
+ return _empty('U3', '返工轮数', auto='auto')
212
+ by_prd = {}
213
+ for r in rows:
214
+ prd = r.get('prd')
215
+ if prd:
216
+ by_prd.setdefault(prd, []).append(r)
217
+ if not by_prd:
218
+ return _empty('U3', '返工轮数', auto='auto')
219
+ total_evals = sum(len(v) for v in by_prd.values())
220
+ avg_rounds = total_evals / len(by_prd) - 1.0 # 减首轮
221
+ avg_rounds = max(avg_rounds, 0.0)
222
+ return {
223
+ 'id': 'U3', 'name': '返工轮数',
224
+ 'value': round(avg_rounds, 2), 'unit': '轮', 'level': _level_low(avg_rounds, 1.0, 2.0),
225
+ 'auto': 'auto',
226
+ 'note': '单 PRD 被打回后重做的平均轮数 (总eval %d / PRD %d - 1)' % (total_evals, len(by_prd)),
227
+ 'data': {'total_evals': total_evals, 'prd_count': len(by_prd)},
228
+ }
229
+
230
+
231
+ def m_u4_rule_violation_rate(learning_counts):
232
+ """U4 规则违反率: rule_violation / prds_generated。🟢自动"""
233
+ events = _collect_feedback()
234
+ violations = sum(1 for e in events if e.get('event') == 'rule_violation')
235
+ prds = learning_counts.get('prds_generated', 0)
236
+ if prds == 0:
237
+ return _empty('U4', '规则违反率', auto='auto')
238
+ rate = 100.0 * violations / prds
239
+ return {
240
+ 'id': 'U4', 'name': '规则违反率',
241
+ 'value': round(rate, 1), 'unit': '%', 'level': _level_low(rate, 10, 25),
242
+ 'auto': 'auto', 'note': '关键规则(如 ask_platform_first)违反 %d 次 / %d PRD' % (violations, prds),
243
+ 'data': {'violations': violations, 'prds': prds},
244
+ }
245
+
246
+
247
+ def m_u1_adoption_rate(eval_rows):
248
+ """U1 流程采纳率: 带REQ且经eval的 / 全部prd文档。🟡半自动"""
249
+ md_files = _published_prd_files()
250
+ if not md_files:
251
+ return _empty('U1', '流程采纳率', auto='semi')
252
+ evald_prds = set(r.get('prd', '') for r in eval_rows if r.get('prd'))
253
+ # 看文档名里带 REQ 的(工作流产出的命名特征), 且 eval 过
254
+ req_files = [f for f in md_files if REQ_FILENAME_RE.search(f.name)]
255
+ req_and_evald = [f for f in req_files if any(f.name == e or e in f.name for e in evald_prds)]
256
+ rate = 100.0 * len(req_and_evald) / len(md_files) if md_files else 0
257
+ return {
258
+ 'id': 'U1', 'name': '流程采纳率',
259
+ 'value': round(rate, 1), 'unit': '%', 'level': _level(rate, 70, 40),
260
+ 'auto': 'semi',
261
+ 'note': 'REQ文档且经eval %d / 全部prd文档 %d (含培训/模板材料, 需人工核对)' % (len(req_and_evald), len(md_files)),
262
+ 'data': {'workflow_prds': len(req_and_evald), 'total_docs': len(md_files)},
263
+ }
264
+
265
+
266
+ def m_u5_bypass_rate(eval_rows):
267
+ """U5 绕过率: 进了prd目录但没eval记录的REQ文档占比。🟡半自动"""
268
+ md_files = _published_prd_files()
269
+ if not md_files:
270
+ return _empty('U5', '绕过率', auto='semi')
271
+ evald_prds = set(r.get('prd', '') for r in eval_rows if r.get('prd'))
272
+ req_files = [f for f in md_files if REQ_FILENAME_RE.search(f.name)]
273
+ if not req_files:
274
+ # 没有任何 REQ 命名文档 = 全是手写 (强信号: 工作流完全没用起来)
275
+ return {
276
+ 'id': 'U5', 'name': '绕过率',
277
+ 'value': None, 'unit': '%', 'level': 'red',
278
+ 'auto': 'semi',
279
+ 'note': 'data/docs/prd 下无 REQ 命名文档 (%d 个 .md 均为手写) → 工作流产出 0' % len(md_files),
280
+ 'data': {'bypassed': len(md_files), 'req_docs': 0,
281
+ 'manual_docs': len(md_files)},
282
+ }
283
+ bypassed = [f for f in req_files if not any(f.name == e or e in f.name for e in evald_prds)]
284
+ rate = 100.0 * len(bypassed) / len(req_files)
285
+ return {
286
+ 'id': 'U5', 'name': '绕过率',
287
+ 'value': round(rate, 1), 'unit': '%', 'level': _level_low(rate, 15, 40),
288
+ 'auto': 'semi',
289
+ 'note': 'REQ文档未经eval直接提交 %d / REQ文档 %d' % (len(bypassed), len(req_files)),
290
+ 'data': {'bypassed': len(bypassed), 'req_docs': len(req_files)},
291
+ }
292
+
293
+
294
+ def m_u6_activity(days):
295
+ """U6 活跃度: 近N天去重dev数 + 命令分布。🟢自动"""
296
+ events = _collect_feedback()
297
+ recent = [e for e in events if _within(e.get('ts', ''), days)]
298
+ devs = set(e.get('dev') for e in recent if e.get('dev'))
299
+ # 命令分布: 从 task_completed/commit_done/prd_* 等事件名粗估
300
+ cmd_dist = {}
301
+ for e in recent:
302
+ ev = e.get('event', '')
303
+ if ev in ('prd_accepted', 'prd_rejected', 'task_completed', 'commit_done',
304
+ 'review_done', 'design_import'):
305
+ cmd_dist[ev] = cmd_dist.get(ev, 0) + 1
306
+ distinct_cmds = len(cmd_dist)
307
+ n_devs = len(devs)
308
+ # 绿: >=2人 且 覆盖>=4命令; 黄: 1人或<4; 红: 0人
309
+ if n_devs == 0:
310
+ level = 'red'
311
+ elif n_devs >= 2 and distinct_cmds >= 4:
312
+ level = 'green'
313
+ else:
314
+ level = 'yellow'
315
+ return {
316
+ 'id': 'U6', 'name': '活跃度',
317
+ 'value': n_devs, 'unit': '人/%d天' % days, 'level': level,
318
+ 'auto': 'auto', 'note': '近%d天 %d 人活跃, 覆盖 %d 类命令事件' % (days, n_devs, distinct_cmds),
319
+ 'data': {'devs': sorted(devs), 'cmd_distribution': cmd_dist},
320
+ }
321
+
322
+
323
+ # ── 流程 (F 系列) ──────────────────────────────────────────────────────
324
+
325
+ def m_f1_closure_rate(tasks):
326
+ """F1 端到端闭环率: PRD→Task→Commit 三跳齐全。🔴待埋点(软估算)"""
327
+ has_req_id = sum(1 for t in tasks if t.get('req_id'))
328
+ # 软估算: 有 stage_ts.completed 的 task 视为"走到了task"
329
+ completed_tasks = [t for t in tasks if (t.get('stage_ts') or {}).get('completed')]
330
+ learning = _collect_learning_yaml()
331
+ commits = learning.get('commits_done', 0)
332
+ # 软: 完成的task里"可能"对应了commit (无关联键, 只能上下界估)
333
+ return {
334
+ 'id': 'F1', 'name': '端到端闭环率',
335
+ 'value': None, 'unit': '%', 'level': '-',
336
+ 'auto': 'instrumentation_needed',
337
+ 'note': 'NEEDS_INSTRUMENTATION: task.json 无 req_id, commit_done 无 task 引用, 无法精确串联',
338
+ 'data': {
339
+ 'soft_estimate': '完成task %d 个, commit %d 次 (无关联键, 仅计数)' % (len(completed_tasks), commits),
340
+ 'tasks_with_req_id': has_req_id,
341
+ 'completed_tasks': len(completed_tasks),
342
+ 'commits': commits,
343
+ 'fix': 'task.json 加 req_id 字段 + commit_done 加 task 引用后自动升级为准',
344
+ },
345
+ }
346
+
347
+
348
+ def m_f2_funnel(learning_counts):
349
+ """F2 转化漏斗: 各级事件留存。🔴待埋点(review/design没埋)"""
350
+ def _get(*keys):
351
+ for k in keys:
352
+ if k in learning_counts:
353
+ return learning_counts[k]
354
+ return 0
355
+ funnel = {
356
+ 'PRD产出': _get('prds_generated'),
357
+ 'PRD打回(负向)': _get('rejections'),
358
+ '设计录入': _get('designs_imported'), # 注册了但从未埋点 → 恒0
359
+ 'PRD评审': _get('reviews_done'), # 注册了但从未埋点 → 恒0
360
+ '任务完成': _get('tasks_completed'),
361
+ '代码提交': _get('commits_done'),
362
+ }
363
+ needs_fix = (funnel['设计录入'] == 0 and funnel['PRD产出'] > 0)
364
+ return {
365
+ 'id': 'F2', 'name': '转化漏斗',
366
+ 'value': None, 'unit': '级', 'level': 'yellow' if needs_fix else '-',
367
+ 'auto': 'instrumentation_needed' if needs_fix else 'auto',
368
+ 'note': '设计录入/PRD评审 = 0 (events.py 注册但从未埋点)' if needs_fix else '各级留存计数',
369
+ 'data': funnel,
370
+ }
371
+
372
+
373
+ def m_f3_command_efficiency(days):
374
+ """F3 单命令效率: 迭代轮/工具调用 vs 基线。🟢自动(复用transcript_timing)"""
375
+ summary = None
376
+ try:
377
+ # 尝试复用 transcript_timing (QoderWork transcript)
378
+ sys.path.insert(0, str(REPO / '.qoder' / 'scripts' / 'validation' / 'eval'))
379
+ from transcript_timing import analyze as _analyze # noqa
380
+ # 找 transcript 目录
381
+ sess_dir = Path.home() / '.qoderwork' / 'logs' / 'sessions'
382
+ if sess_dir.is_dir():
383
+ jsonls = list(sess_dir.rglob('*.jsonl'))[-20:] # 最近20个
384
+ agg_turns, agg_tools, agg_cmds = [], [], {}
385
+ for jf in jsonls:
386
+ try:
387
+ res = _analyze(str(jf))
388
+ for t in res.get('turns', []):
389
+ if _within(t.get('submitted_ts'), days):
390
+ agg_turns.append(t.get('num_turns') or 1)
391
+ agg_tools.append(t.get('tool_calls') or 0)
392
+ cmd = t.get('command', '')
393
+ if cmd:
394
+ agg_cmds[cmd] = agg_cmds.get(cmd, 0) + 1
395
+ except Exception:
396
+ continue
397
+ if agg_turns:
398
+ summary = {
399
+ 'avg_rounds': round(sum(agg_turns) / len(agg_turns), 2),
400
+ 'avg_tool_calls': round(sum(agg_tools) / len(agg_tools), 2),
401
+ 'cmd_count': len(agg_cmds),
402
+ 'baseline': 'prd-quick = 1轮/0工具',
403
+ 'top_commands': dict(sorted(agg_cmds.items(), key=lambda x: -x[1])[:5]),
404
+ }
405
+ except Exception:
406
+ summary = None
407
+ if not summary:
408
+ return {
409
+ 'id': 'F3', 'name': '单命令效率',
410
+ 'value': None, 'unit': '', 'level': '-',
411
+ 'auto': 'auto',
412
+ 'note': '无 QoderWork transcript 可读 (非 QoderWork 环境, 或无近期会话); 用 /wl-status 查看',
413
+ 'data': {},
414
+ }
415
+ level = _level_low(summary['avg_rounds'], 2, 4)
416
+ return {
417
+ 'id': 'F3', 'name': '单命令效率',
418
+ 'value': summary['avg_rounds'], 'unit': '轮(平均)', 'level': level,
419
+ 'auto': 'auto', 'note': '平均迭代 %s 轮 / 工具 %s 次 (基线 prd-quick=1轮/0工具)'
420
+ % (summary['avg_rounds'], summary['avg_tool_calls']),
421
+ 'data': summary,
422
+ }
423
+
424
+
425
+ def m_f4_breakpoints(tasks):
426
+ """F4 流程断点: rule_violation高频 + PRD孤岛。🟢自动"""
427
+ events = _collect_feedback()
428
+ violations = [e for e in events if e.get('event') == 'rule_violation']
429
+ by_rule = {}
430
+ for e in violations:
431
+ rule = (e.get('data') or {}).get('rule', 'unknown')
432
+ by_rule.setdefault(rule, []).append(e)
433
+ top_rules = {r: len(v) for r, v in sorted(by_rule.items(), key=lambda x: -len(x[1]))[:3]}
434
+ # PRD 孤岛: task 创建后无 started 的中位数天数
435
+ orphan_days = []
436
+ for t in tasks:
437
+ st = t.get('stage_ts') or {}
438
+ created = st.get('created')
439
+ if created and not st.get('started'):
440
+ try:
441
+ c = datetime.fromisoformat(created.split('.')[0])
442
+ age = (datetime.now() - c).days
443
+ orphan_days.append(age)
444
+ except (ValueError, TypeError):
445
+ continue
446
+ med_orphan = sorted(orphan_days)[len(orphan_days) // 2] if orphan_days else 0
447
+ level = _level_low(med_orphan, 3, 7) if orphan_days else 'green'
448
+ return {
449
+ 'id': 'F4', 'name': '流程断点',
450
+ 'value': med_orphan, 'unit': '天(PRD孤岛中位)', 'level': level,
451
+ 'auto': 'auto', 'note': '规则违反 %d 次; PRD孤岛(task建后未启动) %d 个, 中位 %d 天'
452
+ % (len(violations), len(orphan_days), med_orphan),
453
+ 'data': {'top_violated_rules': top_rules, 'orphan_task_count': len(orphan_days),
454
+ 'orphan_median_days': med_orphan},
455
+ }
456
+
457
+
458
+ def _empty(mid, name, auto='auto'):
459
+ return {'id': mid, 'name': name, 'value': None, 'unit': '', 'level': '-',
460
+ 'auto': auto, 'note': '暂无数据', 'data': {}}
461
+
462
+
463
+ # ═══════════════════════════════════════════════════════════════════════
464
+ # 主流程
465
+ # ═══════════════════════════════════════════════════════════════════════
466
+
467
+ def compute_all(days=7):
468
+ """计算全部指标, 返回 {meta, adoption[], flow[], score}。"""
469
+ eval_rows = _read_jsonl(EVAL_HISTORY)
470
+ learning_counts = _collect_learning_yaml()
471
+ tasks = _collect_tasks()
472
+
473
+ adoption = [
474
+ m_u1_adoption_rate(eval_rows),
475
+ m_u2_first_pass_rate(),
476
+ m_u3_rework_rounds(),
477
+ m_u4_rule_violation_rate(learning_counts),
478
+ m_u5_bypass_rate(eval_rows),
479
+ m_u6_activity(days),
480
+ ]
481
+ flow = [
482
+ m_f1_closure_rate(tasks),
483
+ m_f2_funnel(learning_counts),
484
+ m_f3_command_efficiency(days),
485
+ m_f4_breakpoints(tasks),
486
+ ]
487
+
488
+ # 综合采纳分: 只取有 value 的自动指标, 按达标率算 0-100
489
+ scored = []
490
+ for m in adoption:
491
+ if m.get('value') is not None and m.get('level') in ('green', 'yellow', 'red'):
492
+ scored.append(1.0 if m['level'] == 'green' else (0.5 if m['level'] == 'yellow' else 0.0))
493
+ adoption_score = round(100.0 * sum(scored) / len(scored), 1) if scored else None
494
+
495
+ return {
496
+ 'meta': {
497
+ 'generated_at': datetime.now().isoformat(timespec='seconds'),
498
+ 'window_days': days,
499
+ 'repo': os.path.basename(str(REPO)),
500
+ 'eval_history_rows': len(eval_rows),
501
+ 'feedback_events': len(_collect_feedback()),
502
+ 'tasks': len(tasks),
503
+ 'data_sources': {
504
+ 'eval_history': str(EVAL_HISTORY.relative_to(REPO)) if EVAL_HISTORY.is_file() else '(缺失)',
505
+ 'feedback': 'workspace/members/*/journal/feedback.jsonl',
506
+ 'learning': 'workspace/members/*/journal/learning.yaml',
507
+ },
508
+ },
509
+ 'adoption': adoption,
510
+ 'flow': flow,
511
+ 'adoption_score': adoption_score,
512
+ 'adoption_score_note': ('采纳综合分 = 各自动指标达标率' if scored else
513
+ '采纳分暂无法计算(无有效自动指标数据)'),
514
+ }
515
+
516
+
517
+ _LEVEL_ICON = {'green': '🟢', 'yellow': '🟡', 'red': '🔴', '-': '⬜'}
518
+ _AUTO_TAG = {'auto': '自动', 'semi': '半自动', 'instrumentation_needed': '待埋点'}
519
+
520
+
521
+ def render_human(result):
522
+ """终端人读表格。"""
523
+ m = result['meta']
524
+ print('=' * 64)
525
+ print('QODER 工作流可用性评测')
526
+ print('=' * 64)
527
+ print('窗口: 近 %d 天 | 数据: eval-history %d 行, feedback %d 条, tasks %d 个'
528
+ % (m['window_days'], m['eval_history_rows'], m['feedback_events'], m['tasks']))
529
+
530
+ if result.get('adoption_score') is not None:
531
+ sc = result['adoption_score']
532
+ lvl = '🟢 健康' if sc >= 70 else ('🟡 需关注' if sc >= 40 else '🔴 未用起来')
533
+ print('\n>>> 采纳综合分: %.0f / 100 %s' % (sc, lvl))
534
+
535
+ print('\n── 维度一: 采纳 (主) ── 用不用得起来 ──')
536
+ _print_metrics(result['adoption'])
537
+
538
+ print('\n── 维度二: 流程 (辅) ── 链路顺不顺 ──')
539
+ _print_metrics(result['flow'])
540
+
541
+ print('\n' + '-' * 64)
542
+ print('指标定义: data/docs/constitution/workflow-usability-metrics.md')
543
+ print('详解参数: python usability_score.py --help')
544
+
545
+
546
+ def _print_metrics(metrics):
547
+ for x in metrics:
548
+ icon = _LEVEL_ICON.get(x.get('level', '-'), '⬜')
549
+ tag = _AUTO_TAG.get(x.get('auto', 'auto'), '?')
550
+ val = x.get('value')
551
+ valstr = ('%s %s' % (val, x.get('unit', ''))) if val is not None else '—'
552
+ print(' %s %s [%s] %-12s %s' % (icon, x['id'], tag, x['name'], valstr))
553
+ if x.get('note'):
554
+ print(' %s' % x['note'])
555
+
556
+
557
+ def render_html(result):
558
+ """生成自包含 HTML 看板 (绿黄红仪表盘 + 指标条形)。
559
+
560
+ 吸收宿主 Skills 动态 UI 能力: CLI 用户落盘看, QoderWork 用户走 present.widget 嵌入对话。
561
+ 零外部依赖 (内联 CSS + SVG, 不引 CDN), 照 prototype-generator 的自包含 HTML 范式。
562
+ 用 .format() 避开 CSS 里 % 字符与 % 格式化的歧义。
563
+ """
564
+ m = result['meta']
565
+ score = result.get('adoption_score')
566
+ score_lvl = 'green' if (score or 0) >= 70 else ('yellow' if (score or 0) >= 40 else 'red')
567
+
568
+ def _bar(metric):
569
+ lvl = metric.get('level', '-')
570
+ colors = {'green': '#52c41a', 'yellow': '#faad14', 'red': '#ff4d4f', '-': '#d9d9d9'}
571
+ c = colors.get(lvl, '#d9d9d9')
572
+ val = metric.get('value')
573
+ valstr = ('{} {}'.format(val, metric.get('unit', ''))) if val is not None else '—'
574
+ auto = _AUTO_TAG.get(metric.get('auto', 'auto'), '?')
575
+ icon = _LEVEL_ICON.get(lvl, '⬜')
576
+ note = (metric.get('note') or '').replace('<', '&lt;').replace('>', '&gt;')
577
+ return (
578
+ ' <div class="metric" style="border-left:4px solid {c}">\n'
579
+ ' <div class="m-head"><span class="ico">{icon}</span> <b>{mid} {name}</b>\n'
580
+ ' <span class="tag">{val}</span><span class="auto">{auto}</span></div>\n'
581
+ ' <div class="m-val">{val}</div>\n'
582
+ ' <div class="m-note">{note}</div>\n'
583
+ ' </div>').format(
584
+ c=c, icon=icon, mid=metric.get('id', ''), name=metric.get('name', ''),
585
+ val=valstr, auto=auto, note=note)
586
+
587
+ adoption_bars = '\n'.join(_bar(x) for x in result.get('adoption', [])) or '<div class="metric">无数据</div>'
588
+ flow_bars = '\n'.join(_bar(x) for x in result.get('flow', [])) or '<div class="metric">无数据</div>'
589
+
590
+ # 采纳分仪表盘 (SVG 圆环)
591
+ pct = int(score) if score is not None else 0
592
+ ring_color = {'green': '#52c41a', 'yellow': '#faad14', 'red': '#ff4d4f'}.get(score_lvl, '#d9d9d9')
593
+ circumference = 2 * 3.14159 * 52
594
+ dash = circumference * pct / 100.0
595
+ verdict = {'green': '🟢 健康,用得起来', 'yellow': '🟡 需关注',
596
+ 'red': '🔴 未用起来'}.get(score_lvl, '— 无数据')
597
+
598
+ template = '''<!DOCTYPE html>
599
+ <html lang="zh-CN"><head><meta charset="utf-8">
600
+ <meta name="viewport" content="width=device-width,initial-scale=1">
601
+ <title>QODER 工作流可用性看板</title>
602
+ <style>
603
+ *{{box-sizing:border-box;margin:0;padding:0}}
604
+ body{{font-family:-apple-system,"PingFang SC","Microsoft YaHei",sans-serif;
605
+ background:#f5f7fa;color:#333;padding:20px;max-width:980px;margin:0 auto}}
606
+ h1{{font-size:20px;margin-bottom:4px}}
607
+ .meta{{color:#888;font-size:13px;margin-bottom:18px}}
608
+ .score-wrap{{display:flex;align-items:center;gap:20px;background:#fff;
609
+ border-radius:12px;padding:18px 24px;margin-bottom:20px;box-shadow:0 1px 3px rgba(0,0,0,.08)}}
610
+ .gauge{{width:130px;height:130px;flex-shrink:0}}
611
+ .score-txt b{{font-size:30px;color:{ring_color}}}
612
+ .score-txt .verdict{{display:block;font-size:14px;color:#666;margin-top:2px}}
613
+ h2{{font-size:15px;color:#666;margin:18px 0 10px;border-bottom:1px solid #eee;padding-bottom:6px}}
614
+ .metric{{background:#fff;border-radius:8px;padding:10px 14px;margin-bottom:8px;
615
+ box-shadow:0 1px 2px rgba(0,0,0,.04)}}
616
+ .m-head{{display:flex;align-items:center;gap:8px;font-size:14px;flex-wrap:wrap}}
617
+ .m-head b{{min-width:130px}}
618
+ .m-val{{font-size:18px;font-weight:600;margin:2px 0}}
619
+ .m-note{{font-size:12px;color:#888;line-height:1.5}}
620
+ .tag{{margin-left:auto;background:#f0f0f0;border-radius:4px;padding:1px 8px;font-size:12px}}
621
+ .auto{{background:#e6f7ff;color:#1890ff;border-radius:4px;padding:1px 6px;font-size:11px}}
622
+ .ico{{font-size:16px}}
623
+ .legend{{font-size:12px;color:#999;margin-top:14px}}
624
+ </style></head><body>
625
+ <h1>QODER 工作流可用性看板</h1>
626
+ <div class="meta">窗口近 {days} 天 · eval-history {ev} 行 · feedback {fb} 条 · tasks {tk} 个 · 生成于 {ts}</div>
627
+ <div class="score-wrap">
628
+ <svg class="gauge" viewBox="0 0 120 120">
629
+ <circle cx="60" cy="60" r="52" fill="none" stroke="#f0f0f0" stroke-width="10"/>
630
+ <circle cx="60" cy="60" r="52" fill="none" stroke="{ring_color}" stroke-width="10"
631
+ stroke-dasharray="{dash} {rest}" stroke-linecap="round"
632
+ transform="rotate(-90 60 60)"/>
633
+ <text x="60" y="68" text-anchor="middle" font-size="28" font-weight="bold" fill="{ring_color}">{pct}</text>
634
+ </svg>
635
+ <div class="score-txt">
636
+ <b>{score} / 100</b>
637
+ <span class="verdict">{verdict} — 回答"工作流用不用得起来"</span>
638
+ </div>
639
+ </div>
640
+ <h2>维度一:采纳(主)— 用不用得起来</h2>
641
+ {adoption}
642
+ <h2>维度二:流程(辅)— 链路顺不顺</h2>
643
+ {flow}
644
+ <div class="legend">指标定义:data/docs/constitution/workflow-usability-metrics.md ·
645
+ 🟢达标 🟡需关注 🔴不达标 ⬜无数据 · [自动/半自动/待埋点]标注数据可信度</div>
646
+ </body></html>'''
647
+ return template.format(
648
+ ring_color=ring_color,
649
+ days=m['window_days'], ev=m['eval_history_rows'], fb=m['feedback_events'],
650
+ tk=m['tasks'], ts=m['generated_at'],
651
+ dash='%.1f' % dash, rest='%.1f' % (circumference - dash),
652
+ pct=pct, score=(score if score is not None else '—'), verdict=verdict,
653
+ adoption=adoption_bars, flow=flow_bars,
654
+ )
655
+
656
+
657
+
658
+ def sink_to_patterns(result):
659
+ """把红灯/黄灯项沉淀到 data/learning/patterns/usability-patterns.md (进 git 团队共享)。
660
+
661
+ 吸收宿主 Knowledge Engine 能力的落地出口: wiki/card 是宿主自动生成会被覆盖,
662
+ patterns 是唯一可写且团队共享的载体。每次评测把不达标项 append, 让指标驱动知识沉淀。
663
+ 幂等: 同一天同一指标只记一条(带重复计数), 避免无限膨胀。失败绝不阻塞评测。
664
+ """
665
+ patterns_path = REPO / 'data' / 'learning' / 'patterns' / 'usability-patterns.md'
666
+ try:
667
+ today = datetime.now().strftime('%Y-%m-%d')
668
+ issues = []
669
+ for m in result.get('adoption', []) + result.get('flow', []):
670
+ lvl = m.get('level')
671
+ if lvl in ('red', 'yellow') and m.get('value') is not None:
672
+ valstr = '%s %s' % (m['value'], m.get('unit', ''))
673
+ issues.append(' - [%s] %s %s = %s — %s' % (
674
+ today, m['id'], lvl.upper(), valstr,
675
+ (m.get('note') or '')[:120]))
676
+
677
+ if not issues:
678
+ return # 全绿/无数据, 不打扰
679
+
680
+ # 幂等: 同日已记则跳过(看文件里是否已有今天的记录块)
681
+ try:
682
+ content = patterns_path.read_text(encoding='utf-8')
683
+ except Exception:
684
+ content = ''
685
+ marker = '## %s 评测红灯' % today
686
+ if marker in content:
687
+ return # 今天已沉淀过
688
+
689
+ lines = ['', marker, '(采纳综合分: %s)' % result.get('adoption_score', '—'), ''] + issues
690
+ with open(patterns_path, 'a', encoding='utf-8') as f:
691
+ f.write('\n'.join(lines) + '\n')
692
+ except Exception:
693
+ pass # 沉淀失败绝不阻塞评测
694
+
695
+
696
+ def main():
697
+ parser = argparse.ArgumentParser(description='QODER 工作流可用性评测')
698
+ parser.add_argument('--days', type=int, default=7, help='评测窗口天数 (默认 7)')
699
+ parser.add_argument('--json', action='store_true', help='JSON 输出 (供 wl-status)')
700
+ parser.add_argument('--html', action='store_true',
701
+ help='输出自包含 HTML 看板 (CLI 落盘 / QoderWork 嵌入对话)')
702
+ parser.add_argument('--no-sink', action='store_true', help='不把红灯沉淀到 usability-patterns.md')
703
+ args = parser.parse_args()
704
+
705
+ result = compute_all(days=args.days)
706
+ if args.json:
707
+ print(json.dumps(result, ensure_ascii=False, indent=2))
708
+ elif args.html:
709
+ html = render_html(result)
710
+ # QoderWork 优先嵌入对话流; 不可达则落盘 (双轨)
711
+ sinked = False
712
+ try:
713
+ scripts_dir = str(REPO / '.qoder' / 'scripts')
714
+ if scripts_dir not in sys.path:
715
+ sys.path.insert(0, scripts_dir)
716
+ from capability.registry import resolve
717
+ cap = resolve()
718
+ if cap.present.available:
719
+ sinked = cap.present.widget(html)
720
+ except Exception:
721
+ pass
722
+ if not sinked:
723
+ # 降级: 落盘到当前 dev 的 journal
724
+ out = REPO / 'workspace' / 'members' / (os.environ.get('QODER_DEV', '') or 'shared') / 'journal'
725
+ try:
726
+ # 读真实 dev
727
+ dev_file = REPO / '.qoder' / '.developer'
728
+ if dev_file.is_file():
729
+ for line in dev_file.read_text(encoding='utf-8').splitlines():
730
+ if 'name=' in line:
731
+ out = REPO / 'workspace' / 'members' / line.split('name=', 1)[1].strip().split()[0] / 'journal'
732
+ break
733
+ out.mkdir(parents=True, exist_ok=True)
734
+ fp = out / ('dashboard-%s.html' % datetime.now().strftime('%Y%m%d-%H%M%S'))
735
+ fp.write_text(html, encoding='utf-8')
736
+ print('[HTML 看板] 已落盘: %s' % fp)
737
+ except Exception as e:
738
+ print(html) # 最终兜底: 直接 stdout
739
+ else:
740
+ render_human(result)
741
+ # 红灯沉淀到团队 patterns (人读模式默认开, --json/--html/--no-sink 关)
742
+ if not args.json and not args.html and not args.no_sink:
743
+ sink_to_patterns(result)
744
+
745
+
746
+ NL_HDR = '\n'
747
+
748
+
749
+ if __name__ == '__main__':
750
+ main()