@hupan56/wlkj 3.3.13 → 3.3.15

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 (54) hide show
  1. package/bin/cli.js +0 -7
  2. package/package.json +29 -29
  3. package/templates/qoder/commands/optional/wl-spec.md +0 -8
  4. package/templates/qoder/commands/optional/wl-status.md +0 -8
  5. package/templates/qoder/commands/wl-code.md +0 -8
  6. package/templates/qoder/commands/wl-commit.md +0 -8
  7. package/templates/qoder/commands/wl-init.md +129 -129
  8. package/templates/qoder/commands/wl-search.md +0 -8
  9. package/templates/qoder/commands/wl-task.md +0 -8
  10. package/templates/qoder/commands/wl-test.md +0 -8
  11. package/templates/qoder/config.yaml +2 -3
  12. package/templates/qoder/contracts/insight.md +55 -55
  13. package/templates/qoder/hooks/pre-tool-use-commit.py +124 -124
  14. package/templates/qoder/hooks/session-start.py +384 -384
  15. package/templates/qoder/hooks/stop-eval.py +383 -383
  16. package/templates/qoder/scripts/capability/adapters/mcp.py +322 -322
  17. package/templates/qoder/scripts/capability/registry.py +245 -245
  18. package/templates/qoder/scripts/deployment/setup/install_qoderwork.py +14 -0
  19. package/templates/qoder/scripts/deployment/setup/setup.py +0 -11
  20. package/templates/qoder/scripts/domain/task/wlkj_panel.py +1503 -1503
  21. package/templates/qoder/scripts/foundation/bootstrap.py +145 -156
  22. package/templates/qoder/scripts/foundation/io/context_cache.py +94 -94
  23. package/templates/qoder/scripts/orchestration/wlkj.py +295 -306
  24. package/templates/qoder/scripts/tool_guide.md +70 -70
  25. package/templates/qoder/scripts/validation/eval/alignment_matrix.py +176 -176
  26. package/templates/qoder/scripts/validation/eval/bf2_content_fidelity.py +110 -110
  27. package/templates/qoder/scripts/validation/eval/bf2_llmjudge.py +104 -104
  28. package/templates/qoder/scripts/validation/eval/bf_score.py +218 -218
  29. package/templates/qoder/scripts/validation/eval/code_flywheel.py +150 -150
  30. package/templates/qoder/scripts/validation/eval/dispatcher_ab.py +156 -156
  31. package/templates/qoder/scripts/validation/eval/dispatcher_ab_2026-07-21.json +23 -23
  32. package/templates/qoder/scripts/validation/eval/feature_fidelity_flywheel.py +143 -143
  33. package/templates/qoder/scripts/validation/eval/gradient_matrix.py +261 -261
  34. package/templates/qoder/scripts/validation/eval/gradient_matrix_baseline_2026-07-21.json +33 -33
  35. package/templates/qoder/scripts/validation/eval/metrics_dashboard.py +105 -105
  36. package/templates/qoder/scripts/validation/eval/multi_turn_flywheel.py +118 -118
  37. package/templates/qoder/scripts/validation/eval/prd_fidelity_flywheel.py +128 -128
  38. package/templates/qoder/scripts/validation/eval/prd_flywheel.py +148 -148
  39. package/templates/qoder/scripts/validation/eval/prototype_fidelity_flywheel.py +166 -166
  40. package/templates/qoder/scripts/validation/eval/recall_flywheel.py +148 -148
  41. package/templates/qoder/scripts/validation/eval/robustness_flywheel.py +139 -139
  42. package/templates/qoder/scripts/validation/eval/speed_accuracy_flywheel.py +188 -188
  43. package/templates/qoder/scripts/validation/eval/task_flywheel.py +124 -124
  44. package/templates/qoder/scripts/validation/eval/token_flywheel.py +88 -88
  45. package/templates/qoder/scripts/validation/metrics/eval_code_ac.py +177 -177
  46. package/templates/qoder/scripts/validation/metrics/lint_cases.py +170 -170
  47. package/templates/qoder/scripts/validation/test/test_context_cache.py +78 -78
  48. package/templates/qoder/scripts/validation/test/test_lint_cases.py +60 -60
  49. package/templates/qoder/scripts/validation/test/test_pre_tool_use_commit.py +70 -70
  50. package/templates/qoder/commands/wl-data.md +0 -46
  51. package/templates/qoder/commands/wl-fix.md +0 -47
  52. package/templates/qoder/commands/wl-knowledge.md +0 -41
  53. package/templates/qoder/commands/wl-review.md +0 -49
  54. package/templates/workspace/specs/REQ-TEST-CTX/.context-cache.json +0 -1
@@ -1,156 +1,145 @@
1
- # -*- coding: utf-8 -*-
2
- # Windows 控制台 UTF-8 修复(ctypes 直调 Windows API;所有经 bootstrap 引导的脚本都受益)
3
- import sys as _s
4
- if _s.platform == 'win32':
5
- import ctypes as _ct, io as _io
6
- try:
7
- _ct.windll.kernel32.SetConsoleOutputCP(65001)
8
- _ct.windll.kernel32.SetConsoleCP(65001)
9
- _s.stdout = _io.TextIOWrapper(_s.stdout.buffer, encoding='utf-8', errors='replace')
10
- _s.stderr = _io.TextIOWrapper(_s.stderr.buffer, encoding='utf-8', errors='replace')
11
- except Exception:
12
- pass
13
- """
14
- bootstrap.py - 统一 CLI 启动样板 (DRY) + 子目录化路径自举 (v3.0)
15
-
16
- 治"70处 sys.path + 50处 reconfigure" + 子目录化后的路径定位。
17
-
18
- 子目录化后, 脚本在 scripts/domain/kg/kg.py, 它的 dirname(__file__)=scripts/domain/kg/,
19
- 直接 sys.path.insert(scripts/domain/kg/) 后 `from foundation.x` 会断 (foundation 在 scripts/foundation/)
20
-
21
- 本模块的 setup() 解决: 无论脚本在 scripts/ 根还是 scripts/某子包/,
22
- 都能正确定位 scripts/ 根并注入, 让 `from foundation.x` 和跨包 import 都能用。
23
-
24
- 核心: _find_scripts_root() 向上找含 'foundation/' 子目录的那个目录 = scripts/ 根。
25
-
26
- Usage (两种姿势):
27
- # 姿势1: 子包脚本开头 (推荐)
28
- import os, sys
29
- _S = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 子包→scripts
30
- if _S not in sys.path: sys.path.insert(0, _S)
31
- from bootstrap import setup; setup()
32
-
33
- # 姿势2: scripts/ 根脚本 (老姿势仍可用)
34
- from bootstrap import setup; setup()
35
- """
36
-
37
- import os
38
- import sys
39
-
40
- __all__ = ["setup", "SCRIPTS_DIR", "REPO_ROOT", "find_scripts_root", "ensure_scripts_path"]
41
-
42
-
43
- def find_scripts_root(start=None):
44
- """定位 .qoder/scripts/ 根目录。
45
-
46
- 策略: start (默认 __file__ 的目录) 向上找, 直到某目录含 'common/' 子目录
47
- 且该 common/ 下有 bootstrap.py (即本文件) → 那就是 scripts/ 根。
48
- 兜底: 找含 '.qoder' 的目录 + '/scripts'。
49
- """
50
- start = start or os.path.dirname(os.path.abspath(__file__))
51
- cur = start
52
- for _ in range(10):
53
- # scripts/ 根的标志: 含 common/bootstrap.py
54
- if os.path.isfile(os.path.join(cur, "foundation", "bootstrap.py")):
55
- return cur
56
- parent = os.path.dirname(cur)
57
- if parent == cur:
58
- break
59
- cur = parent
60
- # 兜底: cwd 向上找 .qoder/scripts
61
- cwd = os.getcwd()
62
- for _ in range(10):
63
- cand = os.path.join(cwd, ".qoder", "scripts")
64
- if os.path.isdir(cand):
65
- return cand
66
- parent = os.path.dirname(cwd)
67
- if parent == cwd:
68
- break
69
- cwd = parent
70
- return start
71
-
72
-
73
- def ensure_scripts_path(start=None):
74
- """确保 scripts/ 根在 sys.path 最前。幂等。
75
-
76
- 子包脚本调用此函数后, `from foundation.x` 即可用。
77
- """
78
- root = find_scripts_root(start)
79
- if root not in sys.path:
80
- sys.path.insert(0, root)
81
- return root
82
-
83
-
84
- def _find_scripts_dir() -> str:
85
- """[内部] 定位 scripts/ (兼容旧名)。"""
86
- return find_scripts_root()
87
-
88
-
89
- def ensure_utf8_stdio():
90
- """UTF-8 stdio 配置 (v3.0: 从 utf8.py 并入)。
91
-
92
- 把 stdout/stderr 切到 UTF-8, 失败则忽略 (不崩溃)
93
- 幂等: 多次调用无害。
94
- """
95
- for stream_name in ("stdout", "stderr"):
96
- stream = getattr(sys, stream_name, None)
97
- if stream is None:
98
- continue
99
- reconfigure = getattr(stream, "reconfigure", None)
100
- if reconfigure is None:
101
- continue
102
- try:
103
- reconfigure(encoding="utf-8", errors="replace")
104
- except (TypeError, ValueError, OSError, IOError):
105
- try:
106
- reconfigure(encoding="utf-8")
107
- except Exception:
108
- pass
109
-
110
-
111
- def setup():
112
- """统一 CLI 启动初始化。幂等。
113
-
114
- 做: ensure_utf8_stdio scripts/ 根注入 sys.path ③ scripts/common/ 注入
115
- Returns: (SCRIPTS_DIR, REPO_ROOT)
116
- """
117
- # 先确保 scripts/ 根在 path (本文件可能在 common/ 或被引用)
118
- scripts_dir = ensure_scripts_root = find_scripts_root()
119
- if scripts_dir not in sys.path:
120
- sys.path.insert(0, scripts_dir)
121
- common_dir = os.path.join(scripts_dir, "foundation")
122
- if os.path.isdir(common_dir) and common_dir not in sys.path:
123
- sys.path.insert(0, common_dir)
124
- # 注入所有子包目录 (kg/build/mcp/test/setup/carriers/task/search/report)
125
- # 让跨包 import (旧式不带前缀, 如 import search_index) 仍可用
126
- try:
127
- for _name in os.listdir(scripts_dir):
128
- _subdir = os.path.join(scripts_dir, _name)
129
- if os.path.isdir(_subdir) and _subdir not in sys.path:
130
- sys.path.insert(0, _subdir)
131
- except OSError:
132
- pass
133
-
134
- # 1. UTF-8 stdio (v3.0: utf8.py 已并入 bootstrap, 内联实现)
135
- try:
136
- ensure_utf8_stdio()
137
- except Exception:
138
- for sname in ("stdout", "stderr"):
139
- s = getattr(sys, sname, None)
140
- rc = getattr(s, "reconfigure", None) if s else None
141
- if rc:
142
- try:
143
- rc(encoding="utf-8", errors="replace")
144
- except Exception:
145
- try:
146
- rc(encoding="utf-8")
147
- except Exception:
148
- pass
149
-
150
- repo_root = os.path.dirname(os.path.dirname(scripts_dir))
151
- return scripts_dir, repo_root
152
-
153
-
154
- # 模块级常量
155
- SCRIPTS_DIR = find_scripts_root()
156
- REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(SCRIPTS_DIR))))
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ bootstrap.py - 统一 CLI 启动样板 (DRY) + 子目录化路径自举 (v3.0)
4
+
5
+ 治"70处 sys.path + 50处 reconfigure" + 子目录化后的路径定位。
6
+
7
+ 子目录化后, 脚本在 scripts/domain/kg/kg.py, 它的 dirname(__file__)=scripts/domain/kg/,
8
+ 直接 sys.path.insert(scripts/domain/kg/) 后 `from foundation.x` 会断 (foundation 在 scripts/foundation/)
9
+
10
+ 本模块的 setup() 解决: 无论脚本在 scripts/ 根还是 scripts/某子包/,
11
+ 都能正确定位 scripts/ 根并注入, 让 `from foundation.x` 和跨包 import 都能用。
12
+
13
+ 核心: _find_scripts_root() 向上找含 'foundation/' 子目录的那个目录 = scripts/ 根。
14
+
15
+ Usage (两种姿势):
16
+ # 姿势1: 子包脚本开头 (推荐)
17
+ import os, sys
18
+ _S = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 子包→scripts
19
+ if _S not in sys.path: sys.path.insert(0, _S)
20
+ from bootstrap import setup; setup()
21
+
22
+ # 姿势2: scripts/ 根脚本 (老姿势仍可用)
23
+ from bootstrap import setup; setup()
24
+ """
25
+
26
+ import os
27
+ import sys
28
+
29
+ __all__ = ["setup", "SCRIPTS_DIR", "REPO_ROOT", "find_scripts_root", "ensure_scripts_path"]
30
+
31
+
32
+ def find_scripts_root(start=None):
33
+ """定位 .qoder/scripts/ 根目录。
34
+
35
+ 策略: 从 start (默认 __file__ 的目录) 向上找, 直到某目录含 'common/' 子目录
36
+ 且该 common/ 下有 bootstrap.py (即本文件) → 那就是 scripts/ 根。
37
+ 兜底: 找含 '.qoder' 的目录 + '/scripts'。
38
+ """
39
+ start = start or os.path.dirname(os.path.abspath(__file__))
40
+ cur = start
41
+ for _ in range(10):
42
+ # scripts/ 根的标志: 含 common/bootstrap.py
43
+ if os.path.isfile(os.path.join(cur, "foundation", "bootstrap.py")):
44
+ return cur
45
+ parent = os.path.dirname(cur)
46
+ if parent == cur:
47
+ break
48
+ cur = parent
49
+ # 兜底: cwd 向上找 .qoder/scripts
50
+ cwd = os.getcwd()
51
+ for _ in range(10):
52
+ cand = os.path.join(cwd, ".qoder", "scripts")
53
+ if os.path.isdir(cand):
54
+ return cand
55
+ parent = os.path.dirname(cwd)
56
+ if parent == cwd:
57
+ break
58
+ cwd = parent
59
+ return start
60
+
61
+
62
+ def ensure_scripts_path(start=None):
63
+ """确保 scripts/ 根在 sys.path 最前。幂等。
64
+
65
+ 子包脚本调用此函数后, `from foundation.x` 即可用。
66
+ """
67
+ root = find_scripts_root(start)
68
+ if root not in sys.path:
69
+ sys.path.insert(0, root)
70
+ return root
71
+
72
+
73
+ def _find_scripts_dir() -> str:
74
+ """[内部] 定位 scripts/ (兼容旧名)。"""
75
+ return find_scripts_root()
76
+
77
+
78
+ def ensure_utf8_stdio():
79
+ """UTF-8 stdio 配置 (v3.0: 从 utf8.py 并入)。
80
+
81
+ stdout/stderr 切到 UTF-8, 失败则忽略 (不崩溃)。
82
+ 幂等: 多次调用无害。
83
+ """
84
+ for stream_name in ("stdout", "stderr"):
85
+ stream = getattr(sys, stream_name, None)
86
+ if stream is None:
87
+ continue
88
+ reconfigure = getattr(stream, "reconfigure", None)
89
+ if reconfigure is None:
90
+ continue
91
+ try:
92
+ reconfigure(encoding="utf-8", errors="replace")
93
+ except (TypeError, ValueError, OSError, IOError):
94
+ try:
95
+ reconfigure(encoding="utf-8")
96
+ except Exception:
97
+ pass
98
+
99
+
100
+ def setup():
101
+ """统一 CLI 启动初始化。幂等。
102
+
103
+ 做: ① ensure_utf8_stdio ② scripts/ 根注入 sys.path ③ scripts/common/ 注入
104
+ Returns: (SCRIPTS_DIR, REPO_ROOT)
105
+ """
106
+ # 先确保 scripts/ 根在 path (本文件可能在 common/ 或被引用)
107
+ scripts_dir = ensure_scripts_root = find_scripts_root()
108
+ if scripts_dir not in sys.path:
109
+ sys.path.insert(0, scripts_dir)
110
+ common_dir = os.path.join(scripts_dir, "foundation")
111
+ if os.path.isdir(common_dir) and common_dir not in sys.path:
112
+ sys.path.insert(0, common_dir)
113
+ # 注入所有子包目录 (kg/build/mcp/test/setup/carriers/task/search/report)
114
+ # 让跨包 import (旧式不带前缀, import search_index) 仍可用
115
+ try:
116
+ for _name in os.listdir(scripts_dir):
117
+ _subdir = os.path.join(scripts_dir, _name)
118
+ if os.path.isdir(_subdir) and _subdir not in sys.path:
119
+ sys.path.insert(0, _subdir)
120
+ except OSError:
121
+ pass
122
+
123
+ # 1. UTF-8 stdio (v3.0: utf8.py 已并入 bootstrap, 内联实现)
124
+ try:
125
+ ensure_utf8_stdio()
126
+ except Exception:
127
+ for sname in ("stdout", "stderr"):
128
+ s = getattr(sys, sname, None)
129
+ rc = getattr(s, "reconfigure", None) if s else None
130
+ if rc:
131
+ try:
132
+ rc(encoding="utf-8", errors="replace")
133
+ except Exception:
134
+ try:
135
+ rc(encoding="utf-8")
136
+ except Exception:
137
+ pass
138
+
139
+ repo_root = os.path.dirname(os.path.dirname(scripts_dir))
140
+ return scripts_dir, repo_root
141
+
142
+
143
+ # 模块级常量
144
+ SCRIPTS_DIR = find_scripts_root()
145
+ REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(SCRIPTS_DIR))))
@@ -1,94 +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()]}
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()]}