@hupan56/wlkj 2.7.12 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/bin/cli.js +1435 -1107
  2. package/package.json +29 -29
  3. package/templates/qoder/agents/insight-planning.md +1 -1
  4. package/templates/qoder/agents/insight-research.md +28 -15
  5. package/templates/qoder/commands/optional/wl-insight.md +161 -161
  6. package/templates/qoder/commands/optional/wl-report.md +20 -39
  7. package/templates/qoder/commands/optional/wl-status.md +4 -4
  8. package/templates/qoder/commands/wl-code.md +2 -2
  9. package/templates/qoder/commands/wl-design.md +6 -6
  10. package/templates/qoder/commands/wl-init.md +3 -3
  11. package/templates/qoder/commands/wl-prd.md +26 -13
  12. package/templates/qoder/commands/wl-req.md +43 -0
  13. package/templates/qoder/commands/wl-search.md +78 -47
  14. package/templates/qoder/commands/wl-task.md +343 -40
  15. package/templates/qoder/commands/wl-test.md +43 -17
  16. package/templates/qoder/config.yaml +52 -14
  17. package/templates/qoder/hooks/post-tool-use.py +181 -0
  18. package/templates/qoder/hooks/session-start.py +94 -23
  19. package/templates/qoder/hooks/stop-eval.py +207 -0
  20. package/templates/qoder/hooks/user-prompt-submit.py +139 -0
  21. package/templates/qoder/rules/wl-pipeline.md +85 -49
  22. package/templates/qoder/scripts/README.md +102 -0
  23. package/templates/qoder/scripts/capability/__init__.py +26 -0
  24. package/templates/qoder/scripts/capability/__main__.py +72 -0
  25. package/templates/qoder/scripts/capability/adapters/__init__.py +29 -0
  26. package/templates/qoder/scripts/capability/adapters/cli.py +316 -0
  27. package/templates/qoder/scripts/capability/adapters/mcp.py +334 -0
  28. package/templates/qoder/scripts/capability/adapters/qw.py +271 -0
  29. package/templates/qoder/scripts/capability/caps/__init__.py +27 -0
  30. package/templates/qoder/scripts/capability/caps/context.py +36 -0
  31. package/templates/qoder/scripts/capability/caps/cron.py +50 -0
  32. package/templates/qoder/scripts/capability/caps/identity.py +43 -0
  33. package/templates/qoder/scripts/capability/caps/memory.py +71 -0
  34. package/templates/qoder/scripts/capability/caps/notify.py +41 -0
  35. package/templates/qoder/scripts/capability/caps/present.py +48 -0
  36. package/templates/qoder/scripts/capability/caps/repo.py +31 -0
  37. package/templates/qoder/scripts/capability/caps/sandbox.py +43 -0
  38. package/templates/qoder/scripts/capability/chain.py +92 -0
  39. package/templates/qoder/scripts/capability/memory_chain.py +41 -0
  40. package/templates/qoder/scripts/capability/registry.py +246 -0
  41. package/templates/qoder/scripts/capability/registry_mcp.py +250 -0
  42. package/templates/qoder/scripts/capability/smoke_test.py +211 -0
  43. package/templates/qoder/scripts/capability/smoke_test_report.json +94 -0
  44. package/templates/qoder/scripts/deployment/setup/__init__.py +11 -0
  45. package/templates/qoder/scripts/deployment/setup/carriers.py +669 -0
  46. package/templates/qoder/scripts/deployment/setup/carriers_verify.py +148 -0
  47. package/templates/qoder/scripts/{init_doctor.py → deployment/setup/init_doctor.py} +100 -39
  48. package/templates/qoder/scripts/{install_qoderwork.py → deployment/setup/install_qoderwork.py} +48 -30
  49. package/templates/qoder/scripts/{platform_doctor.py → deployment/setup/platform_doctor.py} +271 -259
  50. package/templates/qoder/scripts/{repo_root.py → deployment/setup/repo_root.py} +9 -2
  51. package/templates/qoder/scripts/{setup.py → deployment/setup/setup.py} +143 -14
  52. package/templates/qoder/scripts/{setup_lanhu.py → deployment/setup/setup_lanhu.py} +979 -963
  53. package/templates/qoder/scripts/domain/design/__init__.py +0 -0
  54. package/templates/qoder/scripts/{fill_prototype.py → domain/design/fill_prototype.py} +17 -5
  55. package/templates/qoder/scripts/{gen_design_doc.py → domain/design/gen_design_doc.py} +406 -394
  56. package/templates/qoder/scripts/domain/kg/__init__.py +11 -0
  57. package/templates/qoder/scripts/domain/kg/build/__init__.py +0 -0
  58. package/templates/qoder/scripts/domain/kg/build/build_entity_registry.py +201 -0
  59. package/templates/qoder/scripts/domain/kg/build/build_relations.py +132 -0
  60. package/templates/qoder/scripts/{build_style_index.py → domain/kg/build/build_style_index.py} +50 -15
  61. package/templates/qoder/scripts/domain/kg/build/build_workflows.py +149 -0
  62. package/templates/qoder/scripts/{kg_build.py → domain/kg/build/kg_build.py} +685 -612
  63. package/templates/qoder/scripts/{kg_build_db.py → domain/kg/build/kg_build_db.py} +338 -327
  64. package/templates/qoder/scripts/{kg_incremental.py → domain/kg/build/kg_incremental.py} +425 -393
  65. package/templates/qoder/scripts/{learn_aggregate.py → domain/kg/build/learn_aggregate.py} +212 -201
  66. package/templates/qoder/scripts/domain/kg/extract/__init__.py +0 -0
  67. package/templates/qoder/scripts/{common → domain/kg/extract}/extract.py +430 -419
  68. package/templates/qoder/scripts/domain/kg/extract/test_extract.py +115 -0
  69. package/templates/qoder/scripts/{common → domain/kg/extract}/ts_extract.py +614 -536
  70. package/templates/qoder/scripts/domain/kg/graph/__init__.py +0 -0
  71. package/templates/qoder/scripts/{kg_link_db.py → domain/kg/graph/kg_link_db.py} +235 -224
  72. package/templates/qoder/scripts/domain/kg/graph/kg_semantic.py +683 -0
  73. package/templates/qoder/scripts/{kg.py → domain/kg/kg.py} +871 -708
  74. package/templates/qoder/scripts/domain/kg/kg_capabilities.py +182 -0
  75. package/templates/qoder/scripts/domain/kg/search/__init__.py +0 -0
  76. package/templates/qoder/scripts/{context_pack.py → domain/kg/search/context_pack.py} +45 -17
  77. package/templates/qoder/scripts/{enrich_prompt.py → domain/kg/search/enrich_prompt.py} +238 -226
  78. package/templates/qoder/scripts/domain/kg/search/prefetch.py +384 -0
  79. package/templates/qoder/scripts/{common → domain/kg/search}/search_engine.py +207 -205
  80. package/templates/qoder/scripts/{search_index.py → domain/kg/search/search_index.py} +210 -88
  81. package/templates/qoder/scripts/domain/kg/server/__init__.py +0 -0
  82. package/templates/qoder/scripts/domain/kg/server/kgd.py +549 -0
  83. package/templates/qoder/scripts/domain/kg/server/perf_bench.py +197 -0
  84. package/templates/qoder/scripts/domain/kg/storage/__init__.py +0 -0
  85. package/templates/qoder/scripts/{kg_duckdb.py → domain/kg/storage/kg_duckdb.py} +70 -45
  86. package/templates/qoder/scripts/domain/learning/__init__.py +0 -0
  87. package/templates/qoder/scripts/{learn.py → domain/learning/learn.py} +157 -146
  88. package/templates/qoder/scripts/domain/report/__init__.py +11 -0
  89. package/templates/qoder/scripts/{add_session.py → domain/report/add_session.py} +256 -244
  90. package/templates/qoder/scripts/{export.py → domain/report/export.py} +72 -4
  91. package/templates/qoder/scripts/{report.py → domain/report/report.py} +292 -281
  92. package/templates/qoder/scripts/domain/report/report_snapshot.py +261 -0
  93. package/templates/qoder/scripts/domain/report/role.py +39 -0
  94. package/templates/qoder/scripts/{status.py → domain/report/status.py} +737 -628
  95. package/templates/qoder/scripts/domain/report/status_snapshot.py +191 -0
  96. package/templates/qoder/scripts/domain/requirement/__init__.py +0 -0
  97. package/templates/qoder/scripts/{archive_prd.py → domain/requirement/archive_prd.py} +389 -377
  98. package/templates/qoder/scripts/domain/requirement/req.py +228 -0
  99. package/templates/qoder/scripts/domain/task/__init__.py +11 -0
  100. package/templates/qoder/scripts/{git_sync.py → domain/task/git_sync.py} +90 -50
  101. package/templates/qoder/scripts/{syncgate.py → domain/task/syncgate.py} +18 -7
  102. package/templates/qoder/scripts/domain/task/task.py +229 -0
  103. package/templates/qoder/scripts/domain/task/task_lifecycle.py +606 -0
  104. package/templates/qoder/scripts/domain/task/task_query.py +162 -0
  105. package/templates/qoder/scripts/domain/task/task_relations.py +425 -0
  106. package/templates/qoder/scripts/{team_sync.py → domain/task/team_sync.py} +101 -23
  107. package/templates/qoder/scripts/domain/task/zentao_sync.py +370 -0
  108. package/templates/qoder/scripts/foundation/__init__.py +0 -0
  109. package/templates/qoder/scripts/foundation/bootstrap.py +145 -0
  110. package/templates/qoder/scripts/foundation/core/__init__.py +0 -0
  111. package/templates/qoder/scripts/foundation/core/cmd_registry.py +112 -0
  112. package/templates/qoder/scripts/foundation/core/config.py +187 -0
  113. package/templates/qoder/scripts/{common → foundation/core}/paths.py +706 -400
  114. package/templates/qoder/scripts/foundation/data/__init__.py +0 -0
  115. package/templates/qoder/scripts/foundation/data/contract.py +317 -0
  116. package/templates/qoder/scripts/{common → foundation/data}/events.py +2 -2
  117. package/templates/qoder/scripts/foundation/data/result.py +223 -0
  118. package/templates/qoder/scripts/{common → foundation/data}/task_utils.py +427 -392
  119. package/templates/qoder/scripts/foundation/identity/__init__.py +0 -0
  120. package/templates/qoder/scripts/foundation/identity/check_publish.py +103 -0
  121. package/templates/qoder/scripts/{common → foundation/identity}/developer.py +239 -238
  122. package/templates/qoder/scripts/foundation/identity/guard.py +159 -0
  123. package/templates/qoder/scripts/{common → foundation/identity}/identity.py +132 -9
  124. package/templates/qoder/scripts/foundation/identity/roles.py +60 -0
  125. package/templates/qoder/scripts/foundation/integrations/__init__.py +0 -0
  126. package/templates/qoder/scripts/{common → foundation/integrations}/active_task.py +30 -15
  127. package/templates/qoder/scripts/{common → foundation/integrations}/feishu.py +11 -10
  128. package/templates/qoder/scripts/{common → foundation/integrations}/terms.py +3 -3
  129. package/templates/qoder/scripts/foundation/integrations/zentao_client.py +220 -0
  130. package/templates/qoder/scripts/foundation/io/__init__.py +0 -0
  131. package/templates/qoder/scripts/{common → foundation/io}/filelock.py +1 -1
  132. package/templates/qoder/scripts/{common → foundation/io}/reqid.py +1 -1
  133. package/templates/qoder/scripts/foundation/protocol/__init__.py +0 -0
  134. package/templates/qoder/scripts/foundation/protocol/duckdb_conn.py +39 -0
  135. package/templates/qoder/scripts/foundation/protocol/mcp_base.py +268 -0
  136. package/templates/qoder/scripts/foundation/utils/__init__.py +0 -0
  137. package/templates/qoder/scripts/{common → foundation/utils}/eval_api.py +12 -1
  138. package/templates/qoder/scripts/{common → foundation/utils}/pip_install.py +23 -6
  139. package/templates/qoder/scripts/{common → foundation/utils}/platform_guard.py +1 -1
  140. package/templates/qoder/scripts/orchestration/__init__.py +0 -0
  141. package/templates/qoder/scripts/orchestration/wlkj.py +185 -0
  142. package/templates/qoder/scripts/protocol/__init__.py +0 -0
  143. package/templates/qoder/scripts/protocol/browser/README.md +207 -0
  144. package/templates/qoder/scripts/protocol/browser/SKILL.md +265 -0
  145. package/templates/qoder/scripts/protocol/browser/config.env +9 -0
  146. package/templates/qoder/scripts/protocol/browser/references/cdp-api.md +110 -0
  147. package/templates/qoder/scripts/protocol/browser/references/migration-2.5.3.md +72 -0
  148. package/templates/qoder/scripts/protocol/browser/references/site-patterns/.gitkeep +0 -0
  149. package/templates/qoder/scripts/protocol/browser/scripts/browser-discovery.mjs +138 -0
  150. package/templates/qoder/scripts/protocol/browser/scripts/cdp-proxy.mjs +672 -0
  151. package/templates/qoder/scripts/protocol/browser/scripts/check-deps.mjs +206 -0
  152. package/templates/qoder/scripts/protocol/browser/scripts/find-url.mjs +253 -0
  153. package/templates/qoder/scripts/protocol/browser/scripts/match-site.mjs +46 -0
  154. package/templates/qoder/scripts/protocol/browser/templates/config.env.template +9 -0
  155. package/templates/qoder/scripts/protocol/mcp/__init__.py +11 -0
  156. package/templates/qoder/scripts/protocol/mcp/kg_mcp_server.py +478 -0
  157. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → protocol/mcp/lanhu_stdio_wrapper.py} +131 -119
  158. package/templates/qoder/scripts/protocol/mcp/mcp_doctor.py +548 -0
  159. package/templates/qoder/scripts/protocol/mcp/mcp_launcher.py +600 -0
  160. package/templates/qoder/scripts/protocol/mcp/mysql_mcp_server.py +461 -0
  161. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +1920 -0
  162. package/templates/qoder/scripts/protocol/transports/__init__.py +56 -0
  163. package/templates/qoder/scripts/protocol/transports/base.py +87 -0
  164. package/templates/qoder/scripts/protocol/transports/cli.py +132 -0
  165. package/templates/qoder/scripts/protocol/transports/http.py +141 -0
  166. package/templates/qoder/scripts/protocol/transports/stdio.py +293 -0
  167. package/templates/qoder/scripts/run_weekly_update.bat +27 -18
  168. package/templates/qoder/scripts/run_weekly_update.sh +22 -13
  169. package/templates/qoder/scripts/validation/__init__.py +0 -0
  170. package/templates/qoder/scripts/validation/eval/__init__.py +0 -0
  171. package/templates/qoder/scripts/validation/eval/perf-report-2026-06-24.md +139 -0
  172. package/templates/qoder/scripts/validation/eval/qwork_harness.py +617 -0
  173. package/templates/qoder/scripts/validation/eval/report-commands.md +220 -0
  174. package/templates/qoder/scripts/validation/eval/transcript_timing.py +386 -0
  175. package/templates/qoder/scripts/validation/metrics/__init__.py +0 -0
  176. package/templates/qoder/scripts/{eval_prd.py → validation/metrics/eval_prd.py} +84 -15
  177. package/templates/qoder/scripts/validation/metrics/usability_score.py +750 -0
  178. package/templates/qoder/scripts/validation/test/__init__.py +11 -0
  179. package/templates/qoder/scripts/validation/test/assertion_gen.py +551 -0
  180. package/templates/qoder/scripts/{autotest.py → validation/test/autotest.py} +1234 -1751
  181. package/templates/qoder/scripts/validation/test/autotest_auth.py +109 -0
  182. package/templates/qoder/scripts/{autotest_batch.py → validation/test/autotest_batch.py} +255 -224
  183. package/templates/qoder/scripts/validation/test/autotest_data.py +680 -0
  184. package/templates/qoder/scripts/validation/test/autotest_opencli.py +646 -0
  185. package/templates/qoder/scripts/{autotest_run.py → validation/test/autotest_run.py} +323 -297
  186. package/templates/qoder/scripts/validation/test/autotest_webaccess.py +571 -0
  187. package/templates/qoder/scripts/{benchmark.py → validation/test/benchmark.py} +17 -5
  188. package/templates/qoder/scripts/{kg_auto_login.py → validation/test/kg_auto_login.py} +208 -196
  189. package/templates/qoder/scripts/{kg_test_runner.py → validation/test/kg_test_runner.py} +13 -2
  190. package/templates/qoder/scripts/{page_probe.py → validation/test/page_probe.py} +470 -459
  191. package/templates/qoder/scripts/validation/test/smoke_all_commands.py +170 -0
  192. package/templates/qoder/settings.json +23 -2
  193. package/templates/qoder/skills/design-import/SKILL.md +235 -226
  194. package/templates/qoder/skills/design-review/SKILL.md +91 -82
  195. package/templates/qoder/skills/prd-generator/SKILL.md +38 -24
  196. package/templates/qoder/skills/prd-review/SKILL.md +8 -6
  197. package/templates/qoder/skills/prototype-generator/SKILL.md +296 -247
  198. package/templates/qoder/skills/spec-coder/SKILL.md +24 -15
  199. package/templates/qoder/skills/spec-generator/SKILL.md +24 -12
  200. package/templates/qoder/skills/test-generator/SKILL.md +9 -7
  201. package/templates/qoder/skills/wl-code/SKILL.md +25 -13
  202. package/templates/qoder/skills/wl-commit/SKILL.md +10 -9
  203. package/templates/qoder/skills/wl-design/SKILL.md +7 -5
  204. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  205. package/templates/qoder/skills/wl-insight/SKILL.md +25 -12
  206. package/templates/qoder/skills/wl-prd-full/SKILL.md +59 -8
  207. package/templates/qoder/skills/wl-prd-quick/SKILL.md +7 -7
  208. package/templates/qoder/skills/wl-prd-review/SKILL.md +18 -6
  209. package/templates/qoder/skills/wl-report/SKILL.md +27 -25
  210. package/templates/qoder/skills/wl-search/SKILL.md +151 -80
  211. package/templates/qoder/skills/wl-spec/SKILL.md +9 -7
  212. package/templates/qoder/skills/wl-status/SKILL.md +50 -18
  213. package/templates/qoder/skills/wl-task/SKILL.md +94 -12
  214. package/templates/qoder/skills/wl-test/SKILL.md +154 -45
  215. package/templates/qoder/templates/prd-full-template.md +7 -0
  216. package/templates/root/AGENTS.md +275 -237
  217. package/templates/root/requirements.txt +3 -0
  218. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +3 -3
  219. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +2 -2
  220. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  221. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  222. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  223. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  224. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  225. package/templates/qoder/scripts/check_carriers.py +0 -238
  226. package/templates/qoder/scripts/check_mcp.py +0 -298
  227. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  228. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  229. package/templates/qoder/scripts/collect_prds.py +0 -31
  230. package/templates/qoder/scripts/common/mentions.py +0 -134
  231. package/templates/qoder/scripts/common/utf8.py +0 -38
  232. package/templates/qoder/scripts/extract_api_params.py +0 -246
  233. package/templates/qoder/scripts/extract_routes.py +0 -54
  234. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  235. package/templates/qoder/scripts/handoff.py +0 -22
  236. package/templates/qoder/scripts/init_developer.py +0 -76
  237. package/templates/qoder/scripts/kg_mcp_server.py +0 -801
  238. package/templates/qoder/scripts/kg_semantic.py +0 -150
  239. package/templates/qoder/scripts/mcp_launcher.py +0 -414
  240. package/templates/qoder/scripts/mysql_mcp_server.py +0 -396
  241. package/templates/qoder/scripts/parse_prds.py +0 -33
  242. package/templates/qoder/scripts/role.py +0 -51
  243. package/templates/qoder/scripts/sync_carriers.py +0 -259
  244. package/templates/qoder/scripts/task.py +0 -1261
  245. package/templates/qoder/scripts/workspace_init.py +0 -102
  246. package/templates/qoder/scripts/zentao_mcp_server.py +0 -424
  247. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  248. /package/templates/qoder/scripts/{common → deployment}/__init__.py +0 -0
  249. /package/templates/qoder/{skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp → scripts/domain/__init__.py} +0 -0
  250. /package/templates/qoder/scripts/{common → domain/kg/graph}/graph_traverse.py +0 -0
  251. /package/templates/qoder/scripts/{common → domain/kg/server}/repowiki.py +0 -0
  252. /package/templates/qoder/scripts/{common → foundation/io}/atomicio.py +0 -0
  253. /package/templates/qoder/scripts/{secure-ls.js → validation/test/secure-ls.js} +0 -0
@@ -1,205 +1,207 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- search_engine.py - 搜索倒排索引 + mtime 缓存 (性能优化 A2)
4
-
5
- 把 search_keywords 的 O(n) 全扫描 3267 keys 降到 O(命中数)。
6
-
7
- 设计:
8
- - exact_index: {word_lower: [keyword_keys]} 精确匹配, O(1) 查找
9
- - prefix/suffix 索引用 trigram (3-gram) 加速子串匹配
10
- - 缓存序列化到 data/index/.inverted-cache.json, 含源 mtime
11
- - 只在 code-keyword.json 变化时重建
12
-
13
- 核心 API:
14
- get_inverted_index(repo_root) -> dict # 带缓存的倒排索引
15
- search_keywords_fast(words, inverted, ki) -> {kw: files} # 快速搜索
16
- """
17
-
18
- import json
19
- import os
20
- import sys
21
- from typing import Dict, List, Optional, Set, Tuple
22
-
23
- _THIS_DIR = os.path.dirname(os.path.abspath(__file__))
24
- if _THIS_DIR not in sys.path:
25
- sys.path.insert(0, _THIS_DIR)
26
-
27
- from atomicio import safe_read_json, atomic_write_json
28
-
29
- __all__ = [
30
- "get_inverted_index",
31
- "search_keywords_fast",
32
- "build_inverted_index",
33
- "MIN_FUZZY_LEN",
34
- ]
35
-
36
- MIN_FUZZY_LEN = 4 # 与 search_index.py 保持一致
37
-
38
-
39
- def _trigrams(s: str) -> Set[str]:
40
- """生成字符串的所有 3-gram (小写)。短于 3 字符则返回 {字符串本身}。"""
41
- s = s.lower()
42
- if len(s) < 3:
43
- return {s}
44
- return {s[i:i+3] for i in range(len(s) - 2)}
45
-
46
-
47
- def build_inverted_index(keyword_index: Dict[str, list]) -> dict:
48
- """从 keyword_index 构建倒排索引。
49
-
50
- Returns:
51
- {
52
- "exact": {word_lower: [keyword_keys]}, # 精确匹配
53
- "trigram": {trigram: set(keyword_keys)}, # 3-gram → 含它的 keys
54
- "keywords": [所有 keyword_keys 排序] # 用于回退全扫描
55
- }
56
- """
57
- exact = {}
58
- trigram = {}
59
- all_keys = sorted(keyword_index.keys())
60
-
61
- for kw in all_keys:
62
- kl = kw.lower()
63
- # 精确索引
64
- exact.setdefault(kl, []).append(kw)
65
- # trigram 索引 (用于子串匹配的候选集缩小)
66
- for tg in _trigrams(kl):
67
- trigram.setdefault(tg, set()).add(kw)
68
-
69
- # set 转 list 便于 JSON 序列化
70
- trigram_serializable = {tg: sorted(keys) for tg, keys in trigram.items()}
71
-
72
- return {
73
- "exact": exact,
74
- "trigram": trigram_serializable,
75
- "keywords": all_keys,
76
- }
77
-
78
-
79
- def get_inverted_index(repo_root: str) -> Optional[dict]:
80
- """获取倒排索引 (带 mtime 缓存)。
81
-
82
- 缓存逻辑:
83
- - 读 .inverted-cache.json 的 source_mtime
84
- - 与 code-keyword.json 当前 mtime 比对
85
- - 不一致或缓存缺失则重建 + 持久化
86
-
87
- Returns:
88
- 倒排索引 dict, 或 None (keyword-index 不存在时)。
89
- """
90
- index_dir = os.path.join(repo_root, "data", "index")
91
- ki_path = os.path.join(index_dir, "code-keyword.json")
92
- cache_path = os.path.join(index_dir, ".inverted-cache.json")
93
-
94
- if not os.path.isfile(ki_path):
95
- return None
96
-
97
- try:
98
- ki_mtime = os.path.getmtime(ki_path)
99
- except OSError:
100
- return None
101
-
102
- # 尝试读缓存
103
- cached = safe_read_json(cache_path, default=None)
104
- if cached and cached.get("source_mtime") == ki_mtime:
105
- # 缓存命中 (trigram 的 set 反序列化为 list, 搜索时兼容)
106
- return cached
107
-
108
- # 缓存失效或缺失, 重建
109
- ki = safe_read_json(ki_path, default=None)
110
- if not ki:
111
- return None
112
-
113
- inverted = build_inverted_index(ki)
114
- inverted["source_mtime"] = ki_mtime
115
- inverted["built_at"] = os.path.getmtime(ki_path)
116
-
117
- # 持久化 (best-effort, 失败不阻塞搜索)
118
- try:
119
- atomic_write_json(cache_path, inverted)
120
- except Exception:
121
- pass
122
-
123
- return inverted
124
-
125
-
126
- def _candidate_keys(word: str, inverted: dict) -> List[str]:
127
- """用倒排索引找到候选 keyword keys (缩小扫描集)。
128
-
129
- 策略:
130
- - 用 trigram 交集找候选 (子串匹配的双方至少共享一个 trigram)
131
- - 精确命中必然包含在 trigram 候选里 (相同字符串共享所有 trigram)
132
- - 候选为空则回退全部 keys (兜底, 罕见)
133
-
134
- 关键: 不能在精确命中后短路 —— query "insurance" 还要匹配
135
- "insurancepolicy" 等 (query 是 kw 的子串), 所以必须返回 trigram 候选集。
136
- """
137
- wl = word.lower()
138
- tgs = _trigrams(wl)
139
- trigram_index = inverted.get("trigram", {})
140
- candidate_sets = []
141
- for tg in tgs:
142
- keys = trigram_index.get(tg)
143
- if keys:
144
- candidate_sets.append(set(keys))
145
-
146
- if not candidate_sets:
147
- # 词太短无 trigram (如 2 字符), 回退精确 + 全扫描兜底
148
- exact = inverted.get("exact", {}).get(wl)
149
- if exact:
150
- return list(exact)
151
- return inverted.get("keywords", [])
152
-
153
- # 取并集: query 含 kw 或 kw 含 query, 至少一个 trigram 重叠
154
- candidates = set()
155
- for cs in candidate_sets:
156
- candidates |= cs
157
-
158
- return sorted(candidates)
159
-
160
-
161
- def search_keywords_fast(
162
- words: List[str],
163
- inverted: Optional[dict],
164
- keyword_index: Dict[str, list],
165
- ) -> Dict[str, list]:
166
- """快速关键词搜索 (用倒排索引缩小候选集)。
167
-
168
- Args:
169
- words: 查询词列表 (已小写化、已 CN 扩展)。
170
- inverted: 倒排索引 (None 则回退到全扫描)。
171
- keyword_index: 原始 keyword-index (用于取 files)。
172
-
173
- Returns:
174
- {keyword_key: files} 匹配结果。
175
- """
176
- matches = {}
177
-
178
- if inverted is None:
179
- # 回退: 全扫描 (旧逻辑)
180
- for word in words:
181
- for kw, files in keyword_index.items():
182
- if _match(word, kw):
183
- matches[kw] = files
184
- return matches
185
-
186
- for word in words:
187
- candidates = _candidate_keys(word, inverted)
188
- for kw in candidates:
189
- files = keyword_index.get(kw)
190
- if files and _match(word, kw):
191
- matches[kw] = files
192
-
193
- return matches
194
-
195
-
196
- def _match(query_word: str, kw: str) -> bool:
197
- """与 search_index._match_keyword 一致的匹配逻辑。"""
198
- w, k = query_word.lower(), kw.lower()
199
- if w == k:
200
- return True
201
- if len(k) >= MIN_FUZZY_LEN and k in w:
202
- return True
203
- if len(w) >= MIN_FUZZY_LEN and w in k:
204
- return True
205
- return False
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ search_engine.py - 搜索倒排索引 + mtime 缓存 (性能优化 A2)
4
+
5
+ 把 search_keywords 的 O(n) 全扫描 3267 keys 降到 O(命中数)。
6
+
7
+ 设计:
8
+ - exact_index: {word_lower: [keyword_keys]} 精确匹配, O(1) 查找
9
+ - prefix/suffix 索引用 trigram (3-gram) 加速子串匹配
10
+ - 缓存序列化到 data/index/.inverted-cache.json, 含源 mtime
11
+ - 只在 code-keyword.json 变化时重建
12
+
13
+ 核心 API:
14
+ get_inverted_index(repo_root) -> dict # 带缓存的倒排索引
15
+ search_keywords_fast(words, inverted, ki) -> {kw: files} # 快速搜索
16
+ """
17
+
18
+ import json
19
+ import os
20
+ import sys
21
+ from typing import Dict, List, Optional, Set, Tuple
22
+
23
+ # v3.0 路径自举: 迁入 kg/ 后, atomicio 在 common/, 需把 common/ 加进 sys.path
24
+ _THIS_DIR = os.path.dirname(os.path.abspath(__file__))
25
+ _cp = os.path.join(os.path.dirname(_THIS_DIR), 'foundation')
26
+ if _cp not in sys.path:
27
+ sys.path.insert(0, _cp)
28
+
29
+ from atomicio import safe_read_json, atomic_write_json
30
+
31
+ __all__ = [
32
+ "get_inverted_index",
33
+ "search_keywords_fast",
34
+ "build_inverted_index",
35
+ "MIN_FUZZY_LEN",
36
+ ]
37
+
38
+ MIN_FUZZY_LEN = 4 # 与 search_index.py 保持一致
39
+
40
+
41
+ def _trigrams(s: str) -> Set[str]:
42
+ """生成字符串的所有 3-gram (小写)。短于 3 字符则返回 {字符串本身}。"""
43
+ s = s.lower()
44
+ if len(s) < 3:
45
+ return {s}
46
+ return {s[i:i+3] for i in range(len(s) - 2)}
47
+
48
+
49
+ def build_inverted_index(keyword_index: Dict[str, list]) -> dict:
50
+ """从 keyword_index 构建倒排索引。
51
+
52
+ Returns:
53
+ {
54
+ "exact": {word_lower: [keyword_keys]}, # 精确匹配
55
+ "trigram": {trigram: set(keyword_keys)}, # 3-gram → 含它的 keys
56
+ "keywords": [所有 keyword_keys 排序] # 用于回退全扫描
57
+ }
58
+ """
59
+ exact = {}
60
+ trigram = {}
61
+ all_keys = sorted(keyword_index.keys())
62
+
63
+ for kw in all_keys:
64
+ kl = kw.lower()
65
+ # 精确索引
66
+ exact.setdefault(kl, []).append(kw)
67
+ # trigram 索引 (用于子串匹配的候选集缩小)
68
+ for tg in _trigrams(kl):
69
+ trigram.setdefault(tg, set()).add(kw)
70
+
71
+ # set 转 list 便于 JSON 序列化
72
+ trigram_serializable = {tg: sorted(keys) for tg, keys in trigram.items()}
73
+
74
+ return {
75
+ "exact": exact,
76
+ "trigram": trigram_serializable,
77
+ "keywords": all_keys,
78
+ }
79
+
80
+
81
+ def get_inverted_index(repo_root: str) -> Optional[dict]:
82
+ """获取倒排索引 (带 mtime 缓存)。
83
+
84
+ 缓存逻辑:
85
+ - .inverted-cache.json 的 source_mtime
86
+ - 与 code-keyword.json 当前 mtime 比对
87
+ - 不一致或缓存缺失则重建 + 持久化
88
+
89
+ Returns:
90
+ 倒排索引 dict, None (keyword-index 不存在时)
91
+ """
92
+ index_dir = os.path.join(repo_root, "data", "index")
93
+ ki_path = os.path.join(index_dir, "code-keyword.json")
94
+ cache_path = os.path.join(index_dir, ".inverted-cache.json")
95
+
96
+ if not os.path.isfile(ki_path):
97
+ return None
98
+
99
+ try:
100
+ ki_mtime = os.path.getmtime(ki_path)
101
+ except OSError:
102
+ return None
103
+
104
+ # 尝试读缓存
105
+ cached = safe_read_json(cache_path, default=None)
106
+ if cached and cached.get("source_mtime") == ki_mtime:
107
+ # 缓存命中 (trigram 的 set 反序列化为 list, 搜索时兼容)
108
+ return cached
109
+
110
+ # 缓存失效或缺失, 重建
111
+ ki = safe_read_json(ki_path, default=None)
112
+ if not ki:
113
+ return None
114
+
115
+ inverted = build_inverted_index(ki)
116
+ inverted["source_mtime"] = ki_mtime
117
+ inverted["built_at"] = os.path.getmtime(ki_path)
118
+
119
+ # 持久化 (best-effort, 失败不阻塞搜索)
120
+ try:
121
+ atomic_write_json(cache_path, inverted)
122
+ except Exception:
123
+ pass
124
+
125
+ return inverted
126
+
127
+
128
+ def _candidate_keys(word: str, inverted: dict) -> List[str]:
129
+ """用倒排索引找到候选 keyword keys (缩小扫描集)。
130
+
131
+ 策略:
132
+ - trigram 交集找候选 (子串匹配的双方至少共享一个 trigram)
133
+ - 精确命中必然包含在 trigram 候选里 (相同字符串共享所有 trigram)
134
+ - 候选为空则回退全部 keys (兜底, 罕见)
135
+
136
+ 关键: 不能在精确命中后短路 —— query "insurance" 还要匹配
137
+ "insurancepolicy" (query 是 kw 的子串), 所以必须返回 trigram 候选集。
138
+ """
139
+ wl = word.lower()
140
+ tgs = _trigrams(wl)
141
+ trigram_index = inverted.get("trigram", {})
142
+ candidate_sets = []
143
+ for tg in tgs:
144
+ keys = trigram_index.get(tg)
145
+ if keys:
146
+ candidate_sets.append(set(keys))
147
+
148
+ if not candidate_sets:
149
+ # 词太短无 trigram (如 2 字符), 回退精确 + 全扫描兜底
150
+ exact = inverted.get("exact", {}).get(wl)
151
+ if exact:
152
+ return list(exact)
153
+ return inverted.get("keywords", [])
154
+
155
+ # 取并集: query 含 kw 或 kw 含 query, 至少一个 trigram 重叠
156
+ candidates = set()
157
+ for cs in candidate_sets:
158
+ candidates |= cs
159
+
160
+ return sorted(candidates)
161
+
162
+
163
+ def search_keywords_fast(
164
+ words: List[str],
165
+ inverted: Optional[dict],
166
+ keyword_index: Dict[str, list],
167
+ ) -> Dict[str, list]:
168
+ """快速关键词搜索 (用倒排索引缩小候选集)。
169
+
170
+ Args:
171
+ words: 查询词列表 (已小写化、已 CN 扩展)。
172
+ inverted: 倒排索引 (None 则回退到全扫描)。
173
+ keyword_index: 原始 keyword-index (用于取 files)。
174
+
175
+ Returns:
176
+ {keyword_key: files} 匹配结果。
177
+ """
178
+ matches = {}
179
+
180
+ if inverted is None:
181
+ # 回退: 全扫描 (旧逻辑)
182
+ for word in words:
183
+ for kw, files in keyword_index.items():
184
+ if _match(word, kw):
185
+ matches[kw] = files
186
+ return matches
187
+
188
+ for word in words:
189
+ candidates = _candidate_keys(word, inverted)
190
+ for kw in candidates:
191
+ files = keyword_index.get(kw)
192
+ if files and _match(word, kw):
193
+ matches[kw] = files
194
+
195
+ return matches
196
+
197
+
198
+ def _match(query_word: str, kw: str) -> bool:
199
+ """与 search_index._match_keyword 一致的匹配逻辑。"""
200
+ w, k = query_word.lower(), kw.lower()
201
+ if w == k:
202
+ return True
203
+ if len(k) >= MIN_FUZZY_LEN and k in w:
204
+ return True
205
+ if len(w) >= MIN_FUZZY_LEN and w in k:
206
+ return True
207
+ return False