argo-search 1.0.1 → 2.9.7

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 (463) hide show
  1. package/LICENSE +201 -21
  2. package/README.en.md +822 -0
  3. package/README.es.md +864 -0
  4. package/README.ja.md +864 -0
  5. package/README.ko.md +864 -0
  6. package/README.md +867 -320
  7. package/SKILL.md +135 -0
  8. package/assets/readme/hero.svg +121 -0
  9. package/assets/readme/made-with-beautify.svg +10 -0
  10. package/assets/readme/proof-routes.svg +73 -0
  11. package/assets/readme/settings-keys.png +0 -0
  12. package/assets/readme/settings-tools.png +0 -0
  13. package/assets/readme/why-better.svg +104 -0
  14. package/assets/readme/workflow.svg +83 -0
  15. package/backends/domain_profiles.json +2308 -96
  16. package/backends/engine_registry.yaml +3314 -503
  17. package/backends/query_synonyms_cn.json +35 -0
  18. package/backends/quota_profiles.json +2409 -161
  19. package/backends/source_types_cn.json +83 -0
  20. package/backends/text_lexicon.json +44 -0
  21. package/bin/argo +703 -0
  22. package/bin/argo.js +71 -39
  23. package/config.yaml +4976 -363
  24. package/cordis.patch.yml +21 -0
  25. package/data/countries.json +40 -0
  26. package/data/lang_matrix/lang_capability.json +842 -0
  27. package/data/lang_matrix/matrix_2026-09-10.json +11921 -0
  28. package/data/regions_cn.json +1 -0
  29. package/docs/ADDING_NEW_ENGINE.md +184 -0
  30. package/docs/ARGO_INTRO.md +124 -0
  31. package/docs/DATA_SOURCE_GAPS.md +81 -0
  32. package/docs/DESIGN_LOCAL_DATA_FUSION.md +90 -0
  33. package/docs/ENGINE_CATALOG.md +564 -0
  34. package/docs/MCP_SETUP.md +106 -0
  35. package/docs/PLATFORM_PATHS.md +111 -0
  36. package/docs/RELEASE_NOTES_v2.8.0.md +71 -0
  37. package/docs/RELEASE_NOTES_v2.8.2.md +63 -0
  38. package/docs/RELEASE_NOTES_v2.8.3.md +55 -0
  39. package/docs/RELEASE_NOTES_v2.8.4.md +79 -0
  40. package/docs/RELEASE_NOTES_v2.8.5.md +102 -0
  41. package/docs/RELEASE_NOTES_v2.8.6.md +66 -0
  42. package/docs/RELEASE_NOTES_v2.8.7.md +65 -0
  43. package/docs/RELEASE_NOTES_v2.8.8.md +67 -0
  44. package/docs/RELEASE_NOTES_v2.8.9.md +67 -0
  45. package/docs/RELEASE_NOTES_v2.9.0.md +64 -0
  46. package/docs/SEARCH_ARCHIVE.md +115 -0
  47. package/docs/assets/architecture.svg +104 -0
  48. package/docs/assets/hero.svg +98 -0
  49. package/docs/engines/bangumi.md +27 -0
  50. package/docs/engines/biorxiv.md +27 -0
  51. package/docs/engines/clawhub.md +27 -0
  52. package/docs/engines/crt_sh.md +27 -0
  53. package/docs/engines/datacite.md +27 -0
  54. package/docs/engines/deps_dev.md +27 -0
  55. package/docs/engines/douban_movie.md +27 -0
  56. package/docs/engines/endoflife.md +27 -0
  57. package/docs/engines/flk_law.md +27 -0
  58. package/docs/engines/fxtwitter.md +27 -0
  59. package/docs/engines/google_news.md +27 -0
  60. package/docs/engines/gov_regulations.md +27 -0
  61. package/docs/engines/guqian.md +27 -0
  62. package/docs/engines/hackernews.md +27 -0
  63. package/docs/engines/iplant.md +27 -0
  64. package/docs/engines/keenable.md +27 -0
  65. package/docs/engines/met_museum.md +27 -0
  66. package/docs/engines/nasa_images.md +27 -0
  67. package/docs/engines/netease_music.md +27 -0
  68. package/docs/engines/npm.md +27 -0
  69. package/docs/engines/nvd.md +27 -0
  70. package/docs/engines/openreview.md +27 -0
  71. package/docs/engines/openstd.md +27 -0
  72. package/docs/engines/parallel_free.md +27 -0
  73. package/docs/engines/people_daily.md +27 -0
  74. package/docs/engines/realtime_index.md +27 -0
  75. package/docs/engines/sec_edgar.md +27 -0
  76. package/docs/engines/seltz.md +27 -0
  77. package/docs/engines/skillsmp.md +27 -0
  78. package/docs/engines/sspai.md +27 -0
  79. package/docs/engines/std_samr.md +27 -0
  80. package/docs/engines/train.md +27 -0
  81. package/docs/engines/un_comtrade.md +27 -0
  82. package/docs/engines/wikisource.md +27 -0
  83. package/docs/engines/zdic.md +27 -0
  84. package/docs/engines/zenodo.md +27 -0
  85. package/docs/engines/zhihu_hot_app.md +27 -0
  86. package/docs//344/270/272/344/273/200/344/271/210/351/200/211/346/213/251argo.md +176 -0
  87. package/engines/README.md +10 -0
  88. package/engines/_template_cli.yaml +39 -0
  89. package/engines/_template_http.yaml +37 -0
  90. package/engines/plugins/_template_plugin.py +50 -0
  91. package/engines/specs/_cisa_kev.yaml +54 -0
  92. package/engines/specs/_osv.yaml +63 -0
  93. package/engines/specs/aljazeera.yaml +43 -0
  94. package/engines/specs/antara.yaml +43 -0
  95. package/engines/specs/artic.yaml +20 -0
  96. package/engines/specs/aviation_weather.yaml +39 -0
  97. package/engines/specs/bangumi.yaml +21 -0
  98. package/engines/specs/biorxiv.yaml +24 -0
  99. package/engines/specs/carbon_intensity.yaml +32 -0
  100. package/engines/specs/cinii.yaml +44 -0
  101. package/engines/specs/clawhub.yaml +49 -0
  102. package/engines/specs/cleveland.yaml +19 -0
  103. package/engines/specs/cn_ai_news.yaml +43 -0
  104. package/engines/specs/core.yaml +35 -0
  105. package/engines/specs/crt_sh.yaml +38 -0
  106. package/engines/specs/ct24.yaml +43 -0
  107. package/engines/specs/datacite.yaml +27 -0
  108. package/engines/specs/datagov_uk.yaml +41 -0
  109. package/engines/specs/ddgs_images.yaml +45 -0
  110. package/engines/specs/deepseek_search.yaml +74 -0
  111. package/engines/specs/deezer.yaml +20 -0
  112. package/engines/specs/deps_dev.yaml +21 -0
  113. package/engines/specs/digup.yaml +36 -0
  114. package/engines/specs/doi.yaml +39 -0
  115. package/engines/specs/douban_movie.yaml +22 -0
  116. package/engines/specs/dw_news.yaml +47 -0
  117. package/engines/specs/egov_law.yaml +21 -0
  118. package/engines/specs/elpais.yaml +43 -0
  119. package/engines/specs/endoflife.yaml +21 -0
  120. package/engines/specs/energy_charts.yaml +21 -0
  121. package/engines/specs/factcheck_org.yaml +52 -0
  122. package/engines/specs/faz.yaml +43 -0
  123. package/engines/specs/federal_register.yaml +57 -0
  124. package/engines/specs/firecrawl.yaml +53 -0
  125. package/engines/specs/flk_law.yaml +24 -0
  126. package/engines/specs/folha.yaml +43 -0
  127. package/engines/specs/france24.yaml +44 -0
  128. package/engines/specs/full_fact.yaml +47 -0
  129. package/engines/specs/fxtwitter.yaml +31 -0
  130. package/engines/specs/gbfs_nyc.yaml +32 -0
  131. package/engines/specs/gdacs.yaml +21 -0
  132. package/engines/specs/gdelt.yaml +64 -0
  133. package/engines/specs/google_news.yaml +22 -0
  134. package/engines/specs/gov_regulations.yaml +38 -0
  135. package/engines/specs/guardian_rss.yaml +59 -0
  136. package/engines/specs/hurriyet.yaml +43 -0
  137. package/engines/specs/iplant.yaml +25 -0
  138. package/engines/specs/jikan.yaml +24 -0
  139. package/engines/specs/jolpica.yaml +20 -0
  140. package/engines/specs/k10plus.yaml +20 -0
  141. package/engines/specs/keenable.yaml +44 -0
  142. package/engines/specs/lefigaro.yaml +43 -0
  143. package/engines/specs/listenbrainz.yaml +19 -0
  144. package/engines/specs/matichon.yaml +43 -0
  145. package/engines/specs/met_museum.yaml +21 -0
  146. package/engines/specs/nasa_images.yaml +45 -0
  147. package/engines/specs/netease_music.yaml +38 -0
  148. package/engines/specs/nhk.yaml +43 -0
  149. package/engines/specs/nhtsa_vpic.yaml +20 -0
  150. package/engines/specs/noaa_swpc.yaml +20 -0
  151. package/engines/specs/nvd.yaml +38 -0
  152. package/engines/specs/obis.yaml +20 -0
  153. package/engines/specs/opencitations.yaml +52 -0
  154. package/engines/specs/openf1.yaml +21 -0
  155. package/engines/specs/openfoodfacts.yaml +45 -0
  156. package/engines/specs/openligadb.yaml +20 -0
  157. package/engines/specs/openreview.yaml +37 -0
  158. package/engines/specs/openstd.yaml +21 -0
  159. package/engines/specs/people_daily.yaml +22 -0
  160. package/engines/specs/realtime_index.yaml +34 -0
  161. package/engines/specs/ror.yaml +20 -0
  162. package/engines/specs/satnogs.yaml +20 -0
  163. package/engines/specs/sec_edgar.yaml +46 -0
  164. package/engines/specs/skillsmp.yaml +50 -0
  165. package/engines/specs/sogou_weixin_browser.yaml +30 -0
  166. package/engines/specs/soilgrids.yaml +25 -0
  167. package/engines/specs/sspai.yaml +36 -0
  168. package/engines/specs/stackexchange.yaml +46 -0
  169. package/engines/specs/std_samr.yaml +21 -0
  170. package/engines/specs/tass.yaml +43 -0
  171. package/engines/specs/tle_mirror.yaml +21 -0
  172. package/engines/specs/train.yaml +37 -0
  173. package/engines/specs/tvmaze.yaml +20 -0
  174. package/engines/specs/twitter_syndication.yaml +31 -0
  175. package/engines/specs/un_comtrade.yaml +23 -0
  176. package/engines/specs/unpaywall.yaml +54 -0
  177. package/engines/specs/vnpress.yaml +43 -0
  178. package/engines/specs/weather.yaml +32 -0
  179. package/engines/specs/who_don.yaml +21 -0
  180. package/engines/specs/who_gho.yaml +20 -0
  181. package/engines/specs/wikimedia_commons.yaml +34 -0
  182. package/engines/specs/wikisource.yaml +22 -0
  183. package/engines/specs/worms.yaml +19 -0
  184. package/engines/specs/x_trending.yaml +34 -0
  185. package/engines/specs/yna.yaml +43 -0
  186. package/engines/specs/ynet.yaml +43 -0
  187. package/engines/specs/zbmath.yaml +40 -0
  188. package/engines/specs/zdic.yaml +22 -0
  189. package/engines/specs/zenodo.yaml +27 -0
  190. package/engines/specs/zhihu_hot_app.yaml +35 -0
  191. package/mcp/clients.yaml +74 -0
  192. package/package.json +51 -12
  193. package/packages/dsh-plugin/README.md +134 -0
  194. package/packages/dsh-plugin/cordis.patch.yml +48 -0
  195. package/packages/dsh-plugin/dsh/client.js +1244 -0
  196. package/packages/dsh-plugin/dsh/index.js +2290 -0
  197. package/packages/dsh-plugin/dsh/native-tools.mjs +743 -0
  198. package/packages/dsh-plugin/package.json +38 -0
  199. package/packages/dsh-plugin/test/client.test.js +232 -0
  200. package/packages/dsh-plugin/test/config.test.js +30 -0
  201. package/packages/dsh-plugin/test/index.test.js +1338 -0
  202. package/references/query-templates.md +96 -0
  203. package/references/query-templates.yaml +118 -0
  204. package/scripts/ab_eval.py +219 -0
  205. package/scripts/ab_eval_p0p1.py +266 -0
  206. package/scripts/adaptive.py +271 -70
  207. package/scripts/answer.py +154 -0
  208. package/scripts/archive_run.py +591 -0
  209. package/scripts/argo_engine_registry.py +172 -0
  210. package/scripts/argo_paths.py +728 -0
  211. package/scripts/article.py +136 -0
  212. package/scripts/arxiv_source.py +201 -0
  213. package/scripts/badcase_ledger.py +252 -0
  214. package/scripts/batch_probe.py +241 -0
  215. package/scripts/benchmark.py +19 -0
  216. package/scripts/bounded_run.py +144 -0
  217. package/scripts/browser_auth.py +323 -0
  218. package/scripts/burstiness_detector.py +147 -0
  219. package/scripts/cache.py +1078 -67
  220. package/scripts/cache_guard.py +271 -0
  221. package/scripts/cache_key_vdom.py +56 -0
  222. package/scripts/cache_similarity.py +170 -0
  223. package/scripts/caliber_isolation.py +195 -0
  224. package/scripts/calibrate_lowquality.py +210 -0
  225. package/scripts/candidate_envelope.py +366 -0
  226. package/scripts/chrome_cdp.py +871 -0
  227. package/scripts/circuit_breaker.py +389 -0
  228. package/scripts/citations.py +334 -0
  229. package/scripts/cite_cli.py +59 -0
  230. package/scripts/cjk_tokens.py +95 -0
  231. package/scripts/clarify.py +47 -24
  232. package/scripts/cli_io.py +240 -0
  233. package/scripts/config.py +683 -45
  234. package/scripts/config_cache.py +402 -0
  235. package/scripts/conn_pool.py +220 -0
  236. package/scripts/content_security.py +685 -0
  237. package/scripts/content_signals.py +730 -0
  238. package/scripts/crawl.py +74 -35
  239. package/scripts/daemon.py +866 -0
  240. package/scripts/digup_bridge.py +214 -0
  241. package/scripts/engine_admission.py +366 -0
  242. package/scripts/engine_dispatch.py +1019 -0
  243. package/scripts/engine_env.py +534 -0
  244. package/scripts/engine_failure.py +411 -0
  245. package/scripts/engine_families.py +732 -0
  246. package/scripts/engine_policy.py +218 -0
  247. package/scripts/engine_requires.py +190 -0
  248. package/scripts/engine_status.py +322 -0
  249. package/scripts/engine_validate.py +638 -0
  250. package/scripts/engines.py +778 -294
  251. package/scripts/engines_base.py +1521 -0
  252. package/scripts/engines_builders.py +298 -0
  253. package/scripts/engines_builders_batch10.py +159 -0
  254. package/scripts/engines_builders_batch11.py +174 -0
  255. package/scripts/engines_builders_batch12.py +103 -0
  256. package/scripts/engines_builders_batch13.py +340 -0
  257. package/scripts/engines_builders_batch9.py +1468 -0
  258. package/scripts/engines_builders_cn.py +2283 -0
  259. package/scripts/engines_builders_data.py +2923 -0
  260. package/scripts/engines_builders_data_macro.py +775 -0
  261. package/scripts/engines_builders_feeds.py +255 -0
  262. package/scripts/engines_builders_intl.py +1239 -0
  263. package/scripts/engines_builders_search.py +440 -0
  264. package/scripts/engines_builders_tech.py +1360 -0
  265. package/scripts/evidence.py +588 -162
  266. package/scripts/evidence_loop.py +592 -0
  267. package/scripts/evidence_tier.py +298 -0
  268. package/scripts/except_sets.py +25 -0
  269. package/scripts/extract.py +55 -16
  270. package/scripts/fact_align.py +207 -0
  271. package/scripts/fact_crosscheck.py +230 -0
  272. package/scripts/fetch.py +25 -12
  273. package/scripts/fetch_browser.py +87 -0
  274. package/scripts/fetch_jsrun.py +122 -0
  275. package/scripts/fetch_quality.py +225 -0
  276. package/scripts/fetch_render_tinyfish.py +117 -0
  277. package/scripts/fetch_v3.py +1779 -0
  278. package/scripts/focus_extract.py +350 -0
  279. package/scripts/fulltext_store.py +169 -0
  280. package/scripts/fxtwitter_api.py +325 -0
  281. package/scripts/gen_engine_catalog.py +364 -0
  282. package/scripts/gen_native_tools.py +91 -0
  283. package/scripts/gen_routing_snapshot.py +184 -0
  284. package/scripts/health_check.py +140 -402
  285. package/scripts/health_probe.py +23 -8
  286. package/scripts/hot_state.py +134 -0
  287. package/scripts/http_client.py +1053 -0
  288. package/scripts/image_ops.py +430 -0
  289. package/scripts/image_vision/vision_probe +0 -0
  290. package/scripts/image_vision/vision_probe.swift +126 -0
  291. package/scripts/install.ps1 +148 -0
  292. package/scripts/install.sh +206 -0
  293. package/scripts/job.py +1186 -0
  294. package/scripts/jump_resolver.py +235 -0
  295. package/scripts/lang_capability.py +150 -0
  296. package/scripts/lang_detect.py +289 -0
  297. package/scripts/lang_matrix_probe.py +329 -0
  298. package/scripts/lang_pref.py +311 -0
  299. package/scripts/link_source.py +340 -0
  300. package/scripts/local_engines.py +321 -0
  301. package/scripts/local_image.py +970 -0
  302. package/scripts/local_seek.py +366 -0
  303. package/scripts/macro_countries.py +82 -0
  304. package/scripts/matrix_search_eval.py +1105 -0
  305. package/scripts/mcp_diag.py +8 -3
  306. package/scripts/mcp_handlers.py +1074 -0
  307. package/scripts/mcp_handlers_surface.py +157 -0
  308. package/scripts/mcp_launch.sh +122 -0
  309. package/scripts/mcp_server.py +118 -417
  310. package/scripts/mcp_setup.py +407 -0
  311. package/scripts/mcp_tools.py +330 -0
  312. package/scripts/mcp_transport.py +191 -0
  313. package/scripts/min_version_scan.py +335 -0
  314. package/scripts/net_proxy.py +258 -0
  315. package/scripts/network_aware.py +152 -0
  316. package/scripts/paper.py +274 -0
  317. package/scripts/pdf_extract.py +518 -0
  318. package/scripts/phrase_filter.py +66 -0
  319. package/scripts/plan.py +422 -0
  320. package/scripts/postprocess_signals.py +150 -0
  321. package/scripts/pre-commit +48 -0
  322. package/scripts/query_enhance.py +157 -0
  323. package/scripts/query_operators.py +196 -0
  324. package/scripts/query_rewriter.py +116 -7
  325. package/scripts/query_signals.py +300 -0
  326. package/scripts/query_templates.py +167 -0
  327. package/scripts/query_understanding.py +411 -0
  328. package/scripts/query_variants.py +298 -0
  329. package/scripts/quota.py +488 -76
  330. package/scripts/rank_signals.py +775 -0
  331. package/scripts/ranking_eval.py +319 -0
  332. package/scripts/readability_extract.py +433 -0
  333. package/scripts/recompute.py +415 -0
  334. package/scripts/recovery.py +616 -0
  335. package/scripts/recovery_l0.py +160 -0
  336. package/scripts/redskill/redskill_engine.py +332 -0
  337. package/scripts/regression_p0p1.py +318 -0
  338. package/scripts/replay_eval.py +640 -0
  339. package/scripts/rerank_tfidf.py +225 -0
  340. package/scripts/research.py +591 -403
  341. package/scripts/research_cli.py +361 -0
  342. package/scripts/research_dossier.py +492 -0
  343. package/scripts/research_expand.py +146 -0
  344. package/scripts/research_gates.py +272 -0
  345. package/scripts/research_report.py +209 -0
  346. package/scripts/research_strategy.py +37 -0
  347. package/scripts/research_work_packages.py +195 -0
  348. package/scripts/result_lang.py +311 -0
  349. package/scripts/robots_guard.py +259 -0
  350. package/scripts/route.py +921 -254
  351. package/scripts/route_bangs.py +40 -0
  352. package/scripts/route_cache.py +225 -0
  353. package/scripts/route_combo.py +910 -0
  354. package/scripts/route_domains.py +328 -0
  355. package/scripts/route_lang.py +605 -0
  356. package/scripts/route_log.py +63 -0
  357. package/scripts/route_policy.py +250 -0
  358. package/scripts/rrf_classic.py +66 -0
  359. package/scripts/screenshot.py +73 -0
  360. package/scripts/search.py +897 -405
  361. package/scripts/search_attribution.py +88 -0
  362. package/scripts/search_benchmark.py +407 -0
  363. package/scripts/search_cli.py +627 -0
  364. package/scripts/search_entry.py +389 -0
  365. package/scripts/search_output.py +606 -0
  366. package/scripts/search_pipeline.py +1077 -0
  367. package/scripts/search_rank.py +991 -0
  368. package/scripts/search_types.py +27 -12
  369. package/scripts/seek_locator.py +68 -0
  370. package/scripts/semantic_evidence.py +215 -0
  371. package/scripts/serp_guard.py +228 -0
  372. package/scripts/serp_kill_log.py +51 -0
  373. package/scripts/single_flight.py +120 -0
  374. package/scripts/social_engines/__init__.py +10 -0
  375. package/scripts/social_engines/bilibili_engine.py +131 -0
  376. package/scripts/social_engines/hackernews_engine.py +113 -0
  377. package/scripts/social_engines/reddit_engine.py +172 -0
  378. package/scripts/social_engines/twitter_engine.py +397 -0
  379. package/scripts/social_engines/v2ex_engine.py +101 -0
  380. package/scripts/social_engines/weibo_engine.py +128 -0
  381. package/scripts/social_engines/xiaohongshu_engine.py +137 -0
  382. package/scripts/social_engines/zhihu_engine.py +139 -0
  383. package/scripts/social_research.py +201 -0
  384. package/scripts/sogou_weixin_browser.py +318 -0
  385. package/scripts/source_ledger.py +249 -0
  386. package/scripts/stage_timing.py +58 -0
  387. package/scripts/stats_cli.py +95 -0
  388. package/scripts/straggler_policy.py +60 -0
  389. package/scripts/stylometry_detector.py +231 -0
  390. package/scripts/sync_backends.py +353 -0
  391. package/scripts/sync_installs.py +29 -0
  392. package/scripts/template_cli.py +127 -0
  393. package/scripts/text_lexicon.py +72 -0
  394. package/scripts/tfidf_router.py +109 -37
  395. package/scripts/time_utils.py +233 -0
  396. package/scripts/topic_research_profiles.py +643 -0
  397. package/scripts/train.py +737 -0
  398. package/scripts/tweet.py +596 -0
  399. package/scripts/url_canon.py +217 -0
  400. package/scripts/url_optimize.py +56 -0
  401. package/scripts/url_safety.py +236 -0
  402. package/scripts/usage_log.py +116 -0
  403. package/scripts/usage_log_clean.py +113 -0
  404. package/scripts/v2ex_nodes.py +311 -0
  405. package/scripts/variant_recall.py +188 -0
  406. package/scripts/watch.py +205 -0
  407. package/scripts/wechat_article.py +258 -0
  408. package/scripts/wide_research.py +402 -0
  409. package/scripts/wx.py +349 -0
  410. package/scripts/x_article_fetch.py +141 -0
  411. package/scripts/x_search.py +234 -0
  412. package/scripts/x_trending.py +189 -0
  413. package/scripts/yaml_load.py +67 -0
  414. package/sub-skills/ego-search/SKILL.md +253 -0
  415. package/sub-skills/ego-search/references/browser-runtime.md +43 -0
  416. package/sub-skills/ego-search/references/install.md +67 -0
  417. package/sub-skills/ego-search/references/original-ego-upgrade.md +51 -0
  418. package/sub-skills/ego-search/scripts/ego_search.py +800 -0
  419. package/sub-skills/ego-search/scripts/merge.py +226 -0
  420. package/sub-skills/ego-search/scripts/quality.py +64 -0
  421. package/sub-skills/ego-search/scripts/runtime.py +172 -0
  422. package/sub-skills/ego-search/scripts/safety.py +109 -0
  423. package/sub-skills/ego-search/scripts/serp_spec.py +109 -0
  424. package/sub-skills/ego-search/scripts/webbridge_adapter.py +374 -0
  425. package/sub-skills/ego-search/tests/test_query_normalize.py +68 -0
  426. package/sub-skills/js-run/SKILL.md +67 -0
  427. package/sub-skills/js-run/scripts/jsrun.py +112 -0
  428. package/sub-skills/js-run/scripts/shim.js +188 -0
  429. package/sub-skills/js-run/tests/fixtures/challenge_like.js +27 -0
  430. package/sub-skills/js-run/tests/test_jsrun.py +165 -0
  431. package/sub-skills/local-search/SKILL.md +50 -53
  432. package/sub-skills/local-search/bm25.py +162 -0
  433. package/sub-skills/local-search/config.yaml +152 -55
  434. package/sub-skills/local-search/engine_registry.py +91 -26
  435. package/sub-skills/local-search/{health_check.py → local_health_check.py} +140 -10
  436. package/sub-skills/local-search/local_search_adapter.py +13 -2
  437. package/sub-skills/local-search/parse_maps.yaml +40 -42
  438. package/sub-skills/local-search/search_v3.py +1182 -65
  439. package/sub-skills/local-search/smart_router.py +47 -7
  440. package/sub-skills/local-seek/SKILL.md +110 -0
  441. package/sub-skills/local-seek/config/domains.yaml +20 -0
  442. package/sub-skills/local-seek/references/auto-behaviors.md +24 -0
  443. package/sub-skills/local-seek/references/fd-mdfind-recipes.md +62 -0
  444. package/sub-skills/local-seek/references/git-integration.md +54 -0
  445. package/sub-skills/local-seek/references/remote-handoff.md +46 -0
  446. package/sub-skills/local-seek/references/rg-recipes.md +96 -0
  447. package/sub-skills/local-seek/references/strategies.md +52 -0
  448. package/sub-skills/local-seek/references/structural-search.md +59 -0
  449. package/sub-skills/local-seek/scripts/eval_rank.py +216 -0
  450. package/sub-skills/local-seek/scripts/eval_seek.py +224 -0
  451. package/sub-skills/local-seek/scripts/seek.py +1465 -0
  452. package/sub-skills/local-seek/scripts/seek_frecency.py +126 -0
  453. package/sub-skills/local-seek/scripts/seek_fts.py +430 -0
  454. package/sub-skills/local-seek/scripts/seek_rank.py +193 -0
  455. package/sub-skills/local-seek/tests/test_content_rank.py +222 -0
  456. package/sub-skills/local-seek/tests/test_determinism_20261006.py +145 -0
  457. package/sub-skills/local-seek/tests/test_noise_tier.py +101 -0
  458. package/sub-skills/local-seek/tests/test_pcre2_probe.py +136 -0
  459. package/sub-skills/local-seek/tests/test_rank_pinyin.py +77 -0
  460. package/sub-skills/local-seek/tests/test_regression_20261003.py +169 -0
  461. package/sub-skills/local-seek/tests/test_regression_20261006.py +146 -0
  462. package/sub-skills/local-seek/tests/test_seek_degradation.py +111 -0
  463. package/sub-skills/local-seek/tests/test_seek_fts.py +313 -0
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env python3
2
+ """usage_log_clean — 生产使用日志的夹具污染隔离(维护工具,隔离不删除)。
3
+
4
+ 背景(2026-10-10 侦察轮发现、10-11 实测定性):本仓 benchmark/评测曾直写
5
+ 生产状态目录,query.jsonl 的 982 条真实记录混入 334 条夹具引擎条目
6
+ (slow_primary / fast_garbage / g1 / fast_backup 等),P50/P90 与引擎频次
7
+ 统计全部失真。脚本侧已于 10-10 接 isolate_state_dir(防新增);本工具清
8
+ 历史存量——夹具行原样移入 <stream>.jsonl.quarantine-<ts>,主文件只留
9
+ 真实条目,数据零丢失。
10
+
11
+ 引擎名真值来源:engines.available_engines()(与路由同源,253 个)。
12
+ engines_used 出现任何表外名即判夹具。两处历史别名显式豁免(引擎改名前的
13
+ 旧条目,不是夹具):openlibrary→open_library、bing→bing_rss。
14
+ recovery.jsonl 无 engines_used 字段,2026-10-11 全文扫描零夹具命中,
15
+ 不在本工具处理范围。
16
+
17
+ 用法(建议 argo 空闲时跑;写回前会重读并把窗口内新追加的行接回 kept,
18
+ 不再静默丢失——但仍建议停掉 daemon/MCP 等持续写入方,避免夹具判定
19
+ 跨窗口不一致):
20
+ python3 scripts/usage_log_clean.py # 干跑:只报告不动文件
21
+ python3 scripts/usage_log_clean.py --apply # 实际隔离
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import argparse
26
+ import json
27
+ import sys
28
+ import time
29
+ from collections import Counter
30
+ from pathlib import Path
31
+
32
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
33
+
34
+ from engines import available_engines # noqa: E402
35
+ from usage_log import usage_log_dir # noqa: E402
36
+ import argo_paths # noqa: E402
37
+
38
+ # 引擎改名前的历史别名:表外但真实,豁免隔离(维护者注释留在账上)
39
+ _ALIAS_KEEP = frozenset({"openlibrary", "bing"})
40
+
41
+ _STREAM = "query"
42
+
43
+
44
+ def _fixture_engines(used: list) -> list[str]:
45
+ """一条记录的表外引擎名(别名豁免后)。"""
46
+ return [e for e in used if e not in _ALIAS_KEEP and e not in _CANON]
47
+
48
+
49
+ _CANON = frozenset(available_engines())
50
+
51
+
52
+ def _clean(path: Path, apply: bool) -> dict:
53
+ lines = path.read_text(encoding="utf-8").splitlines() if path.exists() else []
54
+ kept: list[str] = []
55
+ quarantine: list[str] = []
56
+ bad_names: Counter = Counter()
57
+ for line in lines:
58
+ if not line.strip():
59
+ continue
60
+ try:
61
+ used = json.loads(line).get("engines_used") or []
62
+ except json.JSONDecodeError:
63
+ kept.append(line) # 读不动的行不是隔离对象,原样保留
64
+ continue
65
+ bad = _fixture_engines(used)
66
+ if bad:
67
+ bad_names.update(bad)
68
+ quarantine.append(line)
69
+ else:
70
+ kept.append(line)
71
+ report = {
72
+ "stream": _STREAM,
73
+ "path": str(path),
74
+ "total": len(kept) + len(quarantine),
75
+ "kept": len(kept),
76
+ "quarantined": len(quarantine),
77
+ "fixture_engines": dict(bad_names.most_common()),
78
+ "applied": False,
79
+ }
80
+ if apply and quarantine:
81
+ # 写回前重读一次,把「快照读」与「原子写」之间并发追加的新行接在
82
+ # kept 之后——否则这段窗口里落进的真实查询会被静默吞掉(旧实现
83
+ # 整文件替换为读时的旧视图,docstring 的错误承诺正好相反)。
84
+ # 追加行夹在既有行之后,无法用「是否为快照行」区分,只能按行数增量
85
+ # 取尾部;夹具行本工具只报一次,多余不影响数据(它们仍会进下次账)。
86
+ try:
87
+ current = path.read_text(encoding="utf-8").splitlines()
88
+ except OSError:
89
+ current = lines
90
+ appended = current[len(lines):] if len(current) > len(lines) else []
91
+ kept = kept + [ln for ln in appended if ln.strip()]
92
+ stamp = time.strftime("%Y%m%d-%H%M%S")
93
+ q_path = path.with_name(f"{path.name}.quarantine-{stamp}")
94
+ argo_paths.atomic_write_text(q_path, "\n".join(quarantine) + "\n")
95
+ argo_paths.atomic_write_text(path, "\n".join(kept) + "\n")
96
+ report["applied"] = True
97
+ report["quarantine_path"] = str(q_path)
98
+ report["appended_kept"] = len(appended)
99
+ return report
100
+
101
+
102
+ def main(argv: list[str] | None = None) -> int:
103
+ ap = argparse.ArgumentParser(description="使用日志夹具污染隔离(维护工具)")
104
+ ap.add_argument("--apply", action="store_true",
105
+ help="实际执行隔离;默认干跑只报告")
106
+ args = ap.parse_args(argv)
107
+ path = usage_log_dir() / f"{_STREAM}.jsonl"
108
+ print(json.dumps(_clean(path, apply=args.apply), ensure_ascii=False))
109
+ return 0
110
+
111
+
112
+ if __name__ == "__main__":
113
+ sys.exit(main())
@@ -0,0 +1,311 @@
1
+ #!/usr/bin/env python3
2
+ """v2ex_nodes.py — V2EX 节点路由:把「全站热帖过滤」升级为「相关节点内检索」。
3
+
4
+ ## 问题(第一批的遗留)
5
+
6
+ 第一批把 V2EX 引擎从「抓 /search 页产出幻觉」改成「hot+latest 候选池 + 本地
7
+ 相关性过滤」。但候选池只有 10+10=20 条**全站热帖**,与查询无关,导致长尾
8
+ 查询必然落空(实测 MacBook/招聘 返回 0 条)。
9
+
10
+ 当时判定「官方 API 无搜索端点,只能这样」——**这个退让是错的**。
11
+
12
+ ## 重新定义
13
+
14
+ 官方 API 确实没有搜索,但有 `show.json?node_name=X&page=N`:**按节点取帖**。
15
+ 实测 V2EX 有 1376 个节点,其中 1246 个有内容,节点名 99% 是英文实体词
16
+ (python/docker/iphone/paper…)。于是真实问题不是「如何搜索」,而是:
17
+
18
+ 「如何把用户查询映射到节点空间」
19
+
20
+ ## 为什么是倒排而不是模糊匹配
21
+
22
+ 实测:节点名 99% 是英文实体词,且节点自带 title(中文标题)与 header
23
+ (语义描述)。所以 `查询词元 ∩ 节点名` 就是**精确命中**——不需要算相似度、
24
+ 不需要 embedding。这消除了「准确 vs 通用」的假两难。
25
+
26
+ ## 三层递进(每层失败即降级,不阻断)
27
+
28
+ L1 精确倒排 查询词元 ∩ 节点名 conf=1.0 零 API
29
+ L2 同义/中英扩展 复用 argo query_synonyms_cn conf=0.8 零 API
30
+ L3 header 语义保底 L1/L2 全空时在描述里找词 conf=0.5 零 API
31
+
32
+ 三层全空 = 该查询在 V2EX 确无对应资源,**诚实返回空**,由调用方降级到
33
+ hot/latest 保底(而不是伪造结果)。
34
+
35
+ ## 成本
36
+
37
+ 节点表 1376 条 ≈ 200KB,变化极慢 → 本地缓存 24h。匹配阶段**零 API 调用**。
38
+ 取帖按 Top-K 节点并发,K=3 时占用配额 3/600 = 0.5%。
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import json
44
+ import re
45
+ import threading
46
+ import time
47
+ from pathlib import Path
48
+ from typing import Any
49
+
50
+ from cjk_tokens import cjk_term_grams
51
+
52
+ # 节点表缓存
53
+ _CACHE_TTL = 24 * 3600 # 24h:节点表变化极慢
54
+ _CACHE_FILE = "v2ex_nodes_cache.json"
55
+
56
+ # 单字/双字节点名不参与匹配:实测 "c"/"io"/"u"/"in" 等单字节点会造成噪声命中。
57
+ # 这是**格式约束**(英文单字几乎必然误命中任意查询词元),不是质量判断。
58
+ MIN_NODE_NAME_LEN = 3
59
+
60
+ # 注意:这里曾尝试加「节点热度门槛」(topics >= N)过滤冷门节点,已废弃。
61
+ # 实测数据否定了这类「节点固有属性」判据:
62
+ # · 节点主题数中位数仅 15,一半节点 <15 —— 热度不存在自然分界
63
+ # · u(大学)/in(分享邀请码)/pro(PRO) 都「名实相符」—— 一致性也不能区分
64
+ # 根本原因:节点好坏不是固有属性,而是「节点 × 查询」的联合属性。
65
+ # 同一个 paper 节点,查「论文」是好节点、查「数据库」是坏节点。
66
+ # 正确做法是把判据后移到「结果」:路由只给候选,好坏由既有相关性过滤
67
+ # (_term_hits)决定——命中冷门节点则池小、过滤后为空、诚实返回;
68
+ # 命中热门节点则池大、过滤后命中。分工清晰,无需在路由层设阈值。
69
+
70
+ # 默认取几个节点的帖子(并发)。3 个 = 30 条候选,配额占用 0.5%
71
+ DEFAULT_TOP_K = 3
72
+
73
+ # 节点表完整性下限:低于此数视为「上游半截响应/解析异常」,不写缓存。
74
+ # V2EX 实际 1300+ 节点,测试 fixture 通常只有几条,用它区分真假数据。
75
+ MIN_NODE_TABLE_SIZE = 100
76
+
77
+ _lock = threading.Lock()
78
+ _mem_cache: dict[str, Any] = {}
79
+
80
+
81
+ def _cache_path() -> Path:
82
+ try:
83
+ import argo_paths
84
+ return argo_paths.state_path(_CACHE_FILE)
85
+ except Exception:
86
+ return Path.home() / ".cache" / "unified-search" / _CACHE_FILE
87
+
88
+
89
+ def _load_cached_nodes() -> list[dict] | None:
90
+ """读本地节点表缓存;过期或损坏返回 None。"""
91
+ p = _cache_path()
92
+ try:
93
+ st = p.stat()
94
+ except OSError:
95
+ return None
96
+ if time.time() - st.st_mtime > _CACHE_TTL:
97
+ return None
98
+ try:
99
+ data = json.loads(p.read_text(encoding="utf-8"))
100
+ nodes = data.get("nodes") if isinstance(data, dict) else None
101
+ if isinstance(nodes, list) and nodes:
102
+ return nodes
103
+ except Exception:
104
+ pass
105
+ return None
106
+
107
+
108
+ def _save_nodes(nodes: list[dict]) -> None:
109
+ """写节点表缓存。
110
+
111
+ 防御:节点数低于完整性下限时不写。V2EX 实际有 1300+ 节点,若某次
112
+ 只解析出极少数(上游半截响应 / 测试注入的 fixture / 解析逻辑变更),
113
+ 写进缓存会把不完整数据固化 24h —— 实测曾因测试写入 1 个假节点,
114
+ 导致后续所有真实查询路由失败(缓存里只有 python 一个节点)。
115
+ """
116
+ if len(nodes) < MIN_NODE_TABLE_SIZE:
117
+ return
118
+ p = _cache_path()
119
+ try:
120
+ # 原子写走唯一来源(唯一 tmp 名)——旧实现固定 `.tmp` 名,
121
+ # 并发进程互相搬走临时文件导致写失败。
122
+ import argo_paths as _paths
123
+ _paths.atomic_write_json(p, {"nodes": nodes}, indent=None)
124
+ except Exception:
125
+ pass # 缓存是加速层,写失败不影响功能
126
+
127
+
128
+ def fetch_all_nodes(fetcher=None, engine: str = "v2ex") -> list[dict]:
129
+ """取全节点表(带 24h 本地缓存 + 进程内缓存)。
130
+
131
+ fetcher(url) -> raw text,默认走 argo 统一 GET 出口。
132
+ 只保留有内容的节点(topics>0):空节点取不到帖子,参与匹配无意义。
133
+ engine:默认 fetcher 的归因归属(缺失会把失败记到伪引擎 "?" 名下)。
134
+ """
135
+ global _mem_cache
136
+ with _lock:
137
+ cached = _load_cached_nodes()
138
+ if cached:
139
+ return cached
140
+ if _mem_cache.get("nodes"):
141
+ return _mem_cache["nodes"]
142
+
143
+ if fetcher is None:
144
+ def fetcher(url): # type: ignore
145
+ from engines_base import _http_get_raw
146
+ return _http_get_raw(url, {"Accept": "application/json"}, 10,
147
+ engine=engine)
148
+
149
+ try:
150
+ raw = fetcher("https://www.v2ex.com/api/nodes/all.json")
151
+ if not raw:
152
+ return []
153
+ data = json.loads(raw)
154
+ except Exception:
155
+ return []
156
+
157
+ if not isinstance(data, list):
158
+ return []
159
+ nodes = [
160
+ n for n in data
161
+ if isinstance(n, dict) and n.get("name")
162
+ and int(n.get("topics") or 0) > 0
163
+ ]
164
+ if nodes:
165
+ _save_nodes(nodes)
166
+ with _lock:
167
+ _mem_cache["nodes"] = nodes
168
+ return nodes
169
+
170
+
171
+ def _eligible(n: dict) -> bool:
172
+ """节点是否可参与路由:仅要求名字长度达标。
173
+
174
+ 这是**格式约束**而非质量判断:英文单字节点(c/io/u/in)几乎必然
175
+ 误命中任意查询词元,属结构性噪声,故排除。
176
+ 刻意不做热度/名实一致等「固有属性」筛选——见文件头部说明,
177
+ 节点好坏是「节点 × 查询」的联合属性,路由层无法预判。
178
+ """
179
+ return len((n.get("name") or "")) >= MIN_NODE_NAME_LEN
180
+
181
+
182
+ def _tokenize(query: str) -> set[str]:
183
+ """查询 → 拉丁词元集合(长度门槛见 MIN_NODE_NAME_LEN)。"""
184
+ toks = set(re.findall(r"[a-z0-9]+", (query or "").lower()))
185
+ return {t for t in toks if len(t) >= MIN_NODE_NAME_LEN}
186
+
187
+
188
+ def _expand_synonyms(query: str) -> set[str]:
189
+ """复用 argo 已有的中英同义表做扩展(不新建维护面)。
190
+
191
+ 表在 backends/query_synonyms_cn.json,含 synonyms(中→多语) 与
192
+ en_to_cn(英→中)。这里只取「中文意图 → 英文词面」方向,
193
+ 用于把「论文」扩成 paper、「教程」扩成 guide。
194
+ """
195
+ out: set[str] = set()
196
+ q = (query or "").lower()
197
+ if not q:
198
+ return out
199
+ try:
200
+ p = Path(__file__).resolve().parent.parent / "backends" / "query_synonyms_cn.json"
201
+ except Exception:
202
+ return out
203
+ try:
204
+ data = json.loads(p.read_text(encoding="utf-8"))
205
+ except Exception:
206
+ return out
207
+ syn = data.get("synonyms") or {}
208
+ for cn, eqs in syn.items():
209
+ if cn and cn in q and isinstance(eqs, list):
210
+ for e in eqs:
211
+ out.update(re.findall(r"[a-z0-9]+", str(e).lower()))
212
+ return {t for t in out if len(t) >= MIN_NODE_NAME_LEN}
213
+
214
+
215
+ def pick_nodes(query: str, *, top_k: int = DEFAULT_TOP_K,
216
+ nodes: list[dict] | None = None,
217
+ fetcher=None) -> dict[str, Any]:
218
+ """查询 → 节点列表(三层递进)。
219
+
220
+ 返回 {"nodes": [name...], "confidence": float, "layer": str, "scores": {...}}
221
+ layer: exact / synonym / header / none
222
+ 无命中时返回 nodes=[] + layer="none",调用方据此降级到 hot/latest。
223
+ """
224
+ nodes = nodes if nodes is not None else fetch_all_nodes(fetcher=fetcher)
225
+ if not nodes:
226
+ return {"nodes": [], "confidence": 0.0, "layer": "none", "scores": {}}
227
+
228
+ q = (query or "").lower()
229
+ terms = _tokenize(q)
230
+
231
+ # ── L1 精确倒排:查询词元 ∩ 节点名 ────────────────────────────────
232
+ hits: dict[str, float] = {}
233
+ for n in nodes:
234
+ if not _eligible(n):
235
+ continue
236
+ name = (n.get("name") or "").lower()
237
+ if name in terms:
238
+ # 节点名被查询直接引用 = 最强信号;热度作次级权重避免大节点垄断
239
+ hits[name] = 10.0 + min(int(n.get("topics") or 0) / 100000, 1.0)
240
+ if hits:
241
+ ranked = sorted(hits.items(), key=lambda x: -x[1])
242
+ return {"nodes": [n for n, _ in ranked[:top_k]], "confidence": 1.0,
243
+ "layer": "exact", "scores": dict(ranked[:top_k])}
244
+
245
+ # ── L2 同义/中英扩展后倒排 ────────────────────────────────────────
246
+ expanded = terms | _expand_synonyms(q)
247
+ if expanded != terms:
248
+ for n in nodes:
249
+ if not _eligible(n):
250
+ continue
251
+ name = (n.get("name") or "").lower()
252
+ if name in expanded:
253
+ hits[name] = 8.0 + min(int(n.get("topics") or 0) / 100000, 1.0)
254
+ if hits:
255
+ ranked = sorted(hits.items(), key=lambda x: -x[1])
256
+ return {"nodes": [n for n, _ in ranked[:top_k]], "confidence": 0.8,
257
+ "layer": "synonym", "scores": dict(ranked[:top_k])}
258
+
259
+ # ── L2b 中文标题反查 ──────────────────────────────────────────────
260
+ # 节点的 title/header 是中文(jobs=「酷工作」、rent=「租房」、
261
+ # shanghai=「上海」),可把中文查询意图反查到英文节点名。
262
+ # 实测:「工作」→jobs/career/remote 有效;「招聘」→ 无(词面差异,
263
+ # 此时诚实回落 L3/none,不硬凑)。
264
+ # 要求 CJK 词长 ≥2,避免单字(「上」「大」)噪声;长串按 2-gram 展开
265
+ # (cjk_tokens),否则「怎么找工作」整句当一个词,与「酷工作」永不交集。
266
+ cjk_terms = set(cjk_term_grams(q))
267
+ if cjk_terms:
268
+ for n in nodes:
269
+ if not _eligible(n):
270
+ continue
271
+ name = (n.get("name") or "").lower()
272
+ blob = f"{n.get('title') or ''} {n.get('header') or ''}"
273
+ hit = sum(1 for t in cjk_terms if t in blob)
274
+ if hit:
275
+ hits[name] = float(hit) * 2.0 + min(
276
+ int(n.get("topics") or 0) / 100000, 1.0)
277
+ if hits:
278
+ ranked = sorted(hits.items(), key=lambda x: -x[1])
279
+ return {"nodes": [n for n, _ in ranked[:top_k]], "confidence": 0.7,
280
+ "layer": "cn_title", "scores": dict(ranked[:top_k])}
281
+
282
+ # ── L3 header 语义保底(低置信)────────────────────────────────────
283
+ # 只在描述文本里找词;要求词长 ≥4 以免通用短词误命中
284
+ probe = {t for t in (terms | _expand_synonyms(q)) if len(t) >= 4}
285
+ if probe:
286
+ for n in nodes:
287
+ if not _eligible(n):
288
+ continue
289
+ header = (n.get("header") or "").lower()
290
+ title = (n.get("title") or "").lower()
291
+ blob = f"{header} {title}"
292
+ hit = sum(1 for t in probe if t in blob)
293
+ if hit:
294
+ name = (n.get("name") or "").lower()
295
+ hits[name] = float(hit) + min(int(n.get("topics") or 0) / 100000, 1.0)
296
+ if hits:
297
+ ranked = sorted(hits.items(), key=lambda x: -x[1])
298
+ return {"nodes": [n for n, _ in ranked[:top_k]], "confidence": 0.5,
299
+ "layer": "header", "scores": dict(ranked[:top_k])}
300
+
301
+ return {"nodes": [], "confidence": 0.0, "layer": "none", "scores": {}}
302
+
303
+
304
+ def clear_cache() -> None:
305
+ """清空内存与磁盘节点缓存(测试与强制刷新用)。"""
306
+ with _lock:
307
+ _mem_cache.clear()
308
+ try:
309
+ _cache_path().unlink(missing_ok=True)
310
+ except Exception:
311
+ pass
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env python3
2
+ """variant_recall.py — multi-query 变体召回波(Step 1,argo 普适性改进)。
3
+
4
+ 问题:argo 主搜索路径只发**单个** retrieval_query,模糊/新颖 query 无法通过多路
5
+ 召回补漏——召回天花板 = 单串 × 引擎自身改写能力。query_enhance.retrieval_variants
6
+ 早已写好(归一化 + 拆型号 + 同义变体),却从没接到主路径(此前只在 research /
7
+ 个别付费引擎内部用)。本模块把它接进主路径。
8
+
9
+ 接法(最小且隔离):主 query 召回不足时,用变体对 combo 内主引擎补充召回,结果
10
+ 作为**独立 ranked list** 并入 clean_lists,让 rrf_merge 按「多 query 共识」天然
11
+ 加权(同一 URL 被多变体命中 → RRF 分累加 → 排更前)。
12
+
13
+ 隔离设计(为何绕开 _run_one 直接用 engine_search):
14
+ - 不走熔断/负缓存/per-engine 记账——变体 query 的失败不该污染主引擎健康,
15
+ 也不该把「这个变体没结果」固化成主 query 的负缓存;
16
+ - skip_cache=True:变体结果不读不写缓存,避免缓存键维度膨胀;
17
+ - 失败安全:单条变体/单引擎异常整体吞掉,绝不影响主 query 结果。
18
+
19
+ 成本受控(多 query 时收敛引擎数是通行做法):最轻量预算档 mode=fast|budget
20
+ 不触发;仅主 query 召回不足才补;最多 2 变体 × combo 前 2 引擎,超时收紧到 ≤6s。
21
+
22
+ 为何独立成模块:search_pipeline.py 受 1000 行门禁约束(见 test_module_size_gate),
23
+ 新增召回机制应拆出而非塞进融合管线。本模块**不** import search_pipeline(避免循环),
24
+ 以 duck typing 消费 req.retrieval_query / req.engines / req.max_results / req.eff_timeout
25
+ / req.since_iso / req.until_iso / req.mode / req.depth 等字段。
26
+ """
27
+ from __future__ import annotations
28
+
29
+ import re
30
+ from typing import Any
31
+
32
+ _VARIANT_MAX_QUERIES = 2
33
+ _VARIANT_MAX_ENGINES = 2
34
+ _VARIANT_TIMEOUT_CAP_S = 6.0
35
+ # 波级总预算(秒):per-call 超时盖不住 2×2 串行累计(最坏 4×6s=24s),而主调度
36
+ # 的 auto 预算不含这段追加——必须自设总闸。超线即停,已拿到的部分照常并入。
37
+ _WAVE_BUDGET_S = 8.0
38
+
39
+
40
+ def should_variant_recall(req: Any, clean_lists: list, max_results: int) -> bool:
41
+ """是否触发 multi-query 变体召回波。
42
+
43
+ 两条硬边界(抽成纯函数供回归门直接锁):
44
+ - 最轻量预算档 mode=fast|budget 不触发——时延可预期优先;
45
+ - 仅主 query 召回不足(clean_lists 结果总数 < max_results)才补——结果
46
+ 够就零成本不动,不够才用变体补召回。
47
+
48
+ 刻意**不**按 depth=fast 拦截:默认调用是 mode=auto + depth=fast,而 auto 有
49
+ 10s 总预算(远宽于 mode=fast 的 6s),足以容纳「召回不足才补」的少量变体
50
+ 调用。若连默认路径都拦掉,multi-query 对普通用户永不生效,普适性提升落空。
51
+
52
+ 运维 kill-switch:ARGO_MULTI_QUERY=0 一键回滚,也便于无侵入 A/B 对照。
53
+ """
54
+ try:
55
+ from engine_env import env_flag
56
+ if not env_flag("ARGO_MULTI_QUERY", default=True):
57
+ return False
58
+ except Exception:
59
+ pass # env_flag 不可用时按默认开启,不阻断搜索
60
+ if req.mode in ("fast", "budget"):
61
+ return False
62
+ # 答案型域 route 会下发 early_stop_min_results(1-2 行快照即完整答案):
63
+ # 完整性判据按它算,否则这类域每次搜索都白付变体调用。
64
+ need = max_results
65
+ dec = getattr(req, "decision", None)
66
+ if isinstance(dec, dict):
67
+ m = dec.get("early_stop_min_results")
68
+ if isinstance(m, int) and 0 < m <= max_results:
69
+ need = m
70
+ return sum(len(c) for c in clean_lists) < need
71
+
72
+
73
+ def variant_recall_wave(req: Any, engine_search: Any) -> list[list[dict[str, Any]]]:
74
+ """主 query 召回不足时的 multi-query 补充波;返回要追加的 ranked lists。
75
+
76
+ 返回空列表表示「无需补或无处补」,调用方直接沿用主 query 的 clean_lists。
77
+ 单条变体/单引擎异常整体吞掉(失败安全),不向上抛。
78
+ """
79
+ try:
80
+ from query_enhance import retrieval_variants
81
+ except ImportError:
82
+ return []
83
+ base = req.retrieval_query
84
+ if not base:
85
+ return []
86
+ # 变体池 = retrieval_variants(归一化/拆型号/同义)+ structural_variants(错误裸
87
+ # token/复合词短语);合并去重、去主串,取前 _VARIANT_MAX_QUERIES 个(成本受控)。
88
+ pool = (retrieval_variants(base, max_n=_VARIANT_MAX_QUERIES + 1)[1:]
89
+ + structural_variants(base))
90
+ seen: set[str] = set()
91
+ variants: list[str] = []
92
+ for v in pool:
93
+ k = v.strip().lower()
94
+ if v.strip() and v != base and k not in seen:
95
+ seen.add(k)
96
+ variants.append(v)
97
+ if not variants:
98
+ return []
99
+ engines = (req.engines[:_VARIANT_MAX_ENGINES] if len(req.engines) > _VARIANT_MAX_ENGINES
100
+ else list(req.engines))
101
+ if not engines:
102
+ return []
103
+ try:
104
+ timeout = min(float(req.eff_timeout or _VARIANT_TIMEOUT_CAP_S), _VARIANT_TIMEOUT_CAP_S)
105
+ except (TypeError, ValueError):
106
+ timeout = _VARIANT_TIMEOUT_CAP_S
107
+ import time as _time
108
+ deadline = _time.monotonic() + _WAVE_BUDGET_S
109
+ # CLI --domain / --sub_domain 的引擎入参照主路径(engine_dispatch 同口径)透传,
110
+ # 否则用户的域约束被变体旁路。
111
+ dom_kwargs: dict[str, str] = {}
112
+ _d = getattr(req, "engine_domain", None)
113
+ _sd = getattr(req, "engine_sub_domain", None)
114
+ if _d:
115
+ dom_kwargs["domain"] = _d
116
+ if _sd:
117
+ dom_kwargs["sub_domain"] = _sd
118
+ extra: list[list[dict[str, Any]]] = []
119
+ for v in variants[:_VARIANT_MAX_QUERIES]:
120
+ if _time.monotonic() > deadline:
121
+ break
122
+ for eng in engines:
123
+ if _time.monotonic() > deadline:
124
+ break
125
+ try:
126
+ res = engine_search(
127
+ v, eng, n=req.max_results, timeout=timeout,
128
+ depth=req.depth, mode=req.mode,
129
+ since=req.since_iso, until=req.until_iso, skip_cache=True,
130
+ **dom_kwargs,
131
+ )
132
+ except Exception:
133
+ continue # 单条变体/单引擎失败不影响主结果
134
+ goods = [r for r in (res or []) if isinstance(r, dict) and "error" not in r]
135
+ for r in goods:
136
+ r.setdefault("_engine", eng)
137
+ if goods:
138
+ extra.append(goods)
139
+ return extra
140
+
141
+
142
+ # 结构信号变体(无词典、纯形状识别)——补 retrieval_variants 覆盖不到的两类:
143
+ # 1. 错误/异常裸 token:从 "Rust error[E0499]" 提取 "E0499"(解错页在 issue tracker);
144
+ # 2. 复合词短语:给连字符/snake_case 词加引号("sqlite-vec"),迫使精确匹配。
145
+ # 刻意窄匹配:错误码只认 E\d{3,5}/ERR_*/XxxError 形状,不碰 HTTPS/NGINX 等全大写普通词。
146
+ _ERR_SHAPE = re.compile(r"\b(E\d{3,5}|ERR_[A-Z0-9_]+|[A-Z][A-Za-z]*Error)\b")
147
+ _COMPOUND_SHAPE = re.compile(r"\b[A-Za-z][A-Za-z0-9]*[-_][A-Za-z0-9][A-Za-z0-9_-]*\b")
148
+
149
+
150
+ def structural_variants(query: str, max_n: int = _VARIANT_MAX_QUERIES) -> list[str]:
151
+ """结构信号变体:错误裸 token + 复合词引号短语。无词典、零依赖、确定性。"""
152
+ if not query or not isinstance(query, str):
153
+ return []
154
+ out: list[str] = []
155
+ for m in _ERR_SHAPE.finditer(query):
156
+ tok = m.group(1)
157
+ if tok and tok.lower() != query.lower():
158
+ out.append(tok)
159
+ for m in _COMPOUND_SHAPE.finditer(query):
160
+ tok = m.group(0)
161
+ if ("-" in tok or "_" in tok) and f'"{tok}"' not in out:
162
+ out.append(f'"{tok}"')
163
+ seen: set[str] = set()
164
+ uniq: list[str] = []
165
+ for v in out:
166
+ k = v.lower()
167
+ if k not in seen:
168
+ seen.add(k)
169
+ uniq.append(v)
170
+ return uniq[:max_n]
171
+
172
+
173
+ def augment_with_variants(req: Any, engine_search: Any, clean_lists: list,
174
+ max_results: int) -> tuple[list, int]:
175
+ """按 gate 决定是否补变体召回;返回 (clean_lists, 变体补回条数)。
176
+
177
+ 失败安全:任何异常都原样返回入参 clean_lists,绝不影响主 query 融合。
178
+ 补回条数单列:变体条目不进漏斗 returned 口径,调用方拿它做观测补账。
179
+ """
180
+ if not should_variant_recall(req, clean_lists, max_results):
181
+ return clean_lists, 0
182
+ try:
183
+ extra = variant_recall_wave(req, engine_search)
184
+ except Exception:
185
+ return clean_lists, 0
186
+ if not extra:
187
+ return clean_lists, 0
188
+ return clean_lists + extra, sum(len(c) for c in extra)