@hupan56/wlkj 3.0.0 → 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 (237) hide show
  1. package/bin/cli.js +1435 -1373
  2. package/package.json +1 -1
  3. package/templates/qoder/commands/optional/wl-insight.md +6 -4
  4. package/templates/qoder/commands/optional/wl-report.md +20 -39
  5. package/templates/qoder/commands/optional/wl-status.md +4 -4
  6. package/templates/qoder/commands/wl-code.md +2 -2
  7. package/templates/qoder/commands/wl-design.md +5 -5
  8. package/templates/qoder/commands/wl-init.md +3 -3
  9. package/templates/qoder/commands/wl-prd.md +26 -13
  10. package/templates/qoder/commands/wl-req.md +3 -3
  11. package/templates/qoder/commands/wl-search.md +78 -47
  12. package/templates/qoder/commands/wl-task.md +343 -40
  13. package/templates/qoder/commands/wl-test.md +10 -10
  14. package/templates/qoder/config.yaml +35 -13
  15. package/templates/qoder/hooks/post-tool-use.py +181 -0
  16. package/templates/qoder/hooks/session-start.py +82 -1
  17. package/templates/qoder/hooks/stop-eval.py +207 -0
  18. package/templates/qoder/hooks/user-prompt-submit.py +139 -0
  19. package/templates/qoder/rules/wl-pipeline.md +70 -8
  20. package/templates/qoder/scripts/README.md +78 -115
  21. package/templates/qoder/scripts/capability/__init__.py +26 -0
  22. package/templates/qoder/scripts/capability/__main__.py +72 -0
  23. package/templates/qoder/scripts/capability/adapters/__init__.py +29 -0
  24. package/templates/qoder/scripts/capability/adapters/cli.py +316 -0
  25. package/templates/qoder/scripts/capability/adapters/mcp.py +334 -0
  26. package/templates/qoder/scripts/capability/adapters/qw.py +271 -0
  27. package/templates/qoder/scripts/capability/caps/__init__.py +27 -0
  28. package/templates/qoder/scripts/capability/caps/context.py +36 -0
  29. package/templates/qoder/scripts/capability/caps/cron.py +50 -0
  30. package/templates/qoder/scripts/capability/caps/identity.py +43 -0
  31. package/templates/qoder/scripts/capability/caps/memory.py +71 -0
  32. package/templates/qoder/scripts/capability/caps/notify.py +41 -0
  33. package/templates/qoder/scripts/capability/caps/present.py +48 -0
  34. package/templates/qoder/scripts/capability/caps/repo.py +31 -0
  35. package/templates/qoder/scripts/capability/caps/sandbox.py +43 -0
  36. package/templates/qoder/scripts/capability/chain.py +92 -0
  37. package/templates/qoder/scripts/capability/memory_chain.py +41 -0
  38. package/templates/qoder/scripts/capability/registry.py +246 -0
  39. package/templates/qoder/scripts/capability/registry_mcp.py +250 -0
  40. package/templates/qoder/scripts/capability/smoke_test.py +211 -0
  41. package/templates/qoder/scripts/capability/smoke_test_report.json +94 -0
  42. package/templates/qoder/scripts/{report → deployment/setup}/__init__.py +11 -11
  43. package/templates/qoder/scripts/{setup → deployment/setup}/carriers.py +33 -26
  44. package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
  45. package/templates/qoder/scripts/{setup → deployment/setup}/init_doctor.py +40 -16
  46. package/templates/qoder/scripts/{setup → deployment/setup}/install_qoderwork.py +20 -12
  47. package/templates/qoder/scripts/{setup → deployment/setup}/platform_doctor.py +8 -2
  48. package/templates/qoder/scripts/{setup → deployment/setup}/repo_root.py +113 -112
  49. package/templates/qoder/scripts/{setup → deployment/setup}/setup.py +31 -11
  50. package/templates/qoder/scripts/{setup → deployment/setup}/setup_lanhu.py +13 -7
  51. package/templates/qoder/scripts/domain/__init__.py +0 -0
  52. package/templates/qoder/scripts/domain/design/__init__.py +0 -0
  53. package/templates/qoder/scripts/{report → domain/design}/fill_prototype.py +12 -6
  54. package/templates/qoder/scripts/{report → domain/design}/gen_design_doc.py +8 -2
  55. package/templates/qoder/scripts/{setup → domain/kg}/__init__.py +11 -11
  56. package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
  57. package/templates/qoder/scripts/{kg → domain/kg/build}/build_entity_registry.py +7 -2
  58. package/templates/qoder/scripts/{kg → domain/kg/build}/build_relations.py +7 -2
  59. package/templates/qoder/scripts/{kg → domain/kg/build}/build_style_index.py +13 -7
  60. package/templates/qoder/scripts/{kg → domain/kg/build}/build_workflows.py +7 -2
  61. package/templates/qoder/scripts/{kg → domain/kg/build}/kg_build.py +76 -9
  62. package/templates/qoder/scripts/{kg → domain/kg/build}/kg_build_db.py +7 -2
  63. package/templates/qoder/scripts/{kg → domain/kg/build}/kg_incremental.py +11 -6
  64. package/templates/qoder/scripts/{report → domain/kg/build}/learn_aggregate.py +9 -4
  65. package/templates/qoder/scripts/domain/kg/extract/__init__.py +0 -0
  66. package/templates/qoder/scripts/{common → domain/kg/extract}/extract.py +430 -419
  67. package/templates/qoder/scripts/{common → domain/kg/extract}/test_extract.py +115 -115
  68. package/templates/qoder/scripts/{common → domain/kg/extract}/ts_extract.py +614 -536
  69. package/templates/qoder/scripts/domain/kg/graph/__init__.py +0 -0
  70. package/templates/qoder/scripts/{kg → domain/kg/graph}/kg_link_db.py +7 -2
  71. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
  72. package/templates/qoder/scripts/{kg → domain/kg}/kg.py +263 -30
  73. package/templates/qoder/scripts/{common → domain/kg}/kg_capabilities.py +182 -182
  74. package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
  75. package/templates/qoder/scripts/{kg → domain/kg/search}/context_pack.py +30 -9
  76. package/templates/qoder/scripts/{kg → domain/kg/search}/enrich_prompt.py +10 -4
  77. package/templates/qoder/scripts/{kg → domain/kg/search}/prefetch.py +33 -8
  78. package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
  79. package/templates/qoder/scripts/{kg → domain/kg/search}/search_index.py +143 -77
  80. package/templates/qoder/scripts/domain/kg/server/__init__.py +0 -0
  81. package/templates/qoder/scripts/domain/kg/server/kgd.py +549 -0
  82. package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
  83. package/templates/qoder/scripts/domain/kg/storage/__init__.py +0 -0
  84. package/templates/qoder/scripts/{kg → domain/kg/storage}/kg_duckdb.py +38 -14
  85. package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
  86. package/templates/qoder/scripts/{report → domain/learning}/learn.py +8 -3
  87. package/templates/qoder/scripts/{mcp → domain/report}/__init__.py +11 -11
  88. package/templates/qoder/scripts/{report → domain/report}/add_session.py +9 -3
  89. package/templates/qoder/scripts/{report → domain/report}/export.py +10 -5
  90. package/templates/qoder/scripts/{report → domain/report}/report.py +9 -4
  91. package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
  92. package/templates/qoder/scripts/{report → domain/report}/role.py +10 -4
  93. package/templates/qoder/scripts/{report → domain/report}/status.py +112 -9
  94. package/templates/qoder/scripts/domain/report/status_snapshot.py +191 -0
  95. package/templates/qoder/scripts/domain/requirement/__init__.py +0 -0
  96. package/templates/qoder/scripts/{report → domain/requirement}/archive_prd.py +8 -2
  97. package/templates/qoder/scripts/{report → domain/requirement}/req.py +228 -222
  98. package/templates/qoder/scripts/{kg → domain/task}/__init__.py +11 -11
  99. package/templates/qoder/scripts/{task → domain/task}/git_sync.py +40 -21
  100. package/templates/qoder/scripts/{task → domain/task}/syncgate.py +13 -8
  101. package/templates/qoder/scripts/{task → domain/task}/task.py +13 -5
  102. package/templates/qoder/scripts/{task → domain/task}/task_lifecycle.py +606 -596
  103. package/templates/qoder/scripts/{task → domain/task}/task_query.py +162 -161
  104. package/templates/qoder/scripts/{task → domain/task}/task_relations.py +425 -424
  105. package/templates/qoder/scripts/{task → domain/task}/team_sync.py +9 -4
  106. package/templates/qoder/scripts/domain/task/zentao_sync.py +370 -0
  107. package/templates/qoder/scripts/foundation/__init__.py +0 -0
  108. package/templates/qoder/scripts/{common → foundation}/bootstrap.py +10 -10
  109. package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
  110. package/templates/qoder/scripts/{common → foundation/core}/cmd_registry.py +1 -1
  111. package/templates/qoder/scripts/{common → foundation/core}/config.py +1 -1
  112. package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -586
  113. package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
  114. package/templates/qoder/scripts/{common → foundation/data}/contract.py +2 -2
  115. package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
  116. package/templates/qoder/scripts/{common → foundation/data}/result.py +1 -1
  117. package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -404
  118. package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
  119. package/templates/qoder/scripts/{common → foundation/identity}/check_publish.py +103 -98
  120. package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -239
  121. package/templates/qoder/scripts/{common → foundation/identity}/guard.py +2 -2
  122. package/templates/qoder/scripts/{common → foundation/identity}/identity.py +14 -10
  123. package/templates/qoder/scripts/{common → foundation/identity}/roles.py +1 -1
  124. package/templates/qoder/scripts/foundation/integrations/__init__.py +0 -0
  125. package/templates/qoder/scripts/{common → foundation/integrations}/active_task.py +30 -15
  126. package/templates/qoder/scripts/{common → foundation/integrations}/feishu.py +2 -2
  127. package/templates/qoder/scripts/{common → foundation/integrations}/terms.py +3 -3
  128. package/templates/qoder/scripts/foundation/integrations/zentao_client.py +220 -0
  129. package/templates/qoder/scripts/foundation/io/__init__.py +0 -0
  130. package/templates/qoder/scripts/{common → foundation/io}/filelock.py +1 -1
  131. package/templates/qoder/scripts/{common → foundation/io}/reqid.py +1 -1
  132. package/templates/qoder/scripts/foundation/protocol/__init__.py +0 -0
  133. package/templates/qoder/scripts/foundation/protocol/duckdb_conn.py +39 -0
  134. package/templates/qoder/scripts/{common → foundation/protocol}/mcp_base.py +1 -1
  135. package/templates/qoder/scripts/foundation/utils/__init__.py +0 -0
  136. package/templates/qoder/scripts/{common → foundation/utils}/eval_api.py +12 -1
  137. package/templates/qoder/scripts/{common → foundation/utils}/pip_install.py +23 -6
  138. package/templates/qoder/scripts/{common → foundation/utils}/platform_guard.py +1 -1
  139. package/templates/qoder/scripts/orchestration/__init__.py +0 -0
  140. package/templates/qoder/scripts/orchestration/wlkj.py +185 -0
  141. package/templates/qoder/scripts/protocol/__init__.py +0 -0
  142. package/templates/qoder/scripts/protocol/browser/README.md +207 -0
  143. package/templates/qoder/scripts/protocol/browser/SKILL.md +265 -0
  144. package/templates/qoder/scripts/protocol/browser/config.env +9 -0
  145. package/templates/qoder/scripts/protocol/browser/references/cdp-api.md +110 -0
  146. package/templates/qoder/scripts/protocol/browser/references/migration-2.5.3.md +72 -0
  147. package/templates/qoder/scripts/protocol/browser/references/site-patterns/.gitkeep +0 -0
  148. package/templates/qoder/scripts/protocol/browser/scripts/browser-discovery.mjs +138 -0
  149. package/templates/qoder/scripts/protocol/browser/scripts/cdp-proxy.mjs +672 -0
  150. package/templates/qoder/scripts/protocol/browser/scripts/check-deps.mjs +206 -0
  151. package/templates/qoder/scripts/protocol/browser/scripts/find-url.mjs +253 -0
  152. package/templates/qoder/scripts/protocol/browser/scripts/match-site.mjs +46 -0
  153. package/templates/qoder/scripts/protocol/browser/templates/config.env.template +9 -0
  154. package/templates/qoder/scripts/protocol/mcp/__init__.py +11 -0
  155. package/templates/qoder/scripts/protocol/mcp/kg_mcp_server.py +478 -0
  156. package/templates/qoder/scripts/{mcp → protocol/mcp}/lanhu_stdio_wrapper.py +11 -5
  157. package/templates/qoder/scripts/{mcp → protocol/mcp}/mcp_doctor.py +47 -14
  158. package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
  159. package/templates/qoder/scripts/{mcp → protocol/mcp}/mysql_mcp_server.py +181 -67
  160. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1920 -0
  161. package/templates/qoder/scripts/protocol/transports/__init__.py +56 -0
  162. package/templates/qoder/scripts/protocol/transports/base.py +87 -0
  163. package/templates/qoder/scripts/protocol/transports/cli.py +132 -0
  164. package/templates/qoder/scripts/protocol/transports/http.py +141 -0
  165. package/templates/qoder/scripts/protocol/transports/stdio.py +293 -0
  166. package/templates/qoder/scripts/run_weekly_update.bat +27 -18
  167. package/templates/qoder/scripts/run_weekly_update.sh +22 -13
  168. package/templates/qoder/scripts/validation/__init__.py +0 -0
  169. package/templates/qoder/scripts/validation/eval/__init__.py +0 -0
  170. package/templates/qoder/scripts/validation/eval/perf-report-2026-06-24.md +139 -0
  171. package/templates/qoder/scripts/validation/eval/qwork_harness.py +617 -0
  172. package/templates/qoder/scripts/validation/eval/report-commands.md +220 -0
  173. package/templates/qoder/scripts/validation/eval/transcript_timing.py +386 -0
  174. package/templates/qoder/scripts/validation/metrics/__init__.py +0 -0
  175. package/templates/qoder/scripts/{report → validation/metrics}/eval_prd.py +17 -10
  176. package/templates/qoder/scripts/validation/metrics/usability_score.py +750 -0
  177. package/templates/qoder/scripts/validation/test/__init__.py +11 -0
  178. package/templates/qoder/scripts/validation/test/assertion_gen.py +551 -0
  179. package/templates/qoder/scripts/{test → validation/test}/autotest.py +64 -4
  180. package/templates/qoder/scripts/{common → validation/test}/autotest_auth.py +1 -1
  181. package/templates/qoder/scripts/{test → validation/test}/autotest_batch.py +16 -3
  182. package/templates/qoder/scripts/{test → validation/test}/autotest_data.py +17 -12
  183. package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
  184. package/templates/qoder/scripts/{test → validation/test}/autotest_run.py +17 -3
  185. package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
  186. package/templates/qoder/scripts/{test → validation/test}/benchmark.py +12 -6
  187. package/templates/qoder/scripts/{test → validation/test}/kg_auto_login.py +8 -2
  188. package/templates/qoder/scripts/{test → validation/test}/kg_test_runner.py +7 -2
  189. package/templates/qoder/scripts/{test → validation/test}/page_probe.py +8 -3
  190. package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
  191. package/templates/qoder/settings.json +30 -0
  192. package/templates/qoder/skills/design-import/SKILL.md +22 -13
  193. package/templates/qoder/skills/design-review/SKILL.md +19 -10
  194. package/templates/qoder/skills/prd-generator/SKILL.md +22 -18
  195. package/templates/qoder/skills/prd-review/SKILL.md +6 -4
  196. package/templates/qoder/skills/prototype-generator/SKILL.md +63 -14
  197. package/templates/qoder/skills/spec-coder/SKILL.md +22 -13
  198. package/templates/qoder/skills/spec-generator/SKILL.md +22 -10
  199. package/templates/qoder/skills/test-generator/SKILL.md +7 -5
  200. package/templates/qoder/skills/wl-code/SKILL.md +23 -11
  201. package/templates/qoder/skills/wl-commit/SKILL.md +9 -8
  202. package/templates/qoder/skills/wl-design/SKILL.md +5 -3
  203. package/templates/qoder/skills/wl-init/SKILL.md +6 -6
  204. package/templates/qoder/skills/wl-insight/SKILL.md +23 -10
  205. package/templates/qoder/skills/wl-prd-full/SKILL.md +57 -6
  206. package/templates/qoder/skills/wl-prd-quick/SKILL.md +3 -3
  207. package/templates/qoder/skills/wl-prd-review/SKILL.md +16 -4
  208. package/templates/qoder/skills/wl-report/SKILL.md +25 -23
  209. package/templates/qoder/skills/wl-search/SKILL.md +149 -78
  210. package/templates/qoder/skills/wl-spec/SKILL.md +7 -5
  211. package/templates/qoder/skills/wl-status/SKILL.md +48 -16
  212. package/templates/qoder/skills/wl-task/SKILL.md +92 -10
  213. package/templates/qoder/skills/wl-test/SKILL.md +75 -29
  214. package/templates/qoder/templates/prd-full-template.md +7 -0
  215. package/templates/root/AGENTS.md +275 -236
  216. package/templates/root/requirements.txt +3 -0
  217. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +3 -3
  218. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +2 -2
  219. package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +0 -23
  220. package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +0 -23
  221. package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +0 -1
  222. package/templates/.qoder/learning/eval-history.jsonl +0 -28
  223. package/templates/data/index/wiki-index.json +0 -8
  224. package/templates/qoder/nul +0 -4
  225. package/templates/qoder/scripts/kg/extract.py +0 -398
  226. package/templates/qoder/scripts/kg/kg_semantic.py +0 -156
  227. package/templates/qoder/scripts/mcp/kg_mcp_server.py +0 -606
  228. package/templates/qoder/scripts/mcp/mcp_launcher.py +0 -442
  229. package/templates/qoder/scripts/mcp/zentao_mcp_server.py +0 -384
  230. package/templates/qoder/scripts/task/__init__.py +0 -11
  231. package/templates/qoder/scripts/test/__init__.py +0 -11
  232. package/templates/qoder/scripts/wlkj.py +0 -116
  233. /package/templates/qoder/scripts/{common → deployment}/__init__.py +0 -0
  234. /package/templates/qoder/scripts/{common → domain/kg/graph}/graph_traverse.py +0 -0
  235. /package/templates/qoder/scripts/{common → domain/kg/server}/repowiki.py +0 -0
  236. /package/templates/qoder/scripts/{common → foundation/io}/atomicio.py +0 -0
  237. /package/templates/qoder/scripts/{test → validation/test}/secure-ls.js +0 -0
@@ -0,0 +1,11 @@
1
+ # 子包初始化: 注入 scripts/根 + 所有子包到 sys.path
2
+ # 让跨包 import (旧式不带前缀) 仍可用, 兼容现有代码
3
+ import os as _os, sys as _sys
4
+ _root = _os.path.dirname(_os.path.dirname(_os.path.abspath(__file__)))
5
+ if _root not in _sys.path:
6
+ _sys.path.insert(0, _root)
7
+ # 注入所有子包目录 (兼容旧式 import xxx)
8
+ for _sub in ('kg', 'build', 'test', 'setup', 'io'):
9
+ _subdir = _os.path.join(_root, _sub)
10
+ if _os.path.isdir(_subdir) and _subdir not in _sys.path:
11
+ _sys.path.insert(0, _subdir)
@@ -0,0 +1,551 @@
1
+ # -*- coding: utf-8 -*-
2
+ # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
3
+ import os as _o, sys as _s
4
+ _f = _o.path.abspath(__file__)
5
+ for _ in range(10):
6
+ _f = _o.path.dirname(_f)
7
+ _cp = _o.path.join(_f, 'foundation')
8
+ if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
9
+ break
10
+ if _cp not in _s.path: _s.path.insert(0, _cp)
11
+ from bootstrap import setup; setup()
12
+
13
+ """
14
+ assertion_gen.py — 断言生成器: PRD 验收标准 / GWT 文本 → 可执行 expect 断言
15
+
16
+ == 为什么有它 (上一轮图谱边界分析的落地) ==
17
+ 工作流测试断言层现状:
18
+ - 操作层已闭环 (图谱定位 + autotest_webaccess.py 执行 anchor 点击/填值)
19
+ - 断言层断了: _check_expect 能执行 {fetch/dom, jsonpath, equals},
20
+ 但那个 equals 期望值"从哪来"没人管 —— quick 模式靠 LLM 瞎编, 不可信。
21
+ - 根因: PRD 验收标准是空注释/自由文本, context_pack.py 只取 title+keywords
22
+ 根本不读验收正文 (wl-test/SKILL.md L177 声称能拿到是空头承诺)。
23
+
24
+ 本模块把"PRD 验收标准 → expect 断言"这条断链接上。分工 (呼应三条红线):
25
+ - 人补语义: PRD 里写结构化验收 (GWT 或 "条件-操作-预期" 带接口名/状态码)
26
+ - 图谱补字段: page_probe 查按钮→接口, field_map 查中文标题→字段名
27
+ - 真跑定对错: 本模块只产断言 JSON, 跑不跑交给 quick/webaccess
28
+
29
+ == 两条铁律 ==
30
+ 红线1 (不假装懂业务): PRD 抽不到结构化断言 → 输出"无可抽取", 绝不编造。
31
+ 红线2 (语义不背书): 字段补全命中标 [图谱], 查不到留 {{ask:}} 占位标 [需确认]。
32
+ (红线3: 本模块不执行, 不import playwright/selenium, 不调 LLM)
33
+
34
+ == 断言输出格式 (严格对齐 autotest_webaccess.py:314 _check_expect 契约) ==
35
+ fetch 断言: {intent:"assert", desc, source,
36
+ expect:{fetch:"/api/x", jsonpath:"code", equals:200, method:"POST", body:{}}}
37
+ dom 断言: {intent:"assert", desc, source,
38
+ expect:{dom:"css", jsonpath:"text", equals:"审批中"}}
39
+
40
+ Usage (作为库, 被 autotest.py assertion-gen 子命令调用):
41
+ assertions, report = generate(task='06-24-xxx', gwt=None, page_keyword='保险')
42
+ assertions, report = generate(task=None, gwt='Given ... When ... Then 接口/api/x code=200')
43
+
44
+ Usage (CLI):
45
+ python assertion_gen.py --task <任务名> [--page 保险]
46
+ python assertion_gen.py --gwt "<GWT文本>" [--page 保险]
47
+ """
48
+
49
+ import json
50
+ import os
51
+ import re
52
+ import sys
53
+ from typing import Optional, List, Dict, Any, Tuple
54
+
55
+ if sys.platform == 'win32':
56
+ try:
57
+ sys.stdout.reconfigure(encoding='utf-8')
58
+ except (AttributeError, IOError):
59
+ pass
60
+
61
+ _THIS_DIR = os.path.dirname(os.path.abspath(__file__))
62
+ if _THIS_DIR not in sys.path:
63
+ sys.path.insert(0, _THIS_DIR)
64
+
65
+ from foundation.core.paths import get_repo_root, TASKS_DIR, index_path # noqa: E402
66
+ from foundation.data.task_utils import resolve_task_dir # noqa: E402
67
+
68
+
69
+ # ============================================================
70
+ # 1. 输入: 从 PRD 抽 "验收标准" 章节 (或吃 --gwt 兜底文本)
71
+ # ============================================================
72
+
73
+ # 验收标准章节的常见标题 (中英, 容错)
74
+ _ACCEPTANCE_HEADINGS = (
75
+ '验收标准', '验收点', '验收', 'acceptance criteria', 'acceptance',
76
+ )
77
+
78
+
79
+ def extract_acceptance_from_prd(prd_path: str) -> Tuple[str, bool]:
80
+ """从 PRD markdown 抽 "## 验收标准" 章节正文。
81
+
82
+ 返回 (text, found_section):
83
+ text — 章节正文 (去掉标题行, 保留 checkbox/正文)
84
+ found_section — True=定位到了该章节; False=PRD 里压根没这个章节
85
+ 读不到文件 → (空, False)。
86
+ """
87
+ if not os.path.isfile(prd_path):
88
+ return '', False
89
+ try:
90
+ with open(prd_path, encoding='utf-8') as f:
91
+ lines = f.readlines()
92
+ except (OSError, UnicodeDecodeError):
93
+ try:
94
+ with open(prd_path, encoding='gbk') as f:
95
+ lines = f.readlines()
96
+ except (OSError, UnicodeDecodeError):
97
+ return '', False
98
+
99
+ # 找验收章节起始 (## 验收标准 / ## Acceptance ...)
100
+ start = None
101
+ for i, ln in enumerate(lines):
102
+ low = ln.strip().lower().lstrip('#').strip()
103
+ if low and any(low == h or low.startswith(h) for h in _ACCEPTANCE_HEADINGS):
104
+ start = i
105
+ break
106
+ if start is None:
107
+ return '', False
108
+
109
+ # 截到下一个同级或更高级标题 (## / #) 为止
110
+ body = []
111
+ for ln in lines[start + 1:]:
112
+ stripped = ln.lstrip()
113
+ if stripped.startswith('#'): # 下一个章节
114
+ break
115
+ body.append(ln)
116
+ return ''.join(body), True
117
+
118
+
119
+ def _meaningful_acceptance(text: str) -> bool:
120
+ """验收正文里有没有"可翻译的内容" (排除纯注释/空)。
121
+
122
+ 纯 HTML 注释 <!-- ... --> 或全是空白 → False。
123
+ """
124
+ # 去掉 HTML 注释
125
+ no_comments = re.sub(r'<!--.*?-->', '', text, flags=re.DOTALL)
126
+ # 去掉 markdown checkbox 符号本身不算内容 (- [ ] 这种, 后面得有字)
127
+ # 找有没有实际的中文/字母描述 (长度>3 的连续非空白段)
128
+ tokens = [t for t in re.split(r'[\s\-\[\]()()【】xX✔✓☐·,,。]', no_comments) if len(t) > 2]
129
+ return len(tokens) > 0
130
+
131
+
132
+ # ============================================================
133
+ # 2. 核心抽取: 验收条目 → 候选断言点 (目标类型, 字段路径, 期望值, 原句)
134
+ # ============================================================
135
+
136
+ # 把验收正文切成"条目" (每行/每 Then/每 checkbox 算一条)
137
+ def _split_items(text: str) -> List[str]:
138
+ """把验收正文拆成条目列表。每行一条; GWT 的 Then/And 各算一条。"""
139
+ items = []
140
+ for line in text.splitlines():
141
+ line = line.strip()
142
+ if not line:
143
+ continue
144
+ # 去掉 markdown checkbox / 列表前缀
145
+ line = re.sub(r'^[-*]\s*\[[ xX]\]\s*', '', line)
146
+ line = re.sub(r'^[-*]\s+', '', line)
147
+ # GWT: 一个 Then/And 行可能含多个断言 (用 "且/并且/和/," 连), 拆开各处理
148
+ # 但保留 Given/When 前缀信息 (后面 _classify 用)
149
+ if not line:
150
+ continue
151
+ items.append(line)
152
+ return items
153
+
154
+
155
+ # 接口断言线索: "接口 /api/xxx" / "返回 code=200" / "调用 xxx 接口"
156
+ _RE_API_PATH = re.compile(r'/api/[A-Za-z0-9_/\-]+')
157
+ _RE_CODE_EQ = re.compile(r'(?:code|状态码|返回码)\s*[==:]\s*(\d+)', re.IGNORECASE)
158
+ _RE_MSG_EQ = re.compile(r'(?:msg|message|消息|提示)["\']?\s*[==:]\s*["\']([^"\']+)["\']', re.IGNORECASE)
159
+ # 数字期望 (单独的 =200, 但要避免和 code= 重复)
160
+ _RE_BARE_NUM = re.compile(r'(?<![A-Za-z])[==:]\s*(\d{3})\b')
161
+ # 字段显示断言: "XX列显示YYY" / "显示为YYY" / "XXX变成YYY" / "状态为YYY"
162
+ _RE_FIELD_SHOW = re.compile(
163
+ r'([\u4e00-\u9fa5A-Za-z]{2,8}?)\s*(?:列|字段|状态|文本|内容|值)?\s*'
164
+ r'(?:显示|显示为|变成|变为|更改为|更新为|为|是)\s*[::]?\s*["\']?([\u4e00-\u9fa5A-Za-z0-9_\-]{1,20})["\']?'
165
+ )
166
+
167
+
168
+ def _classify_item(item: str) -> List[Dict[str, Any]]:
169
+ """把一条验收文本分类成 0..N 个候选断言点。
170
+
171
+ 返回 [{kind, field, equals, raw}, ...]
172
+ kind: 'fetch' | 'dom'
173
+ field: fetch 时=jsonpath(code/msg/data); dom 时=待查的中文标题(如"处理状态")
174
+ equals: 期望值
175
+ raw: 原句 (供 desc/source 用)
176
+ 没有任何可识别断言线索 → 返回 [] (调用方决定是否降级提示)
177
+ """
178
+ raw = item
179
+ # 去掉 GWT 前缀 (Given/When 是前置条件, Then/And 才是断言; 但保守起见都扫)
180
+ body = re.sub(r'^(Given|When|Then|And|并且|且|那么|则)\s*:?\s*', '', item, flags=re.IGNORECASE).strip()
181
+ if not body:
182
+ return []
183
+ points = []
184
+ # ── 接口断言 ──
185
+ api_hits = _RE_API_PATH.findall(body)
186
+ code_hit = _RE_CODE_EQ.search(body)
187
+ msg_hit = _RE_MSG_EQ.search(body)
188
+ if api_hits:
189
+ endpoint = api_hits[0]
190
+ if code_hit:
191
+ points.append({'kind': 'fetch', 'field': 'code',
192
+ 'equals': int(code_hit.group(1)), 'raw': raw})
193
+ if msg_hit:
194
+ points.append({'kind': 'fetch', 'field': 'msg',
195
+ 'equals': msg_hit.group(1), 'raw': raw})
196
+ # 有接口但没显式 code/msg → 默认断 code==200 (最常见成功码), 标 source=规则推断
197
+ if not code_hit and not msg_hit:
198
+ points.append({'kind': 'fetch', 'field': 'code',
199
+ 'equals': 200, 'raw': raw, 'inferred': True})
200
+ elif code_hit:
201
+ # 没接口路径但有 "返回 code=200" → 留接口占位
202
+ points.append({'kind': 'fetch', 'field': 'code',
203
+ 'equals': int(code_hit.group(1)), 'raw': raw,
204
+ 'endpoint_missing': True})
205
+ # ── 字段显示断言 (dom) ──
206
+ # 注意去重: 一条里 "状态列显示审批中" 匹配一次
207
+ for m in _RE_FIELD_SHOW.finditer(body):
208
+ title = m.group(1).strip()
209
+ value = m.group(2).strip()
210
+ # 过滤无意义匹配 (如 "在...下" 的 "下")
211
+ if title in ('在', '条件', '操作', '预期', '结果', '描述', '下'):
212
+ continue
213
+ if value in ('描述', '结果'):
214
+ continue
215
+ points.append({'kind': 'dom', 'field': title, 'equals': value, 'raw': raw})
216
+ return points
217
+
218
+
219
+ # ============================================================
220
+ # 3. 字段补全: 候选断言点 → 可执行断言 (调 page_probe + field_map)
221
+ # ============================================================
222
+
223
+ def _load_field_map() -> Dict[str, Dict[str, Any]]:
224
+ """加载 ui-style.json 的 field_map: {字段名: {titles:[中文标题], count}}。
225
+ 反查用: 中文标题 → 字段名。失败返回 {}。"""
226
+ try:
227
+ p = index_path('ui_style')
228
+ if not p.is_file():
229
+ return {}
230
+ with open(p, encoding='utf-8') as f:
231
+ data = json.load(f)
232
+ return data.get('field_map', {}) or {}
233
+ except (OSError, ValueError, KeyError):
234
+ return {}
235
+
236
+
237
+ def _build_title_to_field(field_map: Dict[str, Dict[str, Any]]) -> Dict[str, str]:
238
+ """中文标题 → 字段名 (取 count 最高的, 即最常用)。
239
+ titles 可能是 ["处理状态","状态"] 多个, 都建索引。"""
240
+ title_to_field = {}
241
+ # 先聚合每个 title 对应的所有 (字段, count)
242
+ candidates = {} # title -> [(field, count)]
243
+ for field, info in field_map.items():
244
+ titles = info.get('titles', []) if isinstance(info, dict) else []
245
+ cnt = info.get('count', 0) if isinstance(info, dict) else 0
246
+ for t in titles:
247
+ candidates.setdefault(t, []).append((field, cnt))
248
+ for title, pairs in candidates.items():
249
+ pairs.sort(key=lambda x: -x[1]) # count 高的优先
250
+ title_to_field[title] = pairs[0][0]
251
+ return title_to_field
252
+
253
+
254
+ def _query_page_endpoints(page_keyword: Optional[str]) -> Dict[str, str]:
255
+ """调 page_probe 拿"按钮名→接口路径"映射。失败/无关键字返回 {}。
256
+
257
+ 用图谱把"延期提交"这种按钮名对上 /api/insurance/delay。
258
+ """
259
+ if not page_keyword:
260
+ return {}
261
+ try:
262
+ import page_probe # noqa: E402
263
+ except ImportError:
264
+ sys.path.insert(0, _THIS_DIR)
265
+ try:
266
+ import page_probe # type: ignore # noqa: E402
267
+ except ImportError:
268
+ return {}
269
+ try:
270
+ profile = page_probe.build_page_profile(keyword=page_keyword)
271
+ except Exception:
272
+ return {}
273
+ if not profile.get('ok'):
274
+ return {}
275
+ btn_to_ep = {}
276
+ for pg in profile.get('pages', []):
277
+ for b in pg.get('buttons', []):
278
+ ep = b.get('endpoint')
279
+ if ep and ep != 'None':
280
+ btn_to_ep[b.get('button', '')] = ep
281
+ return btn_to_ep
282
+
283
+
284
+ def _resolve_endpoint(point: Dict[str, Any], btn_to_ep: Dict[str, str]) -> Tuple[Optional[str], str]:
285
+ """给 fetch 断言补 endpoint。返回 (endpoint, source_note)。
286
+
287
+ 优先级: 句子里已含 /api/xxx > 图谱按钮匹配 > 留占位。
288
+ """
289
+ raw = point.get('raw', '')
290
+ # 1. 句子里已有接口路径
291
+ m = _RE_API_PATH.search(raw)
292
+ if m:
293
+ return m.group(0), '规则·句中含接口'
294
+ # 2. 图谱按钮匹配 (句子里有没有按钮名)
295
+ for btn, ep in btn_to_ep.items():
296
+ if btn and btn in raw:
297
+ return ep, '图谱·按钮画像'
298
+ # 3. endpoint_missing 或匹配不上 → 占位
299
+ if point.get('endpoint_missing'):
300
+ return None, '需确认·接口未指明'
301
+ return None, '需确认·接口未指明'
302
+
303
+
304
+ def _resolve_dom_selector(point: Dict[str, Any],
305
+ title_to_field: Dict[str, str]) -> Tuple[str, str]:
306
+ """给 dom 断言补 css 选择器。返回 (dom_css, source_note)。
307
+
308
+ 中文标题查 field_map → 字段名 → 构造 .vxe-cell 系选择器。
309
+ 查不到 → {{ask:}} 占位 (执行时问用户)。
310
+ """
311
+ title = point.get('field', '')
312
+ value = point.get('equals', '')
313
+ if title in title_to_field:
314
+ field = title_to_field[title]
315
+ # VxeGrid 表格单元格常见 class (项目统一用 vxe), 给一个合理猜测选择器
316
+ # 真实执行时 autotest_webaccess eval 会读 innerText, 这里给最可能的
317
+ css = ".vxe-body--row .vxe-cell[title*='%s'], .vxe-cell--column[data-field='%s']" % (value, field)
318
+ return css, '图谱·字段表命中(%s)' % field
319
+ # 查不到 → 占位, 让用户补 CSS
320
+ css = "{{ask:%s列CSS选择器}}" % title
321
+ return css, '需确认·字段未命中'
322
+
323
+
324
+ def _build_assertion(point: Dict[str, Any],
325
+ btn_to_ep: Dict[str, str],
326
+ title_to_field: Dict[str, str]) -> Optional[Dict[str, Any]]:
327
+ """把一个候选断言点组装成完整 assertion dict (对齐 _check_expect)。"""
328
+ kind = point.get('kind')
329
+ if kind == 'fetch':
330
+ endpoint, ep_note = _resolve_endpoint(point, btn_to_ep)
331
+ if endpoint is None:
332
+ endpoint = '{{ask:接口路径}}'
333
+ expect = {
334
+ 'fetch': endpoint,
335
+ 'jsonpath': point.get('field', 'code'),
336
+ 'equals': point.get('equals', 200),
337
+ 'method': 'POST',
338
+ 'body': {},
339
+ }
340
+ inferred = '·推断' if point.get('inferred') else ''
341
+ # desc 聚焦断言点本身: jsonpath=期望值, 不取整行长句
342
+ desc = '%s=%s' % (point.get('field', 'code'), point.get('equals', 200))
343
+ return {'intent': 'assert', 'desc': desc, 'expect': expect,
344
+ 'source': ep_note + inferred}
345
+ if kind == 'dom':
346
+ css, src_note = _resolve_dom_selector(point, title_to_field)
347
+ expect = {
348
+ 'dom': css,
349
+ 'jsonpath': 'text',
350
+ 'equals': point.get('equals', ''),
351
+ }
352
+ # desc 聚焦: 字段标题=期望值
353
+ desc = '%s=%s' % (point.get('field', ''), point.get('equals', ''))
354
+ return {'intent': 'assert', 'desc': desc, 'expect': expect,
355
+ 'source': src_note}
356
+ return None
357
+
358
+
359
+ # ============================================================
360
+ # 4. 编排: generate() — 对外主入口
361
+ # ============================================================
362
+
363
+ def generate(
364
+ task: Optional[str] = None,
365
+ gwt: Optional[str] = None,
366
+ page_keyword: Optional[str] = None,
367
+ ) -> Tuple[List[Dict[str, Any]], Dict[str, Any]]:
368
+ """生成断言。返回 (assertions, report)。
369
+
370
+ 输入二选一: task=任务名(读其 PRD 验收章节) / gwt=GWT 文本。
371
+ page_keyword: 业务关键词(如"保险"), 触发图谱字段补全。
372
+
373
+ report 说明抽取过程 (供调用方/用户审计: 从哪抽的、抽了几条、为什么没抽到)。
374
+ """
375
+ report: Dict[str, Any] = {
376
+ 'input_source': None, # 'prd' | 'gwt'
377
+ 'prd_found_section': None, # PRD 是否有验收章节
378
+ 'acceptance_meaningful': None, # 验收正文是否有可翻译内容
379
+ 'raw_items': 0,
380
+ 'candidate_points': 0,
381
+ 'reason_if_empty': '', # 抽不到的原因 (诚实降级用)
382
+ }
383
+
384
+ # ── 1. 取验收文本 ──
385
+ text = ''
386
+ if gwt and gwt.strip():
387
+ text = gwt
388
+ report['input_source'] = 'gwt'
389
+ elif task:
390
+ task_dir = resolve_task_dir(task)
391
+ if not task_dir.is_dir():
392
+ report['reason_if_empty'] = '任务不存在: %s' % task
393
+ return [], report
394
+ prd_path = os.path.join(str(task_dir), 'prd.md')
395
+ body, found = extract_acceptance_from_prd(prd_path)
396
+ report['input_source'] = 'prd'
397
+ report['prd_found_section'] = found
398
+ if not found:
399
+ report['reason_if_empty'] = 'PRD 中未找到"验收标准/Acceptance"章节'
400
+ return [], report
401
+ text = body
402
+ else:
403
+ report['reason_if_empty'] = '未提供输入 (需要 --task 或 --gwt)'
404
+ return [], report
405
+
406
+ # ── 2. 文本是否有可翻译内容 ──
407
+ meaningful = _meaningful_acceptance(text)
408
+ report['acceptance_meaningful'] = meaningful
409
+ if not meaningful:
410
+ report['reason_if_empty'] = (
411
+ '验收标准章节无可抽取的结构化断言 (内容为空/仅注释/纯描述性)。'
412
+ '可能原因: 未按 Given-When-Then 或"条件-操作-预期"书写。'
413
+ )
414
+ return [], report
415
+
416
+ # ── 3. 拆条 + 分类成候选断言点 ──
417
+ items = _split_items(text)
418
+ report['raw_items'] = len(items)
419
+ candidates: List[Dict[str, Any]] = []
420
+ for it in items:
421
+ candidates.extend(_classify_item(it))
422
+ report['candidate_points'] = len(candidates)
423
+ if not candidates:
424
+ report['reason_if_empty'] = (
425
+ '验收条目无可识别的断言线索 (无接口名/状态码/字段显示断言)。'
426
+ '提示: 在验收里写明接口路径(如 /api/xxx)、期望 code、或"XX列显示YY"。'
427
+ )
428
+ return [], report
429
+
430
+ # ── 4. 字段补全 (图谱/字段表) ──
431
+ btn_to_ep = _query_page_endpoints(page_keyword)
432
+ field_map = _load_field_map()
433
+ title_to_field = _build_title_to_field(field_map)
434
+
435
+ assertions: List[Dict[str, Any]] = []
436
+ seen = set() # 去重: (kind, field, equals)
437
+ for point in candidates:
438
+ key = (point.get('kind'), point.get('field'), str(point.get('equals')))
439
+ if key in seen:
440
+ continue
441
+ seen.add(key)
442
+ a = _build_assertion(point, btn_to_ep, title_to_field)
443
+ if a:
444
+ assertions.append(a)
445
+ return assertions, report
446
+
447
+
448
+ # ============================================================
449
+ # 5. 渲染: 人可读报告 + CLI
450
+ # ============================================================
451
+
452
+ def render_report(assertions: List[Dict[str, Any]], report: Dict[str, Any],
453
+ task: Optional[str] = None) -> str:
454
+ """渲染人可读的抽取报告 (markdown)。"""
455
+ lines = []
456
+ lines.append('=' * 56)
457
+ lines.append('断言生成 · assertion-gen')
458
+ lines.append('=' * 56)
459
+ if task:
460
+ lines.append('任务: %s' % task)
461
+ src = report.get('input_source')
462
+ lines.append('输入来源: %s' % ({
463
+ 'prd': 'PRD 验收标准章节', 'gwt': '--gwt 文本',
464
+ }.get(src, '未知')))
465
+
466
+ if not assertions:
467
+ reason = report.get('reason_if_empty', '无可抽取断言')
468
+ lines.append('')
469
+ lines.append('⚠ %s' % reason)
470
+ lines.append('')
471
+ lines.append('解决:')
472
+ lines.append(' ① 用 --gwt 临时补充结构化文本 (Given/When/Then + 接口名/状态码)')
473
+ lines.append(' ② 或在 PRD 验收标准里补结构化条目后重跑')
474
+ lines.append(' ③ 业务规则断言本就需要人写语义, 图谱/工具只补字段不编断言')
475
+ lines.append('=' * 56)
476
+ return '\n'.join(lines)
477
+
478
+ lines.append('候选条目: %d · 生成断言: %d' % (
479
+ report.get('candidate_points', 0), len(assertions)))
480
+ lines.append('-' * 56)
481
+ for i, a in enumerate(assertions, 1):
482
+ ex = a.get('expect', {})
483
+ if 'fetch' in ex:
484
+ lines.append('%d. [接口] %s' % (i, a.get('desc', '')))
485
+ lines.append(' fetch=%s %s=%s (method=%s)'
486
+ % (ex.get('fetch'), ex.get('jsonpath'), ex.get('equals'),
487
+ ex.get('method')))
488
+ else:
489
+ lines.append('%d. [字段] %s' % (i, a.get('desc', '')))
490
+ lines.append(' dom=%s equals=%s' % (ex.get('dom'), ex.get('equals')))
491
+ lines.append(' 来源: %s' % a.get('source', '?'))
492
+ lines.append('-' * 56)
493
+ # 置信度统计
494
+ confident = sum(1 for a in assertions if '需确认' not in a.get('source', ''))
495
+ lines.append('可信断言: %d/%d (%s)'
496
+ % (confident, len(assertions),
497
+ '可信' if confident == len(assertions) else '含待确认项'))
498
+ if confident < len(assertions):
499
+ lines.append(' ⚠ 标"需确认"的项字段未命中, 执行前需补 {{ask:}} 占位')
500
+ lines.append('=' * 56)
501
+ return '\n'.join(lines)
502
+
503
+
504
+ def main(argv=None):
505
+ import argparse
506
+ p = argparse.ArgumentParser(
507
+ prog='assertion_gen.py',
508
+ description='断言生成: PRD验收/GWT文本 → 可执行 expect 断言 (不编造, 不执行)',
509
+ )
510
+ g = p.add_mutually_exclusive_group(required=False)
511
+ g.add_argument('--task', help='任务名 (读其 prd.md 的验收标准章节)')
512
+ g.add_argument('--gwt', help='GWT/验收文本 (兜底输入, 不读PRD)')
513
+ p.add_argument('--page', default=None, help='业务关键词(如 保险), 触发图谱字段补全')
514
+ p.add_argument('--out', default=None, help='断言 JSON 输出路径 (默认打印, 不落地)')
515
+ args = p.parse_args(argv)
516
+
517
+ if not args.task and not args.gwt:
518
+ p.error('需要 --task 或 --gwt 之一作为输入')
519
+
520
+ assertions, report = generate(
521
+ task=args.task, gwt=args.gwt, page_keyword=args.page)
522
+
523
+ print(render_report(assertions, report, task=args.task))
524
+
525
+ if assertions:
526
+ out_path = args.out
527
+ if not out_path and args.task:
528
+ task_dir = resolve_task_dir(args.task)
529
+ if task_dir.is_dir():
530
+ out_path = os.path.join(str(task_dir), 'assertions.json')
531
+ if out_path:
532
+ payload = {
533
+ '_contract': {'type': 'assertions', 'version': 1,
534
+ 'source': args.task or 'gwt'},
535
+ 'assertions': assertions,
536
+ }
537
+ with open(out_path, 'w', encoding='utf-8') as f:
538
+ json.dump(payload, f, ensure_ascii=False, indent=2)
539
+ print('断言已写入: %s' % out_path)
540
+ print('喂给执行器: 把 assertions 数组并进 quick 用例的 steps (intent=assert)')
541
+ else:
542
+ print()
543
+ print('--- assertions.json ---')
544
+ print(json.dumps(assertions, ensure_ascii=False, indent=2))
545
+ return 0
546
+ # 抽不到 = 诚实退出, 非零码让调用方知道
547
+ return 2
548
+
549
+
550
+ if __name__ == '__main__':
551
+ sys.exit(main())
@@ -2,7 +2,12 @@
2
2
  # -*- coding: utf-8 -*-
3
3
  # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
4
4
  import os as _o, sys as _s
5
- _cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'common')
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
6
11
  if _cp not in _s.path: _s.path.insert(0, _cp)
7
12
  from bootstrap import setup; setup()
8
13
 
@@ -84,8 +89,8 @@ if sys.platform == 'win32':
84
89
 
85
90
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
86
91
 
87
- from common.paths import get_repo_root, get_tasks_dir, get_developer, MEMBERS_DIR, index_path # noqa: E402
88
- from common.task_utils import load_task_json, resolve_task_dir # noqa: E402
92
+ from foundation.core.paths import get_repo_root, get_tasks_dir, get_developer, MEMBERS_DIR, index_path # noqa: E402
93
+ from foundation.data.task_utils import load_task_json, resolve_task_dir # noqa: E402
89
94
 
90
95
  # ---------------------------------------------------------------------------
91
96
 
@@ -705,7 +710,7 @@ def _record_and_report(task_dir, args, base, src, record_json):
705
710
  }
706
711
 
707
712
  try:
708
- from common.atomicio import atomic_append_jsonl
713
+ from foundation.io.atomicio import atomic_append_jsonl
709
714
  os.makedirs(os.path.dirname(HISTORY_PATH), exist_ok=True)
710
715
  atomic_append_jsonl(HISTORY_PATH, record, timeout=10)
711
716
  except Exception as e:
@@ -977,6 +982,51 @@ def cmd_page(args):
977
982
  return 0
978
983
 
979
984
 
985
+ # ---------------------------------------------------------------------------
986
+ # 子命令: assertion-gen (断言生成: PRD验收/GWT → 可执行 expect 断言)
987
+ # ---------------------------------------------------------------------------
988
+
989
+ def cmd_assertion_gen(args):
990
+ """断言生成器: 把 PRD 验收标准 / GWT 文本翻译成可执行 expect 断言。
991
+
992
+ 薄入口, 实际逻辑在 assertion_gen.py。
993
+ 解决痛点: quick 模式瞎编 expect.equals → 现在断言从 PRD 结构化验收抽取,
994
+ 图谱(page_probe/field_map)只补字段不编语义。详见 assertion_gen.py 文档头。
995
+ """
996
+ try:
997
+ import assertion_gen
998
+ except ImportError:
999
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
1000
+ import assertion_gen # type: ignore
1001
+
1002
+ assertions, report = assertion_gen.generate(
1003
+ task=getattr(args, 'task', None),
1004
+ gwt=getattr(args, 'gwt', None),
1005
+ page_keyword=getattr(args, 'page', None),
1006
+ )
1007
+ print(assertion_gen.render_report(assertions, report, task=getattr(args, 'task', None)))
1008
+
1009
+ if assertions:
1010
+ # 落地: 任务模式写 assertions.json; gwt 模式只打印
1011
+ task_dir = None
1012
+ if getattr(args, 'task', None):
1013
+ task_dir = resolve_task_dir(args.task)
1014
+ if task_dir.is_dir():
1015
+ import json as _json
1016
+ out_path = os.path.join(str(task_dir), 'assertions.json')
1017
+ payload = {
1018
+ '_contract': {'type': 'assertions', 'version': 1,
1019
+ 'source': args.task},
1020
+ 'assertions': assertions,
1021
+ }
1022
+ with open(out_path, 'w', encoding='utf-8') as f:
1023
+ _json.dump(payload, f, ensure_ascii=False, indent=2)
1024
+ print('断言已写入: %s' % out_path)
1025
+ print('下一步: 把 assertions 数组并进 quick 用例 steps (intent=assert), '
1026
+ '用 webaccess/quick 执行真跑验证。')
1027
+ return 0 if assertions else 2 # 抽不到=诚实退出非零
1028
+
1029
+
980
1030
  # ---------------------------------------------------------------------------
981
1031
  # 子命令: report
982
1032
  # ---------------------------------------------------------------------------
@@ -1142,6 +1192,16 @@ def build_parser():
1142
1192
  ppg_grp.add_argument('--url', help='页面 URL 或路径关键词(如 /veh/insurance)')
1143
1193
  ppg.set_defaults(func=cmd_page)
1144
1194
 
1195
+ # assertion-gen: PRD验收/GWT → 可执行 expect 断言 (不编造, 不执行)
1196
+ pag = sub.add_parser('assertion-gen',
1197
+ help='断言生成: PRD验收/GWT → 可执行expect断言 (图谱补字段不编语义)')
1198
+ pag_src = pag.add_mutually_exclusive_group(required=False)
1199
+ pag_src.add_argument('--task', help='任务名 (读其 prd.md 验收标准章节)')
1200
+ pag_src.add_argument('--gwt', help='GWT/验收文本 (兜底输入)')
1201
+ pag.add_argument('--page', default=None,
1202
+ help='业务关键词(如 保险), 触发 page_probe 字段补全')
1203
+ pag.set_defaults(func=cmd_assertion_gen)
1204
+
1145
1205
  return p
1146
1206
 
1147
1207
 
@@ -17,7 +17,7 @@ autotest_auth.py - 自动化测试登录态 (单一真源, 消除 3 处重复)
17
17
  get_token(dev=None, base_url=None) -> str|None 读 token, 失效重登
18
18
 
19
19
  Usage:
20
- from common.autotest_auth import get_token, get_developer
20
+ from test.autotest_auth import get_token, get_developer
21
21
  tok = get_token() # 自动读当前开发者, token 失效自动调 kg_auto_login
22
22
  """
23
23