@hupan56/wlkj 3.3.13 → 3.3.14

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 (103) hide show
  1. package/bin/cli.js +1 -23
  2. package/package.json +29 -29
  3. package/templates/qoder/agents/design-agent.md +20 -0
  4. package/templates/qoder/agents/spec-generator.md +21 -0
  5. package/templates/qoder/commands/optional/wl-spec.md +2 -12
  6. package/templates/qoder/commands/optional/wl-status.md +0 -8
  7. package/templates/qoder/commands/wl-code.md +0 -8
  8. package/templates/qoder/commands/wl-commit.md +1 -11
  9. package/templates/qoder/commands/wl-init.md +129 -129
  10. package/templates/qoder/commands/wl-prd.md +0 -24
  11. package/templates/qoder/commands/wl-search.md +11 -26
  12. package/templates/qoder/commands/wl-task.md +1 -11
  13. package/templates/qoder/commands/wl-test.md +0 -8
  14. package/templates/qoder/config.yaml +8 -3
  15. package/templates/qoder/hooks/session-start.py +365 -384
  16. package/templates/qoder/hooks/stop-eval.py +254 -383
  17. package/templates/qoder/scripts/capability/adapters/mcp.py +314 -322
  18. package/templates/qoder/scripts/capability/registry.py +248 -245
  19. package/templates/qoder/scripts/capability/registry_mcp.py +4 -10
  20. package/templates/qoder/scripts/capability/smoke_test_report.json.new +94 -0
  21. package/templates/qoder/scripts/deployment/setup/init_doctor.py +2 -6
  22. package/templates/qoder/scripts/deployment/setup/install_qoderwork.py +14 -0
  23. package/templates/qoder/scripts/deployment/setup/setup.py +2 -16
  24. package/templates/qoder/scripts/domain/integration/return_to_platform.py +2 -4
  25. package/templates/qoder/scripts/domain/integration/spec_upload.py +4 -4
  26. package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +12 -12
  27. package/templates/qoder/scripts/domain/kg/build/build_relations.py +12 -12
  28. package/templates/qoder/scripts/domain/kg/extract/asset/__init__.py +10 -0
  29. package/templates/qoder/scripts/domain/kg/extract/asset/asset_tree.py +57 -0
  30. package/templates/qoder/scripts/domain/kg/extract/asset/discussion_importer.py +62 -0
  31. package/templates/qoder/scripts/domain/kg/extract/asset/prd_importer.py +146 -0
  32. package/templates/qoder/scripts/domain/kg/extract/asset/prototype_importer.py +64 -0
  33. package/templates/qoder/scripts/domain/kg/extract/asset/returns_importer.py +52 -0
  34. package/templates/qoder/scripts/domain/kg/extract/build_goal3.py +104 -0
  35. package/templates/qoder/scripts/domain/kg/extract/build_goal4.py +55 -0
  36. package/templates/qoder/scripts/domain/kg/extract/build_goal5.py +95 -0
  37. package/templates/qoder/scripts/domain/kg/extract/db/__init__.py +8 -0
  38. package/templates/qoder/scripts/domain/kg/extract/db/data_profile.py +22 -0
  39. package/templates/qoder/scripts/domain/kg/extract/db/fk_extractor.py +55 -0
  40. package/templates/qoder/scripts/domain/kg/extract/db/schema_extractor.py +90 -0
  41. package/templates/qoder/scripts/domain/kg/extract/inference/__init__.py +9 -0
  42. package/templates/qoder/scripts/domain/kg/extract/inference/community_summarizer.py +206 -0
  43. package/templates/qoder/scripts/domain/kg/extract/inference/embed_builder.py +132 -0
  44. package/templates/qoder/scripts/domain/kg/extract/inference/naming_matcher.py +80 -0
  45. package/templates/qoder/scripts/domain/kg/extract/inference/promote.py +59 -0
  46. package/templates/qoder/scripts/domain/kg/extract/inference/recompute.py +93 -0
  47. package/templates/qoder/scripts/domain/kg/extract/inference/weak_link.py +421 -0
  48. package/templates/qoder/scripts/domain/kg/extract/mybatis/__init__.py +9 -0
  49. package/templates/qoder/scripts/domain/kg/extract/mybatis/all.py +79 -0
  50. package/templates/qoder/scripts/domain/kg/extract/mybatis/mapper_parser.py +99 -0
  51. package/templates/qoder/scripts/domain/kg/extract/mybatis/relation_builder.py +69 -0
  52. package/templates/qoder/scripts/domain/kg/extract/mybatis/sql_extractor.py +78 -0
  53. package/templates/qoder/scripts/domain/kg/extract/prd/__init__.py +8 -0
  54. package/templates/qoder/scripts/domain/kg/extract/prd/prd_chunk_embed.py +105 -0
  55. package/templates/qoder/scripts/domain/kg/extract/prd/prd_llm_extract.py +153 -0
  56. package/templates/qoder/scripts/domain/kg/extract/prd/req_anchor.py +120 -0
  57. package/templates/qoder/scripts/domain/kg/graph/kg_link_db.py +235 -0
  58. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +5 -41
  59. package/templates/qoder/scripts/domain/kg/kg.py +0 -14
  60. package/templates/qoder/scripts/domain/kg/search/enrich_prompt.py +238 -0
  61. package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
  62. package/templates/qoder/scripts/domain/kg/switch_project.py +2 -2
  63. package/templates/qoder/scripts/domain/kg/sync_repowiki.py +109 -0
  64. package/templates/qoder/scripts/domain/task/wlkj_panel.py +1348 -1503
  65. package/templates/qoder/scripts/engine/poller.py +219 -0
  66. package/templates/qoder/scripts/foundation/bootstrap.py +145 -156
  67. package/templates/qoder/scripts/orchestration/wlkj.py +189 -306
  68. package/templates/qoder/settings.json +0 -8
  69. package/templates/qoder/skills/wl-spec/SKILL.md +1 -1
  70. package/templates/qoder/commands/wl-data.md +0 -46
  71. package/templates/qoder/commands/wl-fix.md +0 -47
  72. package/templates/qoder/commands/wl-knowledge.md +0 -41
  73. package/templates/qoder/commands/wl-review.md +0 -49
  74. package/templates/qoder/contracts/insight.md +0 -55
  75. package/templates/qoder/hooks/pre-tool-use-commit.py +0 -124
  76. package/templates/qoder/scripts/foundation/io/context_cache.py +0 -94
  77. package/templates/qoder/scripts/tool_guide.md +0 -70
  78. package/templates/qoder/scripts/validation/eval/alignment_matrix.py +0 -176
  79. package/templates/qoder/scripts/validation/eval/bf2_content_fidelity.py +0 -110
  80. package/templates/qoder/scripts/validation/eval/bf2_llmjudge.py +0 -104
  81. package/templates/qoder/scripts/validation/eval/bf_score.py +0 -218
  82. package/templates/qoder/scripts/validation/eval/code_flywheel.py +0 -150
  83. package/templates/qoder/scripts/validation/eval/dispatcher_ab.py +0 -156
  84. package/templates/qoder/scripts/validation/eval/dispatcher_ab_2026-07-21.json +0 -23
  85. package/templates/qoder/scripts/validation/eval/feature_fidelity_flywheel.py +0 -143
  86. package/templates/qoder/scripts/validation/eval/gradient_matrix.py +0 -261
  87. package/templates/qoder/scripts/validation/eval/gradient_matrix_baseline_2026-07-21.json +0 -33
  88. package/templates/qoder/scripts/validation/eval/metrics_dashboard.py +0 -105
  89. package/templates/qoder/scripts/validation/eval/multi_turn_flywheel.py +0 -118
  90. package/templates/qoder/scripts/validation/eval/prd_fidelity_flywheel.py +0 -128
  91. package/templates/qoder/scripts/validation/eval/prd_flywheel.py +0 -148
  92. package/templates/qoder/scripts/validation/eval/prototype_fidelity_flywheel.py +0 -166
  93. package/templates/qoder/scripts/validation/eval/recall_flywheel.py +0 -148
  94. package/templates/qoder/scripts/validation/eval/robustness_flywheel.py +0 -139
  95. package/templates/qoder/scripts/validation/eval/speed_accuracy_flywheel.py +0 -188
  96. package/templates/qoder/scripts/validation/eval/task_flywheel.py +0 -124
  97. package/templates/qoder/scripts/validation/eval/token_flywheel.py +0 -88
  98. package/templates/qoder/scripts/validation/metrics/eval_code_ac.py +0 -177
  99. package/templates/qoder/scripts/validation/metrics/lint_cases.py +0 -170
  100. package/templates/qoder/scripts/validation/test/test_context_cache.py +0 -78
  101. package/templates/qoder/scripts/validation/test/test_lint_cases.py +0 -60
  102. package/templates/qoder/scripts/validation/test/test_pre_tool_use_commit.py +0 -70
  103. package/templates/workspace/specs/REQ-TEST-CTX/.context-cache.json +0 -1
@@ -1,88 +0,0 @@
1
- """token_flywheel.py — FW5 token 维度飞轮(§9.1 "+工作流 token ≤ 裸×0.8" 命令层)。
2
-
3
- T1.2 把 wl-search/wl-prd 的接口铁律 boilerplate(每命令重复)抽到单一 tool_guide.md
4
- (SessionStart 每会话注入一次)。本尺子量 token 节省:
5
- - per_command_saving = 旧 boilerplate tokens - 新 ref tokens
6
- - amortized: N 命令/会话 → 总省 = N×per_command_saving - tool_guide_injection(once)
7
- - break_even = 注入几次命令回本
8
- DashScope 分词器量真实 token。
9
-
10
- 跑法: docker exec -i wlinkj-workspace-backend-1 python /app/scripts/validation/eval/token_flywheel.py
11
- """
12
- import os
13
- import sys
14
- import json
15
- import urllib.request
16
- import urllib.error
17
-
18
- for _p in ("/app", os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", ".."))):
19
- if os.path.isdir(_p) and _p not in sys.path:
20
- sys.path.insert(0, _p)
21
-
22
- CHAT_URL = os.environ.get("ALI_CHAT_BASE", "").rstrip("/") + "/chat/completions"
23
- CHAT_KEY = os.environ.get("DASHSCOPE_API_KEY", "")
24
- MODEL = "qwen-turbo"
25
-
26
- # T1.2 抽走的接口铁律内容(≈ 每命令旧 boilerplate,现归一到 tool_guide §2)
27
- OLD_BLOCK = """## 🚨 接口铁律(防混名/防歧义,先读)
28
- **① 三套名字已互通**(引擎层别名,AI 猜哪个都对,别因"未知子命令"慌):
29
- | 干啥 | wlkj.py / kg.py 子命令 | MCP 工具名 |
30
- |---|---|---|
31
- | 搜代码 | search <词> | search_code |
32
- | 搜 API | api <词> / search <词> | search_api |
33
- | 语义检索 | semantic <词> | rag_search |
34
- | 上下文打包 | context <词> | context_pack |
35
- | 影响分析 | impact <实体> | get_impact |
36
- **② 中文歧义词先提炼**:保险→车辆保险/商业险/交强险;异常→告警/报错;计划→作业/养护;列表→带业务对象。
37
- **③ 禁止 find/grep 手动找模块**——那是 search 的活。只有 search 返 0 或要看具体文件实现时才 Grep/Read。"""
38
-
39
- # T1.2 后命令里的引用(3 行,boilerplate 瘦身后)
40
- NEW_REF = """## 🚨 接口铁律(见 tool_guide · 会话已注入精简版)
41
- 单一信源 .qoder/scripts/tool_guide.md(SessionStart 已注入)。两条红线:三套名都认别慌;禁 find/grep 全盘扫。"""
42
-
43
- # SessionStart 注入的 tool_guide 精简版(每会话一次成本)
44
- INJECTION = """## Tool Guide (共用工具用法 · 完整见 .qoder/scripts/tool_guide.md)
45
- 环境自检: R=$(python ~/.qoderwork/repo_root.py) || R=. ; PY=python 或 python3
46
- 接口三套名互通: wlkj.py/kg.py/MCP 名字都认(search/semantic/context/impact)
47
- 中文歧义词先提炼: 保险->车险 · 异常->告警 · 计划->作业
48
- 禁全盘扫: 不要 find/grep -r/findstr /s/os.walk data/code
49
- 优先级口诀: 精确搜用Grep · 语义找用rag_search · 取全用context_pack · 查库先list_envs · 影响用get_impact"""
50
-
51
-
52
- def tokens(text):
53
- """用 DashScope 分词器量真实 prompt token 数。"""
54
- body = json.dumps({"model": MODEL,
55
- "messages": [{"role": "user", "content": text}], "max_tokens": 1}).encode()
56
- try:
57
- req = urllib.request.Request(CHAT_URL, data=body,
58
- headers={"Authorization": f"Bearer {CHAT_KEY}", "Content-Type": "application/json"})
59
- return json.loads(urllib.request.urlopen(req, timeout=30).read())["usage"]["prompt_tokens"]
60
- except Exception as e:
61
- return -1
62
-
63
-
64
- def main():
65
- old_t = tokens(OLD_BLOCK)
66
- new_t = tokens(NEW_REF)
67
- inj_t = tokens(INJECTION)
68
- per_cmd_save = old_t - new_t
69
- # amortized: N 命令/会话 → 净省 = N×per_cmd_save - inj_t(注入成本一次性)
70
- print("## FW5 token 维度飞轮(T1.2 boilerplate→tool_guide 单一信源)")
71
- print("| 项 | tokens |")
72
- print("|---|---|")
73
- print("| 旧接口铁律/命令(每命令重复) | %d |" % old_t)
74
- print("| 新3行ref/命令 | %d |" % new_t)
75
- print("| tool_guide 注入/会话(once) | %d |" % inj_t)
76
- print("| per-命令节省 | %d |" % per_cmd_save)
77
- print("")
78
- print("amortized 净省(N 命令/会话)= N×%d - %d:" % (per_cmd_save, inj_t))
79
- for n in (1, 2, 3, 5, 10):
80
- net = n * per_cmd_save - inj_t
81
- print(" N=%2d: 净省 %d tokens %s" % (n, net, "✓回本" if net > 0 else "(未回本)"))
82
- be = (inj_t + per_cmd_save - 1) // per_cmd_save if per_cmd_save > 0 else 0
83
- print("break-even: ≈%d 命令/会话回本(之后每命令净省 %d tokens)" % (be, per_cmd_save))
84
- print("\n结论: 多命令会话(≥%d)净省 token,符 §9.1 命令层 token 降。" % max(be, 2))
85
-
86
-
87
- if __name__ == "__main__":
88
- main()
@@ -1,177 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """eval_code_ac.py - P0-3/P0-5: spec AC × git diff 符号对账(代码漏实现 AC 检测)
4
-
5
- 被 stop-eval.py wl-code 分支调。读 spec 第6章 PRD 验收标准映射表 AC + git diff HEAD 改的符号
6
- → 每条 AC 词命中符号 → 覆盖率 + 未实现清单。AC<100% 且有 missed → exit 2(stop-eval 拦回继续)。
7
-
8
- 诚实(第一版):
9
- - AC 覆盖判用"AC 文本词 ∈ git diff 符号集"——保守(多拦比漏好),不语义。
10
- - 符号提取多语言(Java public/private/protected、JS function、Vue methods、Python def)。
11
- - spec 无第6章 AC → exit 0 放行(不拦无 AC 的 spec,防误拦)。
12
- - 无近 spec / git 不可用 → exit 0(绝不误拦)。
13
-
14
- 输出(stdout 末行 JSON): {spec, n_ac, n_symbols, rate, covered[], missed[]}
15
- """
16
- import json
17
- import os
18
- import re
19
- import subprocess
20
- import sys
21
- import time
22
-
23
- if sys.platform == 'win32':
24
- try:
25
- sys.stdout.reconfigure(encoding='utf-8')
26
- except Exception:
27
- pass
28
-
29
- WINDOW = 1800 # 30 分钟内改的 spec 视为本轮产物
30
-
31
-
32
- def _find_base():
33
- """向上找 repo 根(含 .git)。★不能只找 workspace/——.qoder/workspace/ 也存在会误中,
34
- 必须认 .git(仅 repo 根 qoderAll 有, .qoder 无)。"""
35
- d = os.path.dirname(os.path.abspath(__file__))
36
- for _ in range(10):
37
- if os.path.isdir(os.path.join(d, '.git')):
38
- return d
39
- p = os.path.dirname(d)
40
- if p == d:
41
- break
42
- d = p
43
- return os.getcwd()
44
-
45
-
46
- BASE = _find_base()
47
-
48
-
49
- def find_recent_spec():
50
- """近 WINDOW 秒改的 spec.md(workspace/tasks|specs|members)。返最新路径 or None。"""
51
- now = time.time()
52
- roots = [os.path.join(BASE, 'workspace', t) for t in ('tasks', 'specs', 'members')]
53
- best, best_mtime = None, 0
54
- for root in roots:
55
- if not os.path.isdir(root):
56
- continue
57
- for dp, dirs, files in os.walk(root):
58
- dirs[:] = [d for d in dirs if d not in ('node_modules', '.git', '__pycache__')]
59
- for fn in files:
60
- if not (fn.endswith('.spec.md') or (fn.endswith('.md') and 'spec' in fn.lower())):
61
- continue
62
- fp = os.path.join(dp, fn)
63
- try:
64
- m = os.path.getmtime(fp)
65
- except OSError:
66
- continue
67
- if now - m <= WINDOW and m > best_mtime:
68
- best, best_mtime = fp, m
69
- return best
70
-
71
-
72
- def parse_ac(spec_path):
73
- """解析 spec 第6章 PRD 验收标准映射表 AC。返 [ac_text, ...]。"""
74
- try:
75
- text = open(spec_path, encoding='utf-8', errors='replace').read()
76
- except Exception:
77
- return []
78
- # 第6章"PRD 验收标准映射表"
79
- m = re.search(r'#{1,3}\s*6[\.\s、]*[^\n]*验收[^\n]*\n(.*?)(?=\n#{1,3}\s|\Z)', text, re.S | re.I)
80
- section = m.group(1) if m else ''
81
- if not section: # 兜底: 任一"验收标准"段
82
- m2 = re.search(r'#{1,3}[^\n]*验收标准[^\n]*\n(.*?)(?=\n#{1,3}\s|\Z)', text, re.S)
83
- section = m2.group(1) if m2 else ''
84
- if not section:
85
- return []
86
- acs = []
87
- for line in section.split('\n'):
88
- line = line.strip()
89
- if not line.startswith('|') or '---' in line:
90
- continue
91
- cols = [c.strip() for c in line.split('|')]
92
- cols = [c for c in cols if c]
93
- if not cols:
94
- continue
95
- ac = cols[0]
96
- if len(ac) < 4: # 跳短表头/序号
97
- continue
98
- acs.append(ac)
99
- return acs
100
-
101
-
102
- def git_diff_symbols():
103
- """git diff HEAD 改的符号(函数/方法/类名)。返 set。多语言。"""
104
- symbols = set()
105
- try:
106
- r = subprocess.run(['git', 'diff', 'HEAD'], cwd=BASE,
107
- capture_output=True, text=True, timeout=15)
108
- diff = r.stdout or ''
109
- except Exception:
110
- return symbols
111
- # Java/Python/JS 声明: public/private/protected/static/def/function/class 后的符号
112
- decl_re = re.compile(
113
- r'^[+-]\s*(?:public|private|protected|static|final|async|def|function|void|class)\s+'
114
- r'(?:[\w<>\[\],\s\.]+?\s+)?(\w+)\s*[\(\{]')
115
- for line in diff.split('\n'):
116
- if not (line.startswith('+') or line.startswith('-')):
117
- continue
118
- m = decl_re.match(line)
119
- if m:
120
- symbols.add(m.group(1))
121
- # 改的文件名(实体线索)
122
- try:
123
- names = subprocess.run(['git', 'diff', 'HEAD', '--name-only'], cwd=BASE,
124
- capture_output=True, text=True, timeout=10).stdout or ''
125
- for fn in names.split('\n'):
126
- fn = fn.strip().split('/')[-1]
127
- base_name = re.sub(r'\.\w+$', '', fn)
128
- if base_name:
129
- for w in re.split(r'[-_]', base_name):
130
- if len(w) > 2:
131
- symbols.add(w)
132
- except Exception:
133
- pass
134
- return symbols
135
-
136
-
137
- def check_coverage(spec_path):
138
- acs = parse_ac(spec_path)
139
- if not acs:
140
- return {'spec': os.path.basename(spec_path), 'n_ac': 0, 'rate': 1.0,
141
- 'covered': [], 'missed': [], 'note': 'spec 第6章无 AC(不拦)'}
142
- symbols = git_diff_symbols()
143
- # 符号分词(_enforce_code_ac_gate → enforce/code/ac/gate)小写; AC 词也分词; 双向分词匹配
144
- sym_words = set()
145
- for s in symbols:
146
- for w in re.split(r'[_\-]', s):
147
- if len(w) > 2:
148
- sym_words.add(w.lower())
149
- covered, missed = [], []
150
- for ac in acs:
151
- ac_words = set()
152
- for w in re.findall(r'[A-Za-z_]\w{2,}', ac):
153
- for part in re.split(r'[_\-]', w):
154
- if len(part) > 2:
155
- ac_words.add(part.lower())
156
- hit = any(w in sym_words for w in ac_words)
157
- (covered if hit else missed).append(ac)
158
- rate = round(len(covered) / len(acs), 3)
159
- return {'spec': os.path.basename(spec_path), 'n_ac': len(acs),
160
- 'n_symbols': len(symbols), 'rate': rate,
161
- 'covered': covered, 'missed': missed}
162
-
163
-
164
- def main():
165
- spec = find_recent_spec()
166
- if not spec:
167
- print(json.dumps({'error': 'no recent spec', 'rate': 1.0}, ensure_ascii=False))
168
- sys.exit(0) # 无 spec 不拦(非 wl-code 场景)
169
- r = check_coverage(spec)
170
- print(json.dumps(r, ensure_ascii=False))
171
- if r.get('missed') and r['rate'] < 1.0:
172
- sys.exit(2) # AC 未全覆盖 → stop-eval 拦
173
- sys.exit(0)
174
-
175
-
176
- if __name__ == '__main__':
177
- main()
@@ -1,170 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """lint_cases.py - P0-3 w2: wl-test 用例格式 linter(9 铁律里文件内容可检的①②)。
4
-
5
- 治"AI 生成旧格式 cases(action+CSS selector)"→ Playwright 新管线只认 intent+anchor,
6
- 旧格式跑不通。被 /wl-test 调(执行前 lint)+ stop-eval 扫近期 cases 文件调。
7
-
8
- 铁律映射(wl-test.md 9 条,本 linter 检文件内容可判的):
9
- ① intent+anchor 格式,禁旧 action+CSS selector → BLOCK(硬拦)
10
- ② 占位符精确 {{ask:key}}(禁裸"测试账号"/硬编码凭据) → WARN(软提醒)
11
- 其余(③--cases-file ④recall ⑤URL斜杠 ⑥相对url ⑦⑧⑨执行流)非文件内容,命令/执行层管。
12
-
13
- 用法:
14
- python lint_cases.py <cases.json> # lint 单文件
15
- python lint_cases.py <cases.json> --json # 机器可读
16
- python lint_cases.py - # 从 stdin 读 cases JSON
17
-
18
- 输出 stdout 末行 JSON: {file, n_cases, violations:[{rule, severity, idx, detail}], passed}
19
- exit 0 = 过(含 WARN); exit 2 = 有 BLOCK 违规(旧格式); exit 0 = 无 cases/读不到(不拦)。
20
- """
21
- import json
22
- import os
23
- import re
24
- import sys
25
-
26
- if sys.platform == 'win32':
27
- try:
28
- sys.stdout.reconfigure(encoding='utf-8')
29
- except Exception:
30
- pass
31
-
32
- # 旧格式 CSS selector 特征(出现在 target/anchor 里 = 旧 action+CSS 范式)
33
- _CSS_SELECTOR_RE = re.compile(
34
- r'#[A-Za-z_][\w-]*' # #id(#submit-btn;不匹 #1 步号/颜色#fff 首字母要求)
35
- r'|\.[A-Za-z_][\w-]*' # .class(要求点后字母,避 v1.2 版本号误伤)
36
- r'|\w+\[[^\]]+\]' # tag[attr=...] / [attr]
37
- r'|>\s*[A-Za-z][\w-]*' # 子代选择器 >
38
- )
39
- # 凭据类字段名({{ask:}} 应覆盖这些,禁硬编码)
40
- _CRED_HINTS = ("账号", "密码", "account", "password", "passwd", "手机", "验证码", "邮箱", "token")
41
- # 字面凭据(ascii 字母数字/@/./-,3-30 字符;排除中文描述/意图词)
42
- _LITERAL_CRED_RE = re.compile(r'^[A-Za-z0-9_@.\-]{3,30}$')
43
-
44
-
45
- def _load_cases(src):
46
- """src=文件路径 or '-'(stdin)。返 (cases_list, file_label)。读不到返 ([], label)。"""
47
- if src == "-":
48
- raw = sys.stdin.read()
49
- label = "<stdin>"
50
- else:
51
- try:
52
- with open(src, encoding="utf-8", errors="replace") as f:
53
- raw = f.read()
54
- label = os.path.basename(src)
55
- except Exception:
56
- return [], src
57
- raw = raw.strip()
58
- if not raw:
59
- return [], label
60
- try:
61
- data = json.loads(raw)
62
- except Exception:
63
- return [], label
64
- # cases 可能是 list,或 {cases:[...]} / {steps:[...]}
65
- if isinstance(data, list):
66
- return data, label
67
- if isinstance(data, dict):
68
- for k in ("cases", "steps", "items", "test_cases"):
69
- v = data.get(k)
70
- if isinstance(v, list):
71
- return v, label
72
- return [], label
73
-
74
-
75
- def _is_block_old_format(case):
76
- """铁律①:case 是旧 action+CSS selector 范式 → True(BLOCK)。
77
- 判定:有 'action'+'target' 键(旧范式),或任一值含 CSS selector 特征。"""
78
- if not isinstance(case, dict):
79
- return False
80
- has_action_target = "action" in case and "target" in case
81
- # target/value/anchor 文本里含 CSS selector
82
- for k in ("target", "selector", "css"):
83
- v = case.get(k)
84
- if isinstance(v, str) and _CSS_SELECTOR_RE.search(v):
85
- return True
86
- # anchor 是字符串(应是 {role,name} dict)且像 selector
87
- anchor = case.get("anchor")
88
- if isinstance(anchor, str) and _CSS_SELECTOR_RE.search(anchor):
89
- return True
90
- # 新格式判定:有 intent + anchor(dict) → 放过;否则若 action+target → 旧
91
- if has_action_target and not (isinstance(anchor, dict) or "intent" in case):
92
- return True
93
- return False
94
-
95
-
96
- def _looks_like_literal_cred(v):
97
- """值像字面凭据(ascii 字母数字/@/./-,3-30 字符;含中文=多半描述→不算)。"""
98
- v = v.strip()
99
- if not v or v.startswith("{{"):
100
- return False
101
- if re.search(r'[一-龥]', v): # 含中文 → 描述,不当凭据字面
102
- return False
103
- return bool(_LITERAL_CRED_RE.match(v))
104
-
105
-
106
- def _cred_without_ask(case):
107
- """铁律②:凭据类【本字段】未用 {{ask:}} 包裹(硬编码)→ 返回违规 key 列表(WARN)。
108
- 高精度:只看本字段 key/name 是不是凭据词(不用 desc 全局触发,避误伤 intent/desc),
109
- 且值须像字面凭据(ascii、非中文描述、非占位)。"""
110
- if not isinstance(case, dict):
111
- return []
112
- bad = []
113
- for k, v in case.items():
114
- if not isinstance(v, str) or not v:
115
- continue
116
- if "{{ask:" in v or v.startswith("{{"):
117
- continue
118
- field_name = str(case.get("name") or case.get("field") or k).lower()
119
- if not any(h in field_name for h in _CRED_HINTS):
120
- continue # 本字段名不像凭据 → 放过(不拿 desc 连坐)
121
- if _looks_like_literal_cred(v):
122
- bad.append(k)
123
- return bad
124
-
125
-
126
- def lint_cases(cases, file_label="<cases>"):
127
- """跑 lint,返 {file, n_cases, violations, passed}。"""
128
- violations = []
129
- if not isinstance(cases, list):
130
- return {"file": file_label, "n_cases": 0, "violations": [], "passed": True, "note": "非 list(空/读不到)→不拦"}
131
- for i, case in enumerate(cases):
132
- if _is_block_old_format(case):
133
- violations.append({
134
- "rule": "① intent+anchor 格式", "severity": "BLOCK", "idx": i,
135
- "detail": "旧 action+CSS selector 范式(Playwright 新管线不认)→ 改 intent+anchor:{role,name}",
136
- })
137
- for k in _cred_without_ask(case):
138
- violations.append({
139
- "rule": "② {{ask:}} 占位", "severity": "WARN", "idx": i,
140
- "detail": "字段 '%s' 像凭据但未用 {{ask:key}} 包裹(硬编码风险)" % k,
141
- })
142
- has_block = any(v["severity"] == "BLOCK" for v in violations)
143
- return {"file": file_label, "n_cases": len(cases),
144
- "n_violations": len(violations), "violations": violations,
145
- "passed": not has_block}
146
-
147
-
148
- def main():
149
- args = sys.argv[1:]
150
- if not args:
151
- sys.stderr.write("用法: python lint_cases.py <cases.json> [--json] | -\n")
152
- return 1
153
- src = args[0]
154
- as_json = "--json" in args
155
- cases, label = _load_cases(src)
156
- result = lint_cases(cases, label)
157
- if as_json:
158
- print(json.dumps(result, ensure_ascii=False))
159
- else:
160
- print("=== wl-test cases lint: %s ===" % label)
161
- print("cases: %d | violations: %d | passed: %s" % (
162
- result["n_cases"], result.get("n_violations", 0), result["passed"]))
163
- for v in result["violations"][:10]:
164
- print(" [%s] case#%d %s — %s" % (v["severity"], v["idx"], v["rule"], v["detail"][:70]))
165
- # BLOCK → exit 2;全过/仅 WARN / 空 → exit 0
166
- return 2 if not result["passed"] else 0
167
-
168
-
169
- if __name__ == "__main__":
170
- sys.exit(main())
@@ -1,78 +0,0 @@
1
- """test_context_cache.py — T1.3 context_cache 模块单测(纯逻辑,无需云/MCP)。
2
- 跑法: python scripts/validation/test/test_context_cache.py
3
- """
4
- import os
5
- import sys
6
- import time
7
- import tempfile
8
-
9
- _HERE = os.path.dirname(os.path.abspath(__file__))
10
- _SCRIPTS = os.path.abspath(os.path.join(_HERE, "..", "..")) # scripts/
11
- if _SCRIPTS not in sys.path and os.path.isdir(_SCRIPTS):
12
- sys.path.insert(0, _SCRIPTS)
13
-
14
- from foundation.io import context_cache as cc # noqa: E402
15
-
16
- PASSED = 0
17
- FAILED = 0
18
-
19
-
20
- def check(cond, msg):
21
- global PASSED, FAILED
22
- if cond:
23
- PASSED += 1
24
- print("OK " + msg)
25
- else:
26
- FAILED += 1
27
- print("FAIL " + msg)
28
-
29
-
30
- def main():
31
- base = tempfile.mkdtemp(prefix="ctxcache_test_")
32
-
33
- # 1. 初始 miss
34
- check(cc.get("REQ-001", "车辆保养", base=base) is None, "初始 miss")
35
-
36
- # 2. put + hit
37
- cc.put("REQ-001", "车辆保养", "CONTEXT_TEXT_HERE", base=base)
38
- check(cc.get("REQ-001", "车辆保养", base=base) == "CONTEXT_TEXT_HERE", "put 后 hit")
39
-
40
- # 3. query 归一化(首尾空格 + 大小写)命中同一桶
41
- check(cc.get("REQ-001", " 车辆保养 ", base=base) == "CONTEXT_TEXT_HERE", "query trim 命中")
42
- check(cc.get("REQ-001", "AbC", base=base) is None or True, "归一化不影响他词") # 占位,下条真测
43
- cc.put("REQ-001", "AbC", "UP", base=base)
44
- check(cc.get("REQ-001", "abc", base=base) == "UP", "query 大小写归一化命中")
45
-
46
- # 4. 不同 query / 不同 REQ 隔离
47
- check(cc.get("REQ-001", "考勤", base=base) is None, "他 query miss")
48
- check(cc.get("REQ-002", "车辆保养", base=base) is None, "他 REQ 隔离 miss")
49
-
50
- # 5. invalidate 清整个 REQ
51
- cc.invalidate("REQ-001", base=base)
52
- check(cc.get("REQ-001", "车辆保养", base=base) is None, "invalidate 后 miss")
53
- check(cc.get("REQ-001", "abc", base=base) is None, "invalidate 清全 query")
54
-
55
- # 6. TTL 过期
56
- cc.put("REQ-003", "x", "OLD", base=base)
57
- check(cc.get("REQ-003", "x", ttl=0.1, base=base) == "OLD", "TTL 内 hit")
58
- time.sleep(0.25)
59
- check(cc.get("REQ-003", "x", ttl=0.1, base=base) is None, "TTL 过期后 miss")
60
-
61
- # 7. 路径消毒(防 REQ id 注入 ../)
62
- p = cc.cache_path("REQ-evil/../../etc", base=base)
63
- rel = os.path.relpath(p, base)
64
- check(".." not in rel and "etc" not in rel.split(os.sep), "REQ id 路径消毒(无穿越)")
65
-
66
- # 8. stats
67
- cc.put("REQ-004", "a", "x", base=base)
68
- cc.put("REQ-004", "b", "y", base=base)
69
- s = cc.stats("REQ-004", base=base)
70
- check(s["entries"] == 2, "stats 条数=2")
71
- check(set(s["queries"]) == {"a", "b"}, "stats query 摘要对")
72
-
73
- print("\n%s passed, %s failed" % (PASSED, FAILED))
74
- return 0 if FAILED == 0 else 1
75
-
76
-
77
- if __name__ == "__main__":
78
- sys.exit(main())
@@ -1,60 +0,0 @@
1
- """test_lint_cases.py — T1.6 w2 lint_cases 单测(good/旧格式BLOCK/凭据WARN)。"""
2
- import os, sys
3
- _HERE = os.path.dirname(os.path.abspath(__file__))
4
- _SCRIPTS = os.path.abspath(os.path.join(_HERE, "..", "..")) # scripts/
5
- _METRICS = os.path.join(_SCRIPTS, "validation", "metrics")
6
- if _METRICS not in sys.path:
7
- sys.path.insert(0, _METRICS)
8
-
9
- import lint_cases as L # noqa
10
-
11
- P = F = 0
12
- def check(cond, msg):
13
- global P, F
14
- if cond: P += 1; print("OK ", msg)
15
- else: F += 1; print("FAIL", msg)
16
-
17
- # ① good: intent+anchor 新格式 → 过
18
- good = [{"intent":"fill","desc":"账号框","value":"{{ask:登录账号}}","anchor":{"role":"textbox","name":"管理员"}}]
19
- r = L.lint_cases(good, "good")
20
- check(r["passed"] is True and r["n_cases"] == 1, "good 新格式 intent+anchor 过")
21
- check(len(r["violations"]) == 0, "good 无违规")
22
-
23
- # ② bad: 旧 action+CSS selector → BLOCK
24
- bad = [{"action":"fill","target":"input[placeholder*='账号']","value":"admin"}]
25
- r = L.lint_cases(bad, "bad")
26
- check(r["passed"] is False, "旧 action+CSS BLOCK")
27
- check(any(v["severity"]=="BLOCK" for v in r["violations"]), "旧格式产 BLOCK 违规")
28
-
29
- # ③ CSS selector 在 target 里(即使有 intent)→ BLOCK
30
- css = [{"intent":"click","anchor":{"role":"button"},"target":"#submit-btn"}]
31
- r = L.lint_cases(css, "css")
32
- check(r["passed"] is False, "CSS selector(#id) BLOCK")
33
-
34
- # ④ 凭据字段硬编码未 {{ask:}} → WARN(不 BLOCK)
35
- cred = [{"intent":"fill","desc":"账号","anchor":{"role":"textbox"},"账号":"admin123"}]
36
- r = L.lint_cases(cred, "cred")
37
- check(r["passed"] is True, "凭据 WARN 不 BLOCK(仍过)")
38
- check(any(v["severity"]=="WARN" for v in r["violations"]), "硬编码凭据产 WARN")
39
-
40
- # ⑤ 凭据字段用 {{ask:}} → 无 WARN
41
- cred_ok = [{"intent":"fill","desc":"账号","anchor":{"role":"textbox"},"账号":"{{ask:登录账号}}"}]
42
- r = L.lint_cases(cred_ok, "cred_ok")
43
- check(len(r["violations"]) == 0, "{{ask:}} 包裹无违规")
44
-
45
- # ⑥ 空/非 list → 过(不拦)
46
- r = L.lint_cases([], "empty")
47
- check(r["passed"] is True and r["n_cases"]==0, "空 cases 过(不拦)")
48
-
49
- # ⑦ 包在 {cases:[...]} 里也能解
50
- import json
51
- # _load_cases 走文件/stdin; 直接验 lint_cases 接 list 已覆盖。验 _load_cases 的 dict 解包:
52
- class _Fake:
53
- pass
54
- # 模拟 dict 解包: lint_cases 接收 list, 但 _load_cases 应从 {cases:[...]} 取出 list
55
- # 这里直接测 lint_cases 不依赖文件 IO
56
- r = L.lint_cases([{"intent":"x","anchor":{"role":"button"}}], "dict-wrap-sim")
57
- check(r["passed"] is True, "单条新格式过")
58
-
59
- print("\n%d passed, %d failed" % (P, F))
60
- sys.exit(0 if F == 0 else 1)
@@ -1,70 +0,0 @@
1
- """test_pre_tool_use_commit.py — T1.6 w3 commit msg 门禁 hook 单测。
2
- 测 _extract_msg + 标记检测 + e2e(advisory/block/has-tag/non-git) via subprocess。
3
- """
4
- import os, sys, json, subprocess
5
- _HERE = os.path.dirname(os.path.abspath(__file__))
6
- _HOOK = os.path.abspath(os.path.join(_HERE, "..", "..", "..", "hooks", "pre-tool-use-commit.py"))
7
-
8
- P = F = 0
9
- def check(cond, msg):
10
- global P, F
11
- if cond: P += 1; print("OK ", msg)
12
- else: F += 1; print("FAIL", msg)
13
-
14
- import importlib.util
15
- spec = importlib.util.spec_from_file_location("pre_tool_use_commit", _HOOK)
16
- H = importlib.util.module_from_spec(spec)
17
- spec.loader.exec_module(H)
18
-
19
- # ① _extract_msg 各种形式
20
- check(H._extract_msg('git commit -m "fix: x"') == "fix: x", "-m 双引号")
21
- check(H._extract_msg("git commit -m 'fix: y'") == "fix: y", "-m 单引号")
22
- check(H._extract_msg('git commit --message=foo') == "foo", "--message=")
23
- check(H._extract_msg('git commit -m "title" -m "body"') == "title\nbody", "多 -m 拼接")
24
- check(H._extract_msg('git commit -am "x" --no-verify') == "x", "-am 混合 flag")
25
-
26
- # ② 标记检测
27
- check(bool(H._REQ_TAG_RE.search("fix: 漏改 [#REQ-a1b2c3d4]")), "[#REQ-uuid] 命中")
28
- check(bool(H._REQ_TAG_RE.search("feat: x [#761]")), "[#禅道号] 命中")
29
- check(not H._REQ_TAG_RE.search("fix: 漏改 无标记"), "无标记不命中")
30
-
31
- def run_hook(stdin_data, env_block=None):
32
- env = dict(os.environ)
33
- if env_block is not None:
34
- env["WLCOMMIT_GATE_BLOCK"] = env_block
35
- else:
36
- env.pop("WLCOMMIT_GATE_BLOCK", None)
37
- r = subprocess.run([sys.executable, _HOOK], input=json.dumps(stdin_data),
38
- capture_output=True, text=True, env=env, timeout=15)
39
- return r.returncode, r.stdout, r.stderr
40
-
41
- def mk(cmd): return {"tool_name": "Bash", "tool_input": {"command": cmd}}
42
-
43
- # ③ e2e advisory(默认): 无标记 → exit 0 + stdout 提醒
44
- rc, out, err = run_hook(mk('git commit -m "fix: 漏改无标记"'))
45
- check(rc == 0, "advisory 无标记 exit 0(不阻断)")
46
- check("提醒" in out, "advisory 产 stdout 提醒")
47
-
48
- # ④ e2e block(env=1): 无标记 → exit 2
49
- rc, out, err = run_hook(mk('git commit -m "fix: 漏改无标记"'), env_block="1")
50
- check(rc == 2, "BLOCK 模式(env=1) 无标记 exit 2")
51
- check("硬拦" in err, "BLOCK 产 stderr 硬拦语")
52
-
53
- # ⑤ e2e 有标记 → exit 0 无提醒(任何模式)
54
- rc, out, err = run_hook(mk('git commit -m "fix: x [#REQ-a1b2c3d4]"'), env_block="1")
55
- check(rc == 0 and "提醒" not in out, "有标记放行(BLOCK 模式也不拦)")
56
-
57
- # ⑥ 非 git commit 命令 → exit 0 静默
58
- rc, out, err = run_hook(mk('git push origin main'))
59
- check(rc == 0 and out == "" and err == "", "非 git commit 静默放行")
60
-
61
- # ⑦ 非 Bash 工具 → exit 0
62
- rc, out, err = run_hook({"tool_name": "Write", "tool_input": {"file_path": "x.py"}})
63
- check(rc == 0, "非 Bash 工具放行")
64
-
65
- # ⑧ platform/dev commit(无标记,advisory)也不硬断 → exit 0(不误拦其他)
66
- rc, out, err = run_hook(mk('git commit -m "chore: 改文档"'))
67
- check(rc == 0, "advisory 默认不误拦工具/文档 commit")
68
-
69
- print("\n%d passed, %d failed" % (P, F))
70
- sys.exit(0 if F == 0 else 1)