@hupan56/wlkj 3.3.8 → 3.3.9

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 (120) hide show
  1. package/package.json +1 -1
  2. package/templates/qoder/commands/optional/wl-spec.md +12 -2
  3. package/templates/qoder/commands/optional/wl-status.md +8 -0
  4. package/templates/qoder/commands/wl-code.md +8 -0
  5. package/templates/qoder/commands/wl-commit.md +11 -1
  6. package/templates/qoder/commands/wl-data.md +46 -0
  7. package/templates/qoder/commands/wl-fix.md +47 -0
  8. package/templates/qoder/commands/wl-init.md +2 -2
  9. package/templates/qoder/commands/wl-knowledge.md +41 -0
  10. package/templates/qoder/commands/wl-prd.md +23 -22
  11. package/templates/qoder/commands/wl-review.md +49 -0
  12. package/templates/qoder/commands/wl-search.md +23 -27
  13. package/templates/qoder/commands/wl-task.md +11 -18
  14. package/templates/qoder/commands/wl-test.md +8 -0
  15. package/templates/qoder/contracts/insight.md +55 -0
  16. package/templates/qoder/hooks/__pycache__/pre-tool-use-commit.cpython-39.pyc +0 -0
  17. package/templates/qoder/hooks/__pycache__/pre-tool-use.cpython-39.pyc +0 -0
  18. package/templates/qoder/hooks/__pycache__/session-start.cpython-39.pyc +0 -0
  19. package/templates/qoder/hooks/__pycache__/stop-eval.cpython-39.pyc +0 -0
  20. package/templates/qoder/hooks/pre-tool-use-commit.py +124 -0
  21. package/templates/qoder/hooks/session-start.py +20 -1
  22. package/templates/qoder/hooks/stop-eval.py +129 -0
  23. package/templates/qoder/scripts/capability/adapters/mcp.py +1 -0
  24. package/templates/qoder/scripts/capability/registry.py +0 -3
  25. package/templates/qoder/scripts/domain/integration/return_to_platform.py +1 -0
  26. package/templates/qoder/scripts/domain/integration/spec_upload.py +3 -3
  27. package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +12 -12
  28. package/templates/qoder/scripts/domain/kg/build/build_relations.py +12 -12
  29. package/templates/qoder/scripts/foundation/__pycache__/__init__.cpython-39.pyc +0 -0
  30. package/templates/qoder/scripts/foundation/io/__pycache__/__init__.cpython-39.pyc +0 -0
  31. package/templates/qoder/scripts/foundation/io/__pycache__/context_cache.cpython-39.pyc +0 -0
  32. package/templates/qoder/scripts/foundation/io/context_cache.py +94 -0
  33. package/templates/qoder/scripts/orchestration/__pycache__/wlkj.cpython-39.pyc +0 -0
  34. package/templates/qoder/scripts/orchestration/wlkj.py +95 -0
  35. package/templates/qoder/scripts/tool_guide.md +70 -0
  36. package/templates/qoder/scripts/validation/eval/__pycache__/alignment_matrix.cpython-39.pyc +0 -0
  37. package/templates/qoder/scripts/validation/eval/__pycache__/bf2_content_fidelity.cpython-39.pyc +0 -0
  38. package/templates/qoder/scripts/validation/eval/__pycache__/bf2_llmjudge.cpython-39.pyc +0 -0
  39. package/templates/qoder/scripts/validation/eval/__pycache__/bf_score.cpython-39.pyc +0 -0
  40. package/templates/qoder/scripts/validation/eval/__pycache__/code_flywheel.cpython-39.pyc +0 -0
  41. package/templates/qoder/scripts/validation/eval/__pycache__/dispatcher_ab.cpython-39.pyc +0 -0
  42. package/templates/qoder/scripts/validation/eval/__pycache__/feature_fidelity_flywheel.cpython-39.pyc +0 -0
  43. package/templates/qoder/scripts/validation/eval/__pycache__/gradient_matrix.cpython-39.pyc +0 -0
  44. package/templates/qoder/scripts/validation/eval/__pycache__/metrics_dashboard.cpython-39.pyc +0 -0
  45. package/templates/qoder/scripts/validation/eval/__pycache__/multi_turn_flywheel.cpython-39.pyc +0 -0
  46. package/templates/qoder/scripts/validation/eval/__pycache__/prd_fidelity_flywheel.cpython-39.pyc +0 -0
  47. package/templates/qoder/scripts/validation/eval/__pycache__/prd_flywheel.cpython-39.pyc +0 -0
  48. package/templates/qoder/scripts/validation/eval/__pycache__/prototype_fidelity_flywheel.cpython-39.pyc +0 -0
  49. package/templates/qoder/scripts/validation/eval/__pycache__/recall_flywheel.cpython-39.pyc +0 -0
  50. package/templates/qoder/scripts/validation/eval/__pycache__/robustness_flywheel.cpython-39.pyc +0 -0
  51. package/templates/qoder/scripts/validation/eval/__pycache__/speed_accuracy_flywheel.cpython-39.pyc +0 -0
  52. package/templates/qoder/scripts/validation/eval/__pycache__/task_flywheel.cpython-39.pyc +0 -0
  53. package/templates/qoder/scripts/validation/eval/__pycache__/token_flywheel.cpython-39.pyc +0 -0
  54. package/templates/qoder/scripts/validation/eval/alignment_matrix.py +176 -0
  55. package/templates/qoder/scripts/validation/eval/bf2_content_fidelity.py +110 -0
  56. package/templates/qoder/scripts/validation/eval/bf2_llmjudge.py +104 -0
  57. package/templates/qoder/scripts/validation/eval/bf_score.py +218 -0
  58. package/templates/qoder/scripts/validation/eval/code_flywheel.py +150 -0
  59. package/templates/qoder/scripts/validation/eval/dispatcher_ab.py +156 -0
  60. package/templates/qoder/scripts/validation/eval/dispatcher_ab_2026-07-21.json +23 -0
  61. package/templates/qoder/scripts/validation/eval/feature_fidelity_flywheel.py +143 -0
  62. package/templates/qoder/scripts/validation/eval/gradient_matrix.py +261 -0
  63. package/templates/qoder/scripts/validation/eval/gradient_matrix_baseline_2026-07-21.json +33 -0
  64. package/templates/qoder/scripts/validation/eval/metrics_dashboard.py +105 -0
  65. package/templates/qoder/scripts/validation/eval/multi_turn_flywheel.py +118 -0
  66. package/templates/qoder/scripts/validation/eval/prd_fidelity_flywheel.py +128 -0
  67. package/templates/qoder/scripts/validation/eval/prd_flywheel.py +148 -0
  68. package/templates/qoder/scripts/validation/eval/prototype_fidelity_flywheel.py +166 -0
  69. package/templates/qoder/scripts/validation/eval/recall_flywheel.py +148 -0
  70. package/templates/qoder/scripts/validation/eval/robustness_flywheel.py +139 -0
  71. package/templates/qoder/scripts/validation/eval/speed_accuracy_flywheel.py +188 -0
  72. package/templates/qoder/scripts/validation/eval/task_flywheel.py +124 -0
  73. package/templates/qoder/scripts/validation/eval/token_flywheel.py +88 -0
  74. package/templates/qoder/scripts/validation/metrics/__pycache__/lint_cases.cpython-39.pyc +0 -0
  75. package/templates/qoder/scripts/validation/metrics/eval_code_ac.py +177 -0
  76. package/templates/qoder/scripts/validation/metrics/lint_cases.py +170 -0
  77. package/templates/qoder/scripts/validation/test/__pycache__/test_context_cache.cpython-39.pyc +0 -0
  78. package/templates/qoder/scripts/validation/test/test_context_cache.py +78 -0
  79. package/templates/qoder/scripts/validation/test/test_lint_cases.py +60 -0
  80. package/templates/qoder/scripts/validation/test/test_pre_tool_use_commit.py +70 -0
  81. package/templates/qoder/settings.json +8 -0
  82. package/templates/qoder/skills/wl-spec/SKILL.md +1 -1
  83. package/templates/workspace/specs/REQ-TEST-CTX/.context-cache.json +1 -0
  84. package/templates/qoder/agents/design-agent.md +0 -20
  85. package/templates/qoder/agents/spec-generator.md +0 -21
  86. package/templates/qoder/scripts/capability/smoke_test_report.json.new +0 -94
  87. package/templates/qoder/scripts/domain/kg/extract/asset/__init__.py +0 -10
  88. package/templates/qoder/scripts/domain/kg/extract/asset/asset_tree.py +0 -57
  89. package/templates/qoder/scripts/domain/kg/extract/asset/discussion_importer.py +0 -62
  90. package/templates/qoder/scripts/domain/kg/extract/asset/prd_importer.py +0 -146
  91. package/templates/qoder/scripts/domain/kg/extract/asset/prototype_importer.py +0 -64
  92. package/templates/qoder/scripts/domain/kg/extract/asset/returns_importer.py +0 -52
  93. package/templates/qoder/scripts/domain/kg/extract/build_goal3.py +0 -104
  94. package/templates/qoder/scripts/domain/kg/extract/build_goal4.py +0 -55
  95. package/templates/qoder/scripts/domain/kg/extract/build_goal5.py +0 -95
  96. package/templates/qoder/scripts/domain/kg/extract/db/__init__.py +0 -8
  97. package/templates/qoder/scripts/domain/kg/extract/db/data_profile.py +0 -22
  98. package/templates/qoder/scripts/domain/kg/extract/db/fk_extractor.py +0 -55
  99. package/templates/qoder/scripts/domain/kg/extract/db/schema_extractor.py +0 -90
  100. package/templates/qoder/scripts/domain/kg/extract/inference/__init__.py +0 -9
  101. package/templates/qoder/scripts/domain/kg/extract/inference/community_summarizer.py +0 -206
  102. package/templates/qoder/scripts/domain/kg/extract/inference/embed_builder.py +0 -132
  103. package/templates/qoder/scripts/domain/kg/extract/inference/naming_matcher.py +0 -80
  104. package/templates/qoder/scripts/domain/kg/extract/inference/promote.py +0 -59
  105. package/templates/qoder/scripts/domain/kg/extract/inference/recompute.py +0 -93
  106. package/templates/qoder/scripts/domain/kg/extract/inference/weak_link.py +0 -421
  107. package/templates/qoder/scripts/domain/kg/extract/mybatis/__init__.py +0 -9
  108. package/templates/qoder/scripts/domain/kg/extract/mybatis/all.py +0 -79
  109. package/templates/qoder/scripts/domain/kg/extract/mybatis/mapper_parser.py +0 -99
  110. package/templates/qoder/scripts/domain/kg/extract/mybatis/relation_builder.py +0 -69
  111. package/templates/qoder/scripts/domain/kg/extract/mybatis/sql_extractor.py +0 -78
  112. package/templates/qoder/scripts/domain/kg/extract/prd/__init__.py +0 -8
  113. package/templates/qoder/scripts/domain/kg/extract/prd/prd_chunk_embed.py +0 -105
  114. package/templates/qoder/scripts/domain/kg/extract/prd/prd_llm_extract.py +0 -153
  115. package/templates/qoder/scripts/domain/kg/extract/prd/req_anchor.py +0 -120
  116. package/templates/qoder/scripts/domain/kg/graph/kg_link_db.py +0 -235
  117. package/templates/qoder/scripts/domain/kg/search/enrich_prompt.py +0 -238
  118. package/templates/qoder/scripts/domain/kg/server/perf_bench.py +0 -197
  119. package/templates/qoder/scripts/domain/kg/sync_repowiki.py +0 -109
  120. package/templates/qoder/scripts/engine/poller.py +0 -219
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """pre-tool-use-commit.py - P0-3 w3: wl-commit msg 门禁(PreToolUse on Bash git commit)
4
+
5
+ stop-eval.py 里 _enforce_commit_msg_gate 的**正确落点**:pre-tool-use(commit 执行前验 msg),
6
+ 而非 stop-eval(后者 git log -1 不分"本轮 wl-commit" vs 平台开发/工具 commit → 误拦)。
7
+
8
+ 做法(确定性 + 不误拦其他):
9
+ 1. PreToolUse 命中 Bash + command 含 "git commit"
10
+ 2. 提取 -m 消息(兼容 -m "x" / -m 'x' / --message=x / 多 -m)
11
+ 3. 查需求关联标记 [#REQ-<uuid>] (S3) 或 [#<禅道号>] (S5.1)
12
+ 4. 缺标记:
13
+ - 默认 ADVISORY(exit 0 + stdout 强提醒)—— 不阻断,避免误拦工具/平台/文档 commit
14
+ - 环境变量 WLCOMMIT_GATE_BLOCK=1 时 → exit 2 硬拦(wl-commit 强流程场景才开)
15
+
16
+ 铁律:永远 exit 0(除非 BLOCK 模式显式开);失败 try/except 静默(hook 崩绝不阻塞)。
17
+ 非 git commit 命令 / 非 Bash → exit 0。
18
+ """
19
+ import json
20
+ import os
21
+ import re
22
+ import sys
23
+
24
+ if sys.platform == 'win32':
25
+ try:
26
+ sys.stdout.reconfigure(encoding='utf-8')
27
+ sys.stderr.reconfigure(encoding='utf-8')
28
+ except Exception:
29
+ pass
30
+
31
+ NL = chr(10)
32
+ BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
33
+ HOOK_ERRORS_LOG = os.path.join(BASE, '.qoder', '.runtime', 'hook-errors.log')
34
+
35
+ # 需求关联标记:[#REQ-xxx] 或 [#123](禅道号)
36
+ _REQ_TAG_RE = re.compile(r'\[#REQ-[\w-]+\]|\[#\d+\]')
37
+ # 提取 git commit -m 的消息(兼容引号/多段/--message= / 组合短旗 -am)
38
+ # (?:^|\s)-[a-zA-Z]*m 匹配 -m / -am / -cm 等(单 dash 短旗组合;空白前缀避 --message 误匹)
39
+ _M_QUOTED = re.compile(r'''(?:^|\s)-[a-zA-Z]*m\s*(['"])(.*?)\1''', re.S)
40
+ _M_BARE = re.compile(r'(?:^|\s)-[a-zA-Z]*m\s+(\S.*?)\s*(?=(?:^|\s)-[a-zA-Z]*m\s|$)', re.S)
41
+ _MSG_EQ = re.compile(r'(?:^|\s)--message=(\S.*?)\s*(?:--|$)', re.S)
42
+
43
+
44
+ def _read_stdin():
45
+ try:
46
+ raw = sys.stdin.read()
47
+ if raw and raw.strip():
48
+ return json.loads(raw)
49
+ except Exception:
50
+ pass
51
+ return {}
52
+
53
+
54
+ def _extract_msg(command):
55
+ """从 git commit 命令里提取 commit message(拼多 -m)。提不到返 ''。"""
56
+ if "--message=" in command:
57
+ m = _MSG_EQ.search(command)
58
+ if m:
59
+ return m.group(1).strip()
60
+ # 所有 -m "..." / -m '...' 段拼接
61
+ parts = _M_QUOTED.findall(command) # [(quote, msg), ...]
62
+ if parts:
63
+ return "\n".join(p[1] for p in parts).strip()
64
+ # 裸 -m word(无引号)
65
+ m = _M_BARE.search(command)
66
+ if m:
67
+ return m.group(1).strip()
68
+ return ""
69
+
70
+
71
+ def _log_err(line):
72
+ from datetime import datetime
73
+ try:
74
+ os.makedirs(os.path.dirname(HOOK_ERRORS_LOG), exist_ok=True)
75
+ with open(HOOK_ERRORS_LOG, 'a', encoding='utf-8') as f:
76
+ f.write('[{}] pre-tool-use-commit: {}{}'.format(
77
+ datetime.now().strftime('%Y-%m-%d %H:%M:%S'), str(line)[:200], NL))
78
+ except Exception:
79
+ pass
80
+
81
+
82
+ def main():
83
+ data = _read_stdin()
84
+ tool = data.get("tool_name") or data.get("tool") or ""
85
+ if tool.lower() not in ("bash", "shell", "execute_bash"):
86
+ sys.exit(0)
87
+ tinput = data.get("tool_input") or data.get("input") or {}
88
+ command = tinput.get("command") or tinput.get("cmd") or ""
89
+ if not isinstance(command, str) or "git commit" not in command:
90
+ sys.exit(0)
91
+ # 跳过 --amend 无 -m / 自动生成(如 merge commit)—— 只验带 -m 的显式 commit
92
+ msg = _extract_msg(command)
93
+ if not msg:
94
+ sys.exit(0) # 提不到 -m(可能 -F 文件或 amend),不拦
95
+ if _REQ_TAG_RE.search(msg):
96
+ sys.exit(0) # 含需求标记,放行
97
+
98
+ # 缺标记
99
+ block_mode = os.environ.get("WLCOMMIT_GATE_BLOCK", "").lower() in ("1", "true", "yes")
100
+ preview = msg.replace("\n", " ")[:100]
101
+ if block_mode:
102
+ sys.stderr.write(
103
+ '【wl-commit msg 门禁·硬拦】commit msg 缺需求关联标记:\n %s\n'
104
+ '修复: git commit --amend 补 [#REQ-<uuid>] (S3) 或 [#<禅道号>] (S5.1 push 后)。\n'
105
+ '(此硬拦由 WLCOMMIT_GATE_BLOCK=1 开启;平台/工具/文档 commit 关该 env 或走非 wl-commit 流程。)\n'
106
+ % preview)
107
+ sys.exit(2)
108
+ # 默认 advisory:不阻断,强提醒
109
+ sys.stdout.write(
110
+ '⚠️ 【wl-commit msg 提醒】本次 commit msg 缺需求关联标记 [#REQ-xxx]/[#禅道号]:\n'
111
+ ' %s\n'
112
+ '若这是 /wl-commit 流程,建议 amend 补标记关联需求(F1 闭环数据);'
113
+ '若是平台/工具/文档 commit 可忽略本提醒。\n' % preview)
114
+ sys.exit(0)
115
+
116
+
117
+ if __name__ == "__main__":
118
+ try:
119
+ main()
120
+ except SystemExit:
121
+ raise
122
+ except Exception as e:
123
+ _log_err("fatal: %s" % str(e)[:150])
124
+ sys.exit(0) # 永不阻塞
@@ -129,7 +129,7 @@ def get_index_info():
129
129
  recent = errors[-10:] # 取最后 10 行检查
130
130
  if len(recent) >= 3:
131
131
  lines.append('[WARN] hook 最近频繁失败 (%d 条错误), 详情见 .qoder/.runtime/hook-errors.log' % len(recent))
132
- lines.append(' 跑 python .qoder/scripts/setup/init_doctor.py --fix 排查')
132
+ lines.append(' 跑 python .qoder/scripts/orchestration/wlkj.py doctor --fix 排查')
133
133
  except OSError:
134
134
  pass
135
135
 
@@ -289,6 +289,22 @@ def get_usability_summary():
289
289
  return NL.join(lines) if lines else ''
290
290
 
291
291
 
292
+ def get_tool_guide():
293
+ """注入 tool_guide 精简版(共用工具用法关键执行位), 命令正文不再重复(单一信源, T1.2)。
294
+ 完整版见 .qoder/scripts/tool_guide.md。纯静态文本, 失败绝不阻塞。"""
295
+ return NL.join([
296
+ '## Tool Guide (共用工具用法 · 完整见 .qoder/scripts/tool_guide.md)',
297
+ '环境自检(任何脚本前先跑, 定 R/$PY):',
298
+ ' R=$(python ~/.qoderwork/repo_root.py 2>/dev/null || python3 ~/.qoderwork/repo_root.py 2>/dev/null) || R=.',
299
+ ' PY=$(python --version >/dev/null 2>&1 && echo python || echo python3)',
300
+ ' 后续统一 $PY "$R/.qoder/scripts/orchestration/wlkj.py" <命令>',
301
+ '接口三套名互通: wlkj.py / kg.py / MCP 名字都认(search/semantic/context/impact), 别因"未知子命令"慌',
302
+ '中文歧义词先提炼再查: 保险->车险/商业险/交强险 · 异常->告警/报错 · 计划->作业/养护',
303
+ '禁全盘扫: 不要 find / grep -r / findstr /s / os.walk data/code, 那是知识层 search 的活',
304
+ '优先级口诀: 精确搜用Grep · 语义找用rag_search · 取全用context_pack · 查库先list_envs · 影响用get_impact',
305
+ ])
306
+
307
+
292
308
  def main():
293
309
  parts = []
294
310
  parts.append('<qoder-context>')
@@ -345,6 +361,9 @@ def main():
345
361
  parts.append('')
346
362
  parts.append(style_info)
347
363
 
364
+ parts.append('')
365
+ parts.append(get_tool_guide())
366
+
348
367
  parts.append('</qoder-context>')
349
368
  # Hook-1: 健康标记 — AI 能感知 hook 是否真的跑了
350
369
  # QoderWork/Quest 里看不到 [hook-ok] 就知道 hook 没生效, 要自取上下文
@@ -179,6 +179,127 @@ def _last_record_by_prd():
179
179
  return last_by_prd
180
180
 
181
181
 
182
+ def _enforce_code_ac_gate():
183
+ """P0-3: wl-code spec AC 门禁。找近 spec + eval_code_ac.py 对账, AC<100% 阻断。
184
+
185
+ 治"代码漏实现 spec AC"(wl-code 自检全打勾反例)。无 spec / 脚本缺失 / 崩溃 → 放行(不阻塞)。
186
+ """
187
+ import subprocess
188
+ qoder_script = os.path.join(BASE, '.qoder', 'scripts', 'validation', 'metrics', 'eval_code_ac.py')
189
+ pkg_script = os.path.join(BASE, 'packages', 'wlkj', 'templates', 'qoder',
190
+ 'scripts', 'validation', 'metrics', 'eval_code_ac.py')
191
+ script = qoder_script if os.path.isfile(qoder_script) else pkg_script
192
+ if not os.path.isfile(script):
193
+ return # 脚本不在, 放行
194
+ try:
195
+ r = subprocess.run([sys.executable, script], cwd=BASE,
196
+ capture_output=True, text=True, timeout=30)
197
+ except Exception:
198
+ return # 崩溃绝不阻塞
199
+ if r.returncode != 2:
200
+ return # exit 0 = 无 spec / AC 全覆盖, 放行
201
+ out_lines = [l for l in (r.stdout or '').split('\n') if l.strip()]
202
+ try:
203
+ data = json.loads(out_lines[-1]) if out_lines else {}
204
+ except Exception:
205
+ data = {}
206
+ missed = data.get('missed', []) or []
207
+ rate = data.get('rate', 0)
208
+ miss_str = '\n'.join(' - ' + str(m)[:80] for m in missed[:5])
209
+ sys.stderr.write(
210
+ '【wl-code AC 门禁】检测到 spec 验收标准未被代码覆盖 (覆盖率 %s%%):\n%s\n'
211
+ '修复: 对照 spec 第6章 PRD 验收标准映射表, 把未实现的 AC 补进代码, 重跑至 100%%。\n'
212
+ % (int(rate * 100), miss_str))
213
+ sys.exit(2)
214
+
215
+
216
+ def _enforce_cases_lint_gate():
217
+ """P0-3 w2: wl-test cases 格式门禁。扫近 10min cases 文件(含 intent/action/anchor 键的 json),
218
+ 跑 lint_cases.py,旧格式(action+CSS selector) → exit 2。无 cases/脚本缺/崩溃 → 放行(不阻塞)。"""
219
+ import subprocess
220
+ qoder_script = os.path.join(BASE, '.qoder', 'scripts', 'validation', 'metrics', 'lint_cases.py')
221
+ pkg_script = os.path.join(BASE, 'packages', 'wlkj', 'templates', 'qoder',
222
+ 'scripts', 'validation', 'metrics', 'lint_cases.py')
223
+ script = qoder_script if os.path.isfile(qoder_script) else pkg_script
224
+ if not os.path.isfile(script):
225
+ return
226
+ now = time.time()
227
+ cases_files = []
228
+ for root in (os.path.join(BASE, 'workspace', 'members'), os.path.join(BASE, 'workspace', 'tasks')):
229
+ if not os.path.isdir(root):
230
+ continue
231
+ for dirpath, dirs, files in os.walk(root):
232
+ dirs[:] = [d for d in dirs if d not in ('node_modules', '.git', '__pycache__', '.secrets')]
233
+ for fn in files:
234
+ if not fn.endswith('.json'):
235
+ continue
236
+ fp = os.path.join(dirpath, fn)
237
+ try:
238
+ if now - os.path.getmtime(fp) > 600:
239
+ continue
240
+ except OSError:
241
+ continue
242
+ try:
243
+ head = open(fp, encoding='utf-8', errors='replace').read(2000)
244
+ except Exception:
245
+ continue
246
+ if '"intent"' in head or '"action"' in head or '"anchor"' in head:
247
+ cases_files.append(fp)
248
+ for fp in cases_files[:5]:
249
+ try:
250
+ r = subprocess.run([sys.executable, script, fp, '--json'], cwd=BASE,
251
+ capture_output=True, text=True, timeout=15)
252
+ except Exception:
253
+ continue
254
+ if r.returncode != 2:
255
+ continue
256
+ out_lines = [l for l in (r.stdout or '').split('\n') if l.strip()]
257
+ try:
258
+ data = json.loads(out_lines[-1]) if out_lines else {}
259
+ except Exception:
260
+ data = {}
261
+ blocks = [v for v in (data.get('violations') or []) if v.get('severity') == 'BLOCK']
262
+ if blocks:
263
+ detail = '\n'.join(' - case#%s %s' % (v.get('idx'), str(v.get('detail', ''))[:60]) for v in blocks[:5])
264
+ sys.stderr.write(
265
+ '【wl-test cases 门禁】%s 检测到旧格式用例(action+CSS selector,新 Playwright 管线不认):\n%s\n'
266
+ '修复: 改 intent+anchor 格式,anchor={role,name}(来自 recall/snapshot),凭据用 {{ask:key}}。\n'
267
+ % (os.path.basename(fp), detail))
268
+ sys.exit(2)
269
+
270
+
271
+ def _enforce_commit_msg_gate():
272
+ """P0-3 w3: wl-commit msg 门禁。近 30min commit msg 须含 [#REQ-xxx] 或 [#禅道号]。
273
+
274
+ 治"commit 不关联需求"(F1 闭环数据断)。memory commit-convention-zentao-id:
275
+ S3 [#REQ-<uuid8>] / S5.1 push 后 [#<禅道号>]。无标记 → exit 2 提示 amend。
276
+ """
277
+ import re as _re
278
+ import subprocess
279
+ try:
280
+ r = subprocess.run(['git', 'log', '-1', '--format=%B|||%ct'], cwd=BASE,
281
+ capture_output=True, text=True, timeout=10)
282
+ out = (r.stdout or '').strip()
283
+ except Exception:
284
+ return # 崩溃不阻塞
285
+ if '|||' not in out:
286
+ return
287
+ msg, _, ts_s = out.rpartition('|||')
288
+ try:
289
+ ts = int(ts_s)
290
+ except ValueError:
291
+ ts = 0
292
+ if ts and time.time() - ts > 1800:
293
+ return # 非本轮 commit
294
+ if _re.search(r'\[#REQ-[\w-]+\]|\[#\d+\]', msg):
295
+ return # 含需求标记, 放行
296
+ sys.stderr.write(
297
+ '【wl-commit msg 门禁】最近 commit msg 缺需求关联标记:\n %s\n'
298
+ '修复: git commit --amend 补 [#REQ-<uuid>] (S3) 或 [#<禅道号>] (S5.1 push 后)。\n'
299
+ % msg.replace('\n', ' ')[:120])
300
+ sys.exit(2)
301
+
302
+
182
303
  def main():
183
304
  try:
184
305
  data = _read_stdin()
@@ -186,6 +307,14 @@ def main():
186
307
  if _is_reentry(data):
187
308
  sys.exit(0)
188
309
 
310
+ # ★ P0-3 wl-code spec AC 门禁(代码漏实现 AC 检测, 治自检全打勾)
311
+ _enforce_code_ac_gate()
312
+ # P0-3 w2 wl-test cases 格式门禁(旧 action+CSS selector → 拦)
313
+ _enforce_cases_lint_gate()
314
+ # w3 wl-commit msg 门禁: 应在 pre-tool-use(git commit 命令前验 msg)而非 stop-eval——
315
+ # stop-eval 的 git log -1 不分"本轮 wl-commit"vs 其他 commit, 会在平台开发等非 wl-commit
316
+ # 场景误拦。函数 _enforce_commit_msg_gate 已定义待 pre-tool-use 细化接入。
317
+
189
318
  # 找近 10 分钟产出的 PRD, 检查是否已【通过】门禁
190
319
  recent = _recent_prd_files()
191
320
  if not recent:
@@ -21,6 +21,7 @@ McpMemoryProvider:memory 职责链的 MCP provider,接通 kg.duckdb learning
21
21
  """
22
22
  from __future__ import annotations
23
23
 
24
+ import json
24
25
  from typing import Any, Optional
25
26
 
26
27
  from ..registry_mcp import McpRegistry, _load_servers
@@ -17,10 +17,7 @@ import os
17
17
  from typing import Any
18
18
 
19
19
  from .caps import (
20
- NoOpCronCap,
21
- NoOpNotifyCap,
22
20
  NoOpPresentCap,
23
- NoOpSandboxCap,
24
21
  )
25
22
 
26
23
 
@@ -35,6 +35,7 @@ def _load_mcp_config() -> dict:
35
35
  except Exception:
36
36
  repo = Path(__file__).resolve().parent.parent.parent.parent.parent
37
37
  for cand in [
38
+ repo / ".qoder" / "mcp_config.json", # ★ switch_project 写这里,优先查(否则读不到 token → 回流误报"无 X-Engine-Token")
38
39
  repo / "mcp_config.json",
39
40
  ]:
40
41
  try:
@@ -5,7 +5,7 @@
5
5
  契约: wlinkj-workspace/docs/SPEC-MCP-CONTRACT.md v1(create/confirm/update/get_spec)
6
6
 
7
7
  流程: 解析契约头 → 有platform_spec_id走update/无走create → confirm → 回写幂等锚
8
- 传输: POST /mcp direct JSON-RPC + X-MCP-Token(同 engine/poller.py:_mcp_call)
8
+ 传输: POST /mcp direct JSON-RPC + X-MCP-Token
9
9
  """
10
10
  from __future__ import annotations
11
11
  import json, os, re, sys, urllib.request, urllib.error
@@ -24,8 +24,8 @@ def _load_mcp_config() -> dict:
24
24
  repo = get_repo_root()
25
25
  except Exception:
26
26
  repo = Path(__file__).resolve().parent.parent.parent.parent.parent
27
- for cand in [repo / "mcp_config.json",
28
- repo / ".qoder" / "mcp_config.json"]:
27
+ for cand in [repo / ".qoder" / "mcp_config.json", # ★ switch_project 写这里,优先(同 return_to_platform,否则可能读到 repo/ 旧文件)
28
+ repo / "mcp_config.json"]:
29
29
  try:
30
30
  if cand.is_file():
31
31
  with open(cand, encoding="utf-8") as f:
@@ -1,17 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """build_entity_registry.py — 从 ui-modules + code-api 构建 entity-registry.json。
3
3
 
4
- # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
5
- import os as _o, sys as _s
6
- _f = _o.path.abspath(__file__)
7
- for _ in range(10):
8
- _f = _o.path.dirname(_f)
9
- _cp = _o.path.join(_f, 'foundation')
10
- if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
11
- break
12
- if _cp not in _s.path: _s.path.insert(0, _cp)
13
- from bootstrap import setup; setup()
14
-
15
4
  产出 data/index/_build_cache/entity-registry.json, 供 kg_duckdb 导入 entities/aliases 表,
16
5
  让 kg.py feature <功能名> 能返回功能画像。
17
6
 
@@ -34,6 +23,18 @@ from bootstrap import setup; setup()
34
23
  业务流程) 由 build_relations.py/build_features.py/build_workflows.py 补充,
35
24
  那些脚本未实现时不影响本脚本的 entity 基础层。
36
25
  """
26
+ # v3.0 路径自举: 引导到 foundation/bootstrap, 统一 sys.path 逻辑
27
+ # ⚠ 必须在 module docstring 之外! 旧版把这块包进 docstring → bootstrap 永不执行 → 跑死。
28
+ import os as _o, sys as _s
29
+ _f = _o.path.abspath(__file__)
30
+ for _ in range(10):
31
+ _f = _o.path.dirname(_f)
32
+ _cp = _o.path.join(_f, 'foundation')
33
+ if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
34
+ break
35
+ if _cp not in _s.path: _s.path.insert(0, _cp)
36
+ from bootstrap import setup; setup()
37
+
37
38
  import json
38
39
  import os
39
40
  import sys
@@ -41,7 +42,6 @@ from datetime import datetime
41
42
 
42
43
  # 路径自适应 (和兄弟脚本一致)
43
44
  THIS_DIR = os.path.dirname(os.path.abspath(__file__))
44
- sys.path.insert(0, os.path.join(THIS_DIR, '..'))
45
45
  from foundation.core.paths import DATA_INDEX_DIR, BUILD_CACHE_DIR
46
46
 
47
47
 
@@ -1,17 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """build_relations.py — 从 trace-chain + code-api 推导关系边, 生成 relations.json。
3
3
 
4
- # v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
5
- import os as _o, sys as _s
6
- _f = _o.path.abspath(__file__)
7
- for _ in range(10):
8
- _f = _o.path.dirname(_f)
9
- _cp = _o.path.join(_f, 'foundation')
10
- if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
11
- break
12
- if _cp not in _s.path: _s.path.insert(0, _cp)
13
- from bootstrap import setup; setup()
14
-
15
4
  产出 data/index/_build_cache/relations.json, 供 kg_duckdb 导入 edges 表,
16
5
  让 kg.py feature 的端点/按钮数据非零, kg.py hop 多跳遍历有边可走。
17
6
 
@@ -32,13 +21,24 @@ from bootstrap import setup; setup()
32
21
  - code-api.json: {endpoint: controller_file} (api_to_controller 直接用)
33
22
  - test-assertions.json: tests (controller_to_tests 推导)
34
23
  """
24
+ # v3.0 路径自举: 引导到 foundation/bootstrap, 统一 sys.path 逻辑
25
+ # ⚠ 必须在 module docstring 之外! 旧版把这块包进 docstring → bootstrap 永不执行 → 跑死。
26
+ import os as _o, sys as _s
27
+ _f = _o.path.abspath(__file__)
28
+ for _ in range(10):
29
+ _f = _o.path.dirname(_f)
30
+ _cp = _o.path.join(_f, 'foundation')
31
+ if _o.path.isfile(_o.path.join(_cp, 'bootstrap.py')):
32
+ break
33
+ if _cp not in _s.path: _s.path.insert(0, _cp)
34
+ from bootstrap import setup; setup()
35
+
35
36
  import json
36
37
  import os
37
38
  import sys
38
39
  from datetime import datetime
39
40
 
40
41
  THIS_DIR = os.path.dirname(os.path.abspath(__file__))
41
- sys.path.insert(0, os.path.join(THIS_DIR, '..'))
42
42
  from foundation.core.paths import DATA_INDEX_DIR, BUILD_CACHE_DIR
43
43
 
44
44
 
@@ -0,0 +1,94 @@
1
+ """context_cache.py — 按 REQ-id 缓存 context_pack/rag_search 结果(T1.3 跨命令上下文缓存)。
2
+
3
+ PRD→spec→code 链路里同一 REQ 反复拉同一份上下文(3 次 KG/MCP 往返)→ 缓存复用,省 token + 减反复搜。
4
+
5
+ 存储: workspace/specs/{REQ}/.context-cache.json
6
+ 结构: {query_hash: {"query":..., "text":..., "ts":...}}
7
+ TTL 默认 24h(PRD/spec 改动后用 invalidate(req) 显式失效)。
8
+
9
+ 纯模块,无外部依赖;任何异常 fail-open(缓存坏了不影响主流程,调用方降级直拉)。
10
+ """
11
+ import os
12
+ import json
13
+ import time
14
+ import hashlib
15
+
16
+ DEFAULT_TTL = 24 * 3600 # 24h
17
+
18
+
19
+ def _repo_root():
20
+ """从本文件向上找含 workspace/ 或 .qoder/ 的目录作仓库根。"""
21
+ d = os.path.dirname(os.path.abspath(__file__))
22
+ for _ in range(7):
23
+ if os.path.isdir(os.path.join(d, "workspace")) or os.path.isdir(os.path.join(d, ".qoder")):
24
+ return d
25
+ parent = os.path.dirname(d)
26
+ if parent == d:
27
+ break
28
+ d = parent
29
+ return os.getcwd()
30
+
31
+
32
+ def _sanitize(req_id):
33
+ return "".join(c for c in str(req_id) if c.isalnum() or c in "-_") or "unknown"
34
+
35
+
36
+ def cache_path(req_id, base=None):
37
+ base = base or _repo_root()
38
+ return os.path.join(base, "workspace", "specs", _sanitize(req_id), ".context-cache.json")
39
+
40
+
41
+ def _load(path):
42
+ try:
43
+ with open(path, encoding="utf-8") as f:
44
+ data = json.load(f)
45
+ return data if isinstance(data, dict) else {}
46
+ except Exception:
47
+ return {}
48
+
49
+
50
+ def _save(path, data):
51
+ try:
52
+ os.makedirs(os.path.dirname(path), exist_ok=True)
53
+ tmp = path + ".tmp"
54
+ with open(tmp, "w", encoding="utf-8") as f:
55
+ json.dump(data, f, ensure_ascii=False, indent=2)
56
+ os.replace(tmp, path) # 原子写
57
+ return True
58
+ except Exception:
59
+ return False
60
+
61
+
62
+ def _qhash(query):
63
+ return hashlib.md5(query.strip().lower().encode("utf-8")).hexdigest()[:12]
64
+
65
+
66
+ def get(req_id, query, ttl=DEFAULT_TTL, base=None):
67
+ """命中且未过期 → 返回 text;否则 None。"""
68
+ data = _load(cache_path(req_id, base))
69
+ entry = data.get(_qhash(query))
70
+ if not entry:
71
+ return None
72
+ if ttl and (time.time() - float(entry.get("ts", 0))) > ttl:
73
+ return None
74
+ return entry.get("text")
75
+
76
+
77
+ def put(req_id, query, text, base=None):
78
+ """写一条缓存(覆盖同 query 旧值)。"""
79
+ path = cache_path(req_id, base)
80
+ data = _load(path)
81
+ data[_qhash(query)] = {"query": query, "text": text, "ts": time.time()}
82
+ return _save(path, data)
83
+
84
+
85
+ def invalidate(req_id, base=None):
86
+ """清整个 REQ 的缓存(PRD/spec 改动后调)。"""
87
+ return _save(cache_path(req_id, base), {})
88
+
89
+
90
+ def stats(req_id, base=None):
91
+ """缓存条数 + query 摘要(调试/观测命中率用)。"""
92
+ data = _load(cache_path(req_id, base))
93
+ return {"entries": len(data),
94
+ "queries": [str(e.get("query", ""))[:40] for e in data.values()]}
@@ -71,6 +71,7 @@ DISPATCH = {
71
71
  # ── 回流平台 (引擎产出 → 平台 AI回流Tab) ──
72
72
  # 用法: wlkj.py return prd <路径> <标题> | return prototype <路径> <功能名> [--platform web]
73
73
  'return': 'domain/integration/return_to_platform.py',
74
+ 'spec-upload': 'domain/integration/spec_upload.py',
74
75
  # ── 设计 ──
75
76
  'fill-prototype': 'domain/design/fill_prototype.py',
76
77
  'design-doc': 'domain/design/gen_design_doc.py',
@@ -95,6 +96,7 @@ DISPATCH = {
95
96
  'autotest-batch': 'validation/test/autotest_batch.py',
96
97
  'benchmark': 'validation/test/benchmark.py',
97
98
  # ── 环境 / 初始化 ──
99
+ 'switch-project': 'domain/kg/switch_project.py',
98
100
  'doctor': 'deployment/setup/init_doctor.py',
99
101
  'init': 'deployment/setup/setup.py',
100
102
  'install-qw': 'deployment/setup/install_qoderwork.py',
@@ -158,6 +160,95 @@ def _usage():
158
160
  return '\n'.join(lines)
159
161
 
160
162
 
163
+ def _ctx_cache_cmd(rest, scripts_root):
164
+ """wlkj.py ctx-cache <REQ-ID> <query> [--platform web|app] — 按 REQ 缓存 context_pack 结果(T1.3)。
165
+ HIT 回放缓存(省一次 KG/MCP 往返);MISS 跑 context_pack 捕获 stdout 写缓存再回放。
166
+ wlkj.py ctx-cache <REQ-ID> --clear — 清该 REQ 缓存(PRD/spec 改动后失效)。
167
+ 任何异常 fail-open 降级直拉(缓存坏了不影响主流程)。
168
+ """
169
+ do_clear = "--clear" in rest
170
+ rest = [a for a in rest if a != "--clear"]
171
+
172
+ req_id = None
173
+ query_parts, passthrough = [], []
174
+ i = 0
175
+ while i < len(rest):
176
+ a = rest[i]
177
+ if a == "--platform" and i + 1 < len(rest):
178
+ passthrough.extend([a, rest[i + 1]]); i += 2; continue
179
+ if a.startswith("-"):
180
+ passthrough.append(a); i += 1; continue
181
+ if req_id is None:
182
+ req_id = a; i += 1; continue
183
+ query_parts.append(a); i += 1
184
+ query = " ".join(query_parts).strip()
185
+ if not req_id or (not query and not do_clear):
186
+ sys.stderr.write("用法: wlkj.py ctx-cache <REQ-ID> <query> [--platform web|app] | ctx-cache <REQ-ID> --clear\n")
187
+ return 1
188
+
189
+ cc = None
190
+ try:
191
+ if scripts_root not in sys.path:
192
+ sys.path.insert(0, scripts_root)
193
+ from foundation.io import context_cache as _cc # noqa
194
+ cc = _cc
195
+ except Exception as e:
196
+ sys.stderr.write("[cache] 模块导入失败, 降级直拉: %s\n" % str(e)[:80])
197
+
198
+ # --clear: 清该 REQ 缓存
199
+ if do_clear:
200
+ if cc is None:
201
+ sys.stderr.write("[cache] 模块不可用, 无法 clear\n"); return 1
202
+ cc.invalidate(req_id)
203
+ sys.stderr.write("[cache] CLEARED req=%s\n" % req_id)
204
+ return 0
205
+
206
+ if cc is not None:
207
+ try:
208
+ cached = cc.get(req_id, query)
209
+ if cached is not None:
210
+ sys.stdout.write(cached if cached.endswith("\n") else cached + "\n")
211
+ sys.stderr.write("[cache] HIT req=%s q=%s (省一次 context_pack 往返)\n" % (req_id, query[:30]))
212
+ return 0
213
+ except Exception as e:
214
+ sys.stderr.write("[cache] get 失败, 降级直拉: %s\n" % str(e)[:80])
215
+
216
+ # MISS / 降级:跑 context_pack,捕获 stdout,写缓存,回放
217
+ script_path = os.path.join(scripts_root, DISPATCH["context"])
218
+ r = subprocess.run([sys.executable, script_path, query] + passthrough, capture_output=True)
219
+ sys.stderr.write(r.stderr.decode(errors="replace"))
220
+ out = r.stdout.decode(errors="replace")
221
+ sys.stdout.write(out)
222
+ if out.strip() and cc is not None:
223
+ try:
224
+ cc.put(req_id, query, out)
225
+ sys.stderr.write("[cache] MISS+PUT req=%s q=%s\n" % (req_id, query[:30]))
226
+ except Exception as e:
227
+ sys.stderr.write("[cache] put 失败(不影响本次输出): %s\n" % str(e)[:80])
228
+ return r.returncode
229
+
230
+
231
+ def _bf_score_cmd(rest, scripts_root):
232
+ """wlkj.py bf-score <prd|proto|code|search> <产物文件> [--feature 主题] — 业务保真打分(T1/FW12)。
233
+ 转发 bf_score.py 对真命令产出文件打 BF 分(PRD→BF1/2, 原型→BF3/4, code/search→BF5/6)。
234
+ bf_score.py 跑在云容器(接真 db_tables/design_system/真PRD);本地无业务数据时降级提示。"""
235
+ script = os.path.join(scripts_root, "validation", "eval", "bf_score.py")
236
+ if not os.path.isfile(script):
237
+ sys.stderr.write("[bf-score] bf_score.py 不在,跳过\n")
238
+ return 1
239
+ r = subprocess.run([sys.executable, script] + rest)
240
+ return r.returncode
241
+
242
+
243
+ # 带 custom 逻辑的命令(在 DISPATCH 透明转发前拦截)
244
+ SPECIAL = {
245
+ 'ctx-cache': _ctx_cache_cmd,
246
+ 'context-cache': _ctx_cache_cmd,
247
+ 'bf-score': _bf_score_cmd,
248
+ 'bfscore': _bf_score_cmd,
249
+ }
250
+
251
+
161
252
  def main():
162
253
  argv = sys.argv[1:]
163
254
  if not argv or argv[0] in ('-h', '--help', 'help'):
@@ -170,6 +261,10 @@ def main():
170
261
  # scripts/ 根 = __file__ 的上两级 (orchestration/ -> scripts/)
171
262
  scripts_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
172
263
 
264
+ # T1.3 带 custom 逻辑的命令(如 ctx-cache 缓存)在透明转发前拦截
265
+ if cmd in SPECIAL:
266
+ return SPECIAL[cmd](rest, scripts_root)
267
+
173
268
  script = DISPATCH.get(cmd)
174
269
  if not script:
175
270
  # Fallback: 当作脚本名在 scripts/ 下递归找 (支撑 'wlkj.py run <脚本>')