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,193 @@
1
+ #!/usr/bin/env python3
2
+ """seek_rank.py — local-seek 的内容命中相关度重排(BM25F + BM25+,零依赖)。
3
+
4
+ 为什么存在:rg_search 此前按 (路径,行号) 有界堆选池——那是 2026-10-06 为治
5
+ 「rg 发射序不确定」引入的**确定性**口径,但它自身无内容相关性:多词查询的
6
+ top5 常被配置/文档里的顺带提及占满,真正的实现文件进不了前五(2026-10-07
7
+ eval_rank 基线实测,11 用例 topk_hit 5/11、MRR 0.36)。本模块在**同一个
8
+ 确定性池**(cap ≤400、与发射序无关)内做相关度重排:池子怎么选不变,
9
+ 只变池内的呈现次序——确定性契约(tests/test_determinism_20261006.py)
10
+ 由此延续:打分只依赖 (路径, 行号, 行文本, 查询),并列分按 (路径, 行号)
11
+ 升序决出稳定次序。
12
+ (本 docstring 刻意不举具体查询例:本文件就在被搜语料里,注释出现常见
13
+ 查询原词会给 eval 制造自污染。)
14
+
15
+ 算法(两件套):
16
+ BM25F Robertson / Zaragoza / Taylor, "Simple BM25 extension to multiple
17
+ weighted fields" (CIKM 2004)。取 Lucene 式**逐字段 BM25 加权求和**
18
+ 的近似(k1/b 按字段独立),对 ≤400 条的小语料池与全域式等价且更简单。
19
+ BM25+ Lv & Zhai, "Lower-Bounding Term Frequency Normalization"
20
+ (ICTIR 2011)。TF 项加下界 δ——修正 BM25 对短字段(文件名通常只有
21
+ 几个词)的过度惩罚,保证「字段命中即有底分」。
22
+
23
+ 字段(文件命中行的三元结构):
24
+ name 文件名(额外做 camelCase 细分) weight 2.0, b 0.3(短字段弱归一)
25
+ dir 父目录段(同上) weight 0.6, b 0.3
26
+ line 命中行文本 weight 1.0, b 0.75(长字段强归一)
27
+
28
+ 切词口径(与 local-search/bm25.py **同源不同形**,分叉原因见下):
29
+ - 全部字段与查询共用 T1:字母数字连续串小写 + CJK 连续串二元组
30
+ (单字串出单字;范围沿用 seek.CJK_RE,含假名/谚文)。
31
+ 下划线/点号是**分隔符**——「cache_key_vdom」「node.js」必须切得开,
32
+ 否则文件名字段对这类查询永远失配(首版踩过:T1 保留 _ 时
33
+ 「cache_key_vdom」整体成词,文件名字段全灭)。
34
+ - name/dir 额外收录 camelCase 细分词(驼峰名拆成子词,查询侧同样收录),
35
+ 让「分写的多个子词」也能命中连写的驼峰文件名。
36
+ - 与 bm25.py 的分叉原因:那边服务「引擎聚合结果」(title+snippet+引擎
37
+ 位次先验混合),这边服务「文件命中行」(path+line,无位次先验);
38
+ 且 local-seek 不 import 兄弟子技能(零依赖纪律)。两文件 docstring
39
+ 互相指认,防无人知晓的分叉漂移。
40
+
41
+ 逃生门:ARGO_SEEK_CONTENT_RERANK=0 时 seek.py 不调用本模块;查询切不出
42
+ 词元(正则/纯符号)时调用方跳过——两种情况行为与改动前逐位一致。
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ import math
48
+ import re
49
+ from collections import Counter
50
+
51
+ _CJK_RUN = r"[\u3400-\u4dbf\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]+"
52
+ # T1 主切词:字母数字连续串(不含 _ . 等分隔符)+ CJK 连续串
53
+ _RUN_RE = re.compile(r"[A-Za-z0-9]+|" + _CJK_RUN)
54
+ # camelCase 细分:小写/数字段、大写段(后随小写时取「大写+小写尾巴」)
55
+ _CAMEL_RE = re.compile(r"[a-z0-9]+|[A-Z](?=[a-z])[a-z]*|[A-Z]+(?![a-z])")
56
+ _CJK_ONLY_RE = re.compile("^" + _CJK_RUN + "$")
57
+
58
+ # BM25 常数(与 local-search/bm25.py 同口径:Okapi k1=1.5 b=0.75)
59
+ _K1 = 1.5
60
+ _DELTA = 1.0 # BM25+ 下界(Lv & Zhai 2011)
61
+
62
+ # 字段权重与长度归一化强度:文件名命中最值钱(「文件恰好叫这个」是强意图
63
+ # 信号);目录段最弱(scripts/tests 这类段名人人都有,IDF 会自动压它)。
64
+ # 短字段 b 调低:avgdl 只有几个词,强归一化会把略长文件名的分数压没。
65
+ _FIELDS = (
66
+ ("name", 2.0, 0.3),
67
+ ("dir", 0.6, 0.3),
68
+ ("line", 1.0, 0.75),
69
+ )
70
+
71
+ # 文件级命中数先验:同一文件在池里命中多行(_TOPIC_PRIOR_WEIGHT × ln(1+n))
72
+ # 比「只顺带提及一行」更可能是「关于这个主题」的文件——经典 IR 的文档级
73
+ # 词项证据在行粒度输出上的等价物。对数封口:命中 100 行的文件也不会把
74
+ # 单行全覆盖的文档按在地上。典型量级 +2~+5 分,够翻「部分命中 vs 全覆盖
75
+ # 单行」的中位对决,翻不了强覆盖单行的 top1。
76
+ _TOPIC_PRIOR_WEIGHT = 1.2
77
+
78
+ # 逐字短语加成:查询里的完整 CJK 连续串(≥2 字)或 ASCII 词(≥3 字符)在
79
+ # 行文本里**逐字出现**时加分。动机:二元组切词分不清「噪声档的实现行」和
80
+ # 「顺带提到噪声二字的无关文档」——逐字出现是词面层能给出的最强信号
81
+ # (Lucene 短语加成的手搓等价物,零分词器依赖)。每短语一份、封顶两份,
82
+ # 量级与一个中频词元命中相当,够翻二元组的歧义、翻不了多词全覆盖行。
83
+ _PHRASE_BONUS = 3.0
84
+ _PHRASE_BONUS_CAP = 6.0
85
+
86
+
87
+ def _split_camel(run: str) -> list[str]:
88
+ """驼峰细分:BM25Okapi → bm / 25 / okapi(小写输出;无驼峰则空)。"""
89
+ if run.islower() or run.isdigit():
90
+ return []
91
+ return [m.group(0).lower() for m in _CAMEL_RE.finditer(run)]
92
+
93
+
94
+ def _tokens(text: str, *, camel: bool = False) -> list[str]:
95
+ """T1 切词;camel=True 时对每个 ASCII 段追加驼峰细分词。"""
96
+ out: list[str] = []
97
+ for m in _RUN_RE.finditer(text):
98
+ run = m.group(0)
99
+ if run.isascii():
100
+ out.append(run.lower())
101
+ if camel:
102
+ out.extend(_split_camel(run))
103
+ elif len(run) == 1:
104
+ out.append(run)
105
+ else:
106
+ out.extend(run[i:i + 2] for i in range(len(run) - 1))
107
+ return out
108
+
109
+
110
+ def _row_fields(fp: str, text: str) -> dict[str, Counter]:
111
+ """一行命中在三字段下的词频表。fp 无目录段时 dir 为空表。"""
112
+ fp = fp.rstrip("/")
113
+ name, _, dirpart = fp.rpartition("/")
114
+ fields = {
115
+ "name": Counter(_tokens(name, camel=True)),
116
+ "dir": Counter(_tokens(dirpart, camel=True)),
117
+ "line": Counter(_tokens(text or "")),
118
+ }
119
+ return fields
120
+
121
+
122
+ def rank_rows(rows: list[tuple[str, int, str]], raw_query: str,
123
+ boost_fn=None) -> list[tuple[str, int, str]]:
124
+ """BM25F 重排:返回相关度降序的新列表(并列分按 (路径, 行号) 升序)。
125
+
126
+ rows 是 rg_search 有界池的 (路径, 行号, 截断行文本) 三元组(≤400 条)。
127
+ 查询切不出词元时原样返回——正则/纯符号查询没有相关性信号可算。
128
+
129
+ boost_fn:可选的 (路径 → 加成分) 注入点,供 seek.py 在
130
+ ARGO_SEEK_FRECENCY=1 时传入访问频次加成(见 seek_frecency.py)。
131
+ 本模块保持无状态,不感知任何使用信号。
132
+ """
133
+ q_tf = Counter(_tokens(raw_query or "", camel=True))
134
+ if not q_tf or not rows:
135
+ return rows
136
+
137
+ # 逐字短语表:完整 CJK 连续串 + 较长 ASCII 连续串(行文本做子串检查用)
138
+ phrases = [m.group(0).lower()
139
+ for m in re.finditer(r"[A-Za-z0-9_\-+#.]{3,}|" + _CJK_RUN,
140
+ raw_query or "")]
141
+ phrases = list(dict.fromkeys(phrases))
142
+
143
+ row_fields = [_row_fields(fp, txt) for fp, _ln, txt in rows]
144
+ n_rows = len(row_fields)
145
+ avgdl: dict[str, float] = {}
146
+ for f, _w, _b in _FIELDS:
147
+ total = sum(sum(row_fields[i][f].values()) for i in range(n_rows))
148
+ avgdl[f] = (total / n_rows) or 1.0
149
+
150
+ # 文件级命中数:池内同路径的行数(对数先验,见 _TOPIC_PRIOR_WEIGHT)
151
+ file_hits: Counter = Counter(fp for fp, _ln, _txt in rows)
152
+
153
+ # 文档频率:每字段一次遍历(行数 × 行内词元数,池 ≤400 时毫秒级)
154
+ df: dict[str, Counter] = {f: Counter() for f, _w, _b in _FIELDS}
155
+ for f, _w, _b in _FIELDS:
156
+ f_df = df[f]
157
+ for f_tf in row_fields:
158
+ for term in f_tf[f]:
159
+ f_df[term] += 1
160
+
161
+ scores: list[float] = []
162
+ for i, f_tf in enumerate(row_fields):
163
+ total = 0.0
164
+ for f, w, b in _FIELDS:
165
+ tf_map = f_tf[f]
166
+ if not tf_map:
167
+ continue
168
+ dl = sum(tf_map.values())
169
+ norm = _K1 * (1.0 - b + b * dl / avgdl[f])
170
+ f_df = df[f]
171
+ for qt, qtf in q_tf.items():
172
+ tf = tf_map.get(qt)
173
+ if not tf:
174
+ continue
175
+ idf = math.log(1.0 + (n_rows - f_df[qt] + 0.5) / (f_df[qt] + 0.5))
176
+ # BM25+:饱和项加下界 δ(只对命中的词,未命中不送底分)
177
+ sat = (tf * (_K1 + 1.0)) / (tf + norm)
178
+ total += w * qtf * (sat + _DELTA) * idf
179
+ total += _TOPIC_PRIOR_WEIGHT * math.log1p(file_hits[rows[i][0]])
180
+ if phrases:
181
+ low = (rows[i][2] or "").lower()
182
+ hits = sum(1 for ph in phrases if ph in low)
183
+ total += min(hits, 2) * _PHRASE_BONUS
184
+ if boost_fn is not None:
185
+ try:
186
+ total += boost_fn(rows[i][0])
187
+ except Exception:
188
+ pass # 注入的加成失败不拖累打分本体
189
+ scores.append(total)
190
+
191
+ order = sorted(range(n_rows),
192
+ key=lambda i: (-scores[i], rows[i][0], rows[i][1]))
193
+ return [rows[i] for i in order]
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env python3
2
+ """tests/test_content_rank.py — 内容 BM25F 重排 + fzf V2 评分契约(2026-10-07 方案甲)。
3
+
4
+ 守四类不变式:
5
+ A. 重排闸门:逃生门/正则查询/count/context 一律保持 (路径,行号) 原序
6
+ B. 重排质量:文件名命中 > 纯行命中;多词全覆盖 > 单词擦边;噪声档
7
+ 分层在重排后依然成立(真源在前)
8
+ C. 确定性:打分只依赖内容,发射序打乱输出逐位一致(2026-10-06 契约延续)
9
+ D. fzf V2:边界/驼峰/连续/gap 惩罚的相对次序与 fzf 口径一致,全等最优
10
+
11
+ 打桩纪律:unittest.mock.patch.object(自动恢复)——直接给 s.run 赋值且
12
+ 不恢复会把同进程后续测试的真实 rg 顶掉(2026-10-06 实锤教训,见
13
+ test_determinism_20261006.py 模块头)。
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import subprocess
19
+ import sys
20
+ import unittest
21
+ from pathlib import Path
22
+ from unittest.mock import patch
23
+
24
+ ROOT = Path(__file__).resolve().parent.parent
25
+ sys.path.insert(0, str(ROOT / "scripts"))
26
+
27
+ import seek as s # noqa: E402
28
+ import seek_rank as r # noqa: E402
29
+
30
+
31
+ def _proc(stdout: str, rc: int = 0) -> subprocess.CompletedProcess:
32
+ return subprocess.CompletedProcess(args=[], returncode=rc,
33
+ stdout=stdout, stderr="")
34
+
35
+
36
+ class TestRerankGate(unittest.TestCase):
37
+ """A. 闸门:不该重排的场景必须保持 (路径,行号) 原序。"""
38
+
39
+ ROWS = [
40
+ ("b/beta.py", 5, "alpha"),
41
+ ("a/alpha.py", 9, "alpha"),
42
+ ("c/gamma.md", 1, "alpha"),
43
+ ]
44
+
45
+ def _search(self, raw_query, fixed=True, ctx=0, count=False):
46
+ with patch.object(s, "run", return_value=_proc("a/x.py:1:hit\n")):
47
+ # 直接对池行调用重排闸门(不走 rg,隔离网络与文件系统)
48
+ return s._rerank_content(list(self.ROWS), raw_query, ctx, count, fixed)
49
+
50
+ def test_escape_hatch_restores_order(self):
51
+ import os
52
+ old = os.environ.pop("ARGO_SEEK_CONTENT_RERANK", None)
53
+ os.environ["ARGO_SEEK_CONTENT_RERANK"] = "0"
54
+ try:
55
+ self.assertEqual(self._search("alpha"), self.ROWS)
56
+ finally:
57
+ os.environ.pop("ARGO_SEEK_CONTENT_RERANK", None)
58
+ if old is not None:
59
+ os.environ["ARGO_SEEK_CONTENT_RERANK"] = old
60
+
61
+ def test_regex_query_not_reranked(self):
62
+ # fixed=False(正则/PCRE2):词元切分只会产生碎片,闸门必须放行原序
63
+ self.assertEqual(self._search(r"\d{4}", fixed=False), self.ROWS)
64
+
65
+ def test_count_and_context_not_reranked(self):
66
+ self.assertEqual(self._search("alpha", count=True), self.ROWS)
67
+ self.assertEqual(self._search("alpha", ctx=3), self.ROWS)
68
+
69
+ def test_no_token_query_not_reranked(self):
70
+ self.assertEqual(self._search("!!!"), self.ROWS)
71
+
72
+
73
+ class TestRerankQuality(unittest.TestCase):
74
+ """B. 重排质量:相关度次序的三条核心判据。"""
75
+
76
+ def test_filename_match_beats_content_only(self):
77
+ rows = [
78
+ ("docs/notes.md", 12, "支持噪声档降权的说明文字。"),
79
+ ("src/noise_tier.py", 3, "placeholder"),
80
+ ("src/other.py", 8, "噪声相关但文件无关。"),
81
+ ]
82
+ out = r.rank_rows(rows, "noise_tier")
83
+ self.assertEqual(out[0][0], "src/noise_tier.py")
84
+
85
+ def test_multi_token_coverage_beats_partial(self):
86
+ rows = [
87
+ ("a/one.md", 1, "只提到缓存一个词。"),
88
+ ("b/both.md", 2, "语义缓存与 minhash 软命中都在这行。"),
89
+ ]
90
+ out = r.rank_rows(rows, "语义缓存 minhash")
91
+ self.assertEqual(out[0][0], "b/both.md")
92
+
93
+ def test_verbatim_phrase_beats_bigram_scatter(self):
94
+ # 二元组分不清「噪声档的实现行」与「只有噪声二字的无关行」:
95
+ # 逐字短语加成必须把前者顶上去
96
+ rows = [
97
+ ("a/x.md", 1, "设备噪声很大。"),
98
+ ("b/y.py", 2, "噪声档的阈值定义在此。"),
99
+ ]
100
+ out = r.rank_rows(rows, "噪声档")
101
+ self.assertEqual(out[0][0], "b/y.py")
102
+
103
+ def test_ties_break_by_path_line(self):
104
+ rows = [("b.py", 2, "same"), ("a.py", 9, "same"), ("a.py", 1, "same")]
105
+ out = r.rank_rows(rows, "same")
106
+ self.assertEqual([(p, l) for p, l, _ in out],
107
+ [("a.py", 1), ("a.py", 9), ("b.py", 2)])
108
+
109
+ def test_noise_floor_after_rerank(self):
110
+ # 重排改变池内次序后,噪声档分层(真源在前)必须依然成立——
111
+ # 分层发生在 rg_search 的 _apply_noise_floor(重排之后),走全链路验证
112
+ rows = [
113
+ "tmp/x.py:1:noise tier keyword",
114
+ "src/y.py:2:noise tier keyword here",
115
+ ]
116
+ with patch.object(s, "run", return_value=_proc("\n".join(rows) + "\n")):
117
+ out, err = s.rg_search(["noise"], ".", [], [], 0, False, 5,
118
+ fixed=True, raw_query="noise tier keyword")
119
+ self.assertIsNone(err)
120
+ self.assertTrue(out, "真源行不应消失")
121
+ self.assertTrue(out[0][0].startswith("src/"),
122
+ f"真源应排最前,实际: {out[0][0]}")
123
+
124
+
125
+ class TestRerankDeterminism(unittest.TestCase):
126
+ """C. 确定性:打平发射序(2026-10-06 契约在重排开启下延续)。"""
127
+
128
+ def test_output_independent_of_emission_order(self):
129
+ rows = [f"src/f{i:03d}.py:{j + 1}:alpha beta hit {i}-{j}"
130
+ for i in range(40) for j in range(1)]
131
+ with patch.object(s, "run", return_value=_proc("\n".join(rows) + "\n")):
132
+ a, err = s.rg_search(["alpha"], ".", [], [], 0, False, 5,
133
+ fixed=True, raw_query="alpha beta")
134
+ import random
135
+ shuffled = rows[:]
136
+ random.Random(7).shuffle(shuffled)
137
+ with patch.object(s, "run", return_value=_proc("\n".join(shuffled) + "\n")):
138
+ b, err2 = s.rg_search(["alpha"], ".", [], [], 0, False, 5,
139
+ fixed=True, raw_query="alpha beta")
140
+ self.assertIsNone(err)
141
+ self.assertIsNone(err2)
142
+ self.assertEqual(a, b)
143
+
144
+
145
+ class TestFzfV2(unittest.TestCase):
146
+ """D. fzf FuzzyMatchV2 口径。"""
147
+
148
+ def test_no_match_is_zero(self):
149
+ self.assertEqual(s._fzf_score("zzz.txt", "rep"), 0)
150
+
151
+ def test_smart_case(self):
152
+ self.assertGreater(s._fzf_score("Report_v2.md", "report"), 0)
153
+ # 模式带大写 → 区分大小写:R 匹配、r 不配
154
+ self.assertEqual(s._fzf_score("report.md", "R"), 0)
155
+
156
+ def test_boundary_beats_midword(self):
157
+ # bar 在 foo_bar(词边界)> xbarx(词中)
158
+ self.assertGreater(s._fzf_score("foo_bar.py", "bar"),
159
+ s._fzf_score("xbarx.py", "bar"))
160
+
161
+ def test_consecutive_beats_gappy(self):
162
+ self.assertGreater(s._fzf_score("xabc.md", "abc"),
163
+ s._fzf_score("axbxc.md", "abc"))
164
+
165
+ def test_camel_bonus(self):
166
+ self.assertGreater(s._fzf_score("parseHTTPResponse.py", "hr"), 0)
167
+
168
+ def test_dp_optimal_alignment(self):
169
+ # xaabc 搜 ab:DP 应发现「跳过词首 a、取边界 b」的对齐优于顺位连打
170
+ # (顺位贪心:a@1 + b@4 有 gap;最优:a@1 + b@4 不如 a@3?——此处锚定
171
+ # 已知反超事实:xaabc 的分必须高于等长无边界对照 axbxc 的同模式分)
172
+ self.assertGreater(s._fzf_score("xaabc", "ab"),
173
+ s._fzf_score("axbxc", "ab"))
174
+
175
+ def test_exact_beats_partial(self):
176
+ self.assertGreater(s._fzf_score("seek.py", "seek.py"),
177
+ s._fzf_score("seek_helper.py", "seek.py"))
178
+
179
+ def test_exact_case_beats_icase(self):
180
+ self.assertGreaterEqual(s._fzf_score("Seek.py", "seek.py"),
181
+ s._fzf_score("seek.py", "seek.py")
182
+ ) # 全等大写版含 case bonus,不小于纯 icase
183
+
184
+
185
+ class TestFrecencyWiring(unittest.TestCase):
186
+ """frecency 默认关闭:不启用时不产生任何行为差异。"""
187
+
188
+ def test_disabled_by_default(self):
189
+ import os
190
+ old = os.environ.pop("ARGO_SEEK_FRECENCY", None)
191
+ try:
192
+ import seek_frecency
193
+ self.assertFalse(seek_frecency.enabled())
194
+ self.assertEqual(seek_frecency.boost("/any/path"), 0.0)
195
+ finally:
196
+ if old is not None:
197
+ os.environ["ARGO_SEEK_FRECENCY"] = old
198
+
199
+ def test_boost_and_record_roundtrip(self):
200
+ import os
201
+ import tempfile
202
+ old_flag = os.environ.pop("ARGO_SEEK_FRECENCY", None)
203
+ old_db = os.environ.pop("ARGO_SEEK_FRECENCY_DB", None)
204
+ with tempfile.TemporaryDirectory() as td:
205
+ os.environ["ARGO_SEEK_FRECENCY"] = "1"
206
+ os.environ["ARGO_SEEK_FRECENCY_DB"] = str(Path(td) / "f.json")
207
+ try:
208
+ import seek_frecency
209
+ seek_frecency.record(["/a/b.py", "/c/d.py"])
210
+ self.assertGreater(seek_frecency.boost("/a/b.py"), 0.0)
211
+ self.assertEqual(seek_frecency.boost("/z/missing.py"), 0.0)
212
+ finally:
213
+ os.environ.pop("ARGO_SEEK_FRECENCY", None)
214
+ os.environ.pop("ARGO_SEEK_FRECENCY_DB", None)
215
+ if old_flag is not None:
216
+ os.environ["ARGO_SEEK_FRECENCY"] = old_flag
217
+ if old_db is not None:
218
+ os.environ["ARGO_SEEK_FRECENCY_DB"] = old_db
219
+
220
+
221
+ if __name__ == "__main__":
222
+ unittest.main()
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env python3
2
+ """tests/test_determinism_20261006.py — 宽查询输出确定性回归(方案一,2026-10-06)。
3
+
4
+ 守 v1.4.1 审计遗留的定性修复:
5
+ 1. rg 并行遍历发射序不定 →「先到先得」截断拿到任意的 cap 条,同查询
6
+ 两次跑结果集与排序都不同。修复后有界堆按 (路径,行号,内容) 选池,
7
+ **同一命中集无论以什么顺序发射,输出逐条一致**。
8
+ 2. count 模式并列计数按路径升序决出稳定次序。
9
+ 3. grep 兜底路径同款语义。
10
+ 4. 锁定 ja/ko 2-gram 既有行为(CJK_RE 覆盖假名/谚文——2026-10-06 实测
11
+ 已支持,修正审计记忆里「无分词只能整词匹配」的过时记录)。
12
+
13
+ 打桩纪律:一律 unittest.mock.patch.object(自动恢复)。首版直接给
14
+ s.run 赋值且不恢复,把同进程后续测试的真实 rg 全部替换成假输出——
15
+ 套件 9 红的根因(2026-10-06 实锤,教训记录在此防再犯)。
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import subprocess
21
+ import sys
22
+ import unittest
23
+ from pathlib import Path
24
+ from unittest.mock import patch
25
+
26
+ ROOT = Path(__file__).resolve().parent.parent
27
+ sys.path.insert(0, str(ROOT / "scripts"))
28
+
29
+ import seek as s # noqa: E402
30
+
31
+
32
+ def _proc(stdout: str, rc: int = 0) -> subprocess.CompletedProcess:
33
+ return subprocess.CompletedProcess(args=[], returncode=rc,
34
+ stdout=stdout, stderr="")
35
+
36
+
37
+ def _matches(n_files: int, lines_per: int = 1) -> list[str]:
38
+ """n_files 个文件的 rg 命中行(路径 f{i:03d}.py 保证字典序可控)。"""
39
+ out = []
40
+ for i in range(n_files):
41
+ for j in range(lines_per):
42
+ out.append(f"src/f{i:03d}.py:{j + 1}:hit {i}-{j}")
43
+ return out
44
+
45
+
46
+ class RgBodyDeterminism(unittest.TestCase):
47
+ """rg 主体路径:池子选择与发射序无关。"""
48
+
49
+ def test_output_independent_of_emission_order(self):
50
+ rows = _matches(40)
51
+ # max_results=5 → cap = max(5, min(20, 400)) = 20:命中 40 > cap,
52
+ # 池子选择真正发生截断
53
+ for drop_noise in (True, False):
54
+ with self.subTest(drop_noise=drop_noise):
55
+ with patch.object(s, "run",
56
+ return_value=_proc("\n".join(rows) + "\n")):
57
+ a, err = s.rg_search(["q"], ".", [], [], 0, False, 5,
58
+ fixed=True, drop_noise=drop_noise)
59
+ with patch.object(s, "run",
60
+ return_value=_proc(
61
+ "\n".join(reversed(rows)) + "\n")):
62
+ b, err2 = s.rg_search(["q"], ".", [], [], 0, False, 5,
63
+ fixed=True, drop_noise=drop_noise)
64
+ self.assertIsNone(err)
65
+ self.assertIsNone(err2)
66
+ self.assertEqual(a, b, "同一命中集、不同发射序 → 输出必须一致")
67
+
68
+ def test_pool_is_path_smallest_and_sorted(self):
69
+ rows = _matches(40)
70
+ with patch.object(s, "run",
71
+ return_value=_proc("\n".join(rows) + "\n")):
72
+ out, err = s.rg_search(["q"], ".", [], [], 0, False, 5,
73
+ fixed=True, drop_noise=False)
74
+ self.assertIsNone(err)
75
+ self.assertEqual([r[0] for r in out],
76
+ [f"src/f{i:03d}.py" for i in range(5)])
77
+ self.assertEqual(len(out), 5)
78
+
79
+ def test_noise_floor_layering_deterministic(self):
80
+ """clean 在前 noisy 在后的分层语义在确定池子上仍然成立。"""
81
+ rows = (["src/clean_{:03d}.py:1:hit".format(i) for i in range(30)]
82
+ + ["src/repos/lib_{:03d}.py:1:hit".format(i) for i in range(30)])
83
+ outs = []
84
+ for order in (rows, list(reversed(rows))):
85
+ with patch.object(s, "run",
86
+ return_value=_proc("\n".join(order) + "\n")):
87
+ out, err = s.rg_search(["q"], ".", [], [], 0, False, 5,
88
+ fixed=True, drop_noise=True)
89
+ self.assertIsNone(err)
90
+ outs.append(out)
91
+ self.assertEqual(outs[0], outs[1])
92
+ self.assertTrue(all("/repos/" not in r[0] for r in outs[0]),
93
+ "clean 够数时噪声档不得混入")
94
+
95
+
96
+ class CountTieDeterminism(unittest.TestCase):
97
+ def test_equal_counts_sorted_by_path(self):
98
+ rows = ["src/b.py:5", "src/a.py:5", "src/c.py:2"]
99
+ with patch.object(s, "run",
100
+ return_value=_proc("\n".join(rows) + "\n")):
101
+ out, err = s.rg_search(["q"], ".", [], [], 0, True, 10,
102
+ fixed=True, drop_noise=False)
103
+ self.assertIsNone(err)
104
+ self.assertEqual([r[0] for r in out],
105
+ ["src/a.py", "src/b.py", "src/c.py"])
106
+
107
+
108
+ class GrepFallbackDeterminism(unittest.TestCase):
109
+ def test_body_and_count_deterministic(self):
110
+ rows = _matches(40)
111
+ for order in (rows, list(reversed(rows))):
112
+ with patch.object(s, "run",
113
+ return_value=_proc("\n".join(order) + "\n")):
114
+ body, err = s.grep_search("grep", ["q"], ".", [], [], 0,
115
+ False, 5, fixed=True)
116
+ self.assertIsNone(err)
117
+ self.assertEqual([r[0] for r in body],
118
+ [f"src/f{i:03d}.py" for i in range(5)])
119
+ cnt_rows = ["src/b.py:5", "src/a.py:5"]
120
+ with patch.object(s, "run",
121
+ return_value=_proc("\n".join(cnt_rows) + "\n")):
122
+ cnt, err = s.grep_search("grep", ["q"], ".", [], [], 0,
123
+ True, 10, fixed=True)
124
+ self.assertIsNone(err)
125
+ self.assertEqual([r[0] for r in cnt], ["src/a.py", "src/b.py"])
126
+
127
+
128
+ class JaKoBigramLocked(unittest.TestCase):
129
+ """锁定既有行为(实测于 2026-10-06):CJK_RE 已覆盖假名/谚文。"""
130
+
131
+ def test_japanese_compound_gets_bigrams(self):
132
+ pats, literal = s.build_patterns("プロンプトエンジニアリング入門")
133
+ self.assertIn("プロンプトエンジニアリング入門", pats) # 整词
134
+ self.assertIn("プロ", pats) # 首个 2-gram
135
+ self.assertTrue(literal)
136
+
137
+ def test_korean_spaced_words_and_compound(self):
138
+ pats, _ = s.build_patterns("프로프트 검색 설정")
139
+ self.assertIn("프로프트", pats) # 复合词整词
140
+ self.assertIn("프로", pats) # 复合词 2-gram
141
+ self.assertIn("검색", pats) # 空格分隔词保持整词
142
+
143
+
144
+ if __name__ == "__main__":
145
+ unittest.main()
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env python3
2
+ """噪声档降权的回归测试(2026-09-27 新增,方案 C3)。
3
+
4
+ 实测背景(Documents/GPT 搜 "import",rg 61899 处命中):
5
+ vendor/生成物/归档 7% + 测试/fixture/benchmark 27% + tmp/ 7% = 41%
6
+ 搜代码时,四成命中是「不想看到的东西」。原来的 DEFAULT_EXCLUDES 挡掉了
7
+ node_modules/dist 之类,但漏了三类最大的:repos/(克隆仓)、tests|fixtures|
8
+ benchmark、tmp/。
9
+
10
+ **为什么是降权不是排除**:排除 = 这些内容永远搜不到。搜「某个克隆仓里怎么
11
+ 写的」「我的测试怎么写的」是真实且常见的用法,排除会让工具在这些查询上
12
+ 直接回答「未找到匹配」——那比返回噪声更糟。降权 + 无命中自动回落,两者兼得。
13
+
14
+ 本文件锁定的性质:
15
+ 1. 默认搜不到噪声档内容(降权生效);
16
+ 2. 噪声档独有内容**仍可达**(回落生效,不制造「明明有却搜不到」);
17
+ 3. --include-noise 显式关闭降权;
18
+ 4. 回落不影响正常命中(有真源时不去噪声档白跑一趟)。
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import os
23
+ import subprocess
24
+ import sys
25
+ from pathlib import Path
26
+
27
+ SEEK = Path(__file__).resolve().parent.parent / "scripts" / "seek.py"
28
+
29
+
30
+ def _have(*tools) -> bool:
31
+ import shutil
32
+ return all(shutil.which(t) for t in tools)
33
+
34
+
35
+ def _run(args, cwd):
36
+ return subprocess.run([sys.executable, str(SEEK)] + args, cwd=cwd,
37
+ capture_output=True, text=True, timeout=120)
38
+
39
+
40
+ def _make_tree(root: Path):
41
+ """构造一棵含「真实源」与「噪声档」两类内容的最小目录树。
42
+
43
+ 标记词刻意选 `zzmarker_*`:pytest 的 tmp_path 目录名会带测试函数名
44
+ (如 `test_noise_tier_is_deprioritiz0`),若标记词是 `pytest` 之类,
45
+ 路径本身就会命中,测试变成在验证「路径里有这个词」而非「噪声档降权」。
46
+ 这是第一版用例写错的地方。
47
+ """
48
+ (root / "src").mkdir(parents=True)
49
+ (root / "src" / "app.py").write_text("import os\nimport sys\n", encoding="utf-8")
50
+ (root / "repos" / "thirdparty").mkdir(parents=True)
51
+ (root / "repos" / "thirdparty" / "lib.py").write_text(
52
+ "import zzzarker_clonelib\n", encoding="utf-8")
53
+ (root / "tests").mkdir()
54
+ (root / "tests" / "test_x.py").write_text(
55
+ "import zzzarker_fixture\n", encoding="utf-8")
56
+
57
+
58
+ def test_noise_tier_is_deprioritized_and_reachable(tmp_path):
59
+ if not _have("rg"):
60
+ import pytest
61
+ pytest.skip("rg 未安装")
62
+ _make_tree(tmp_path)
63
+ proj = str(tmp_path)
64
+
65
+ # 噪声档独有内容:搜得到(相对根判定,本例的 tests/ 在根之下),
66
+ # 且**不触发**回落重搜——它本来就没被排除,只是被排到真源之后。
67
+ r = _run(["zzzarker_fixture", "--path", proj], tmp_path)
68
+ assert "test_x.py" in r.stdout, r.stdout
69
+ assert "噪声档" not in r.stdout, "没被排除就不该多跑一趟回落"
70
+
71
+ # 正常源:不受降权影响
72
+ r2 = _run(["import", "--path", proj], tmp_path)
73
+ assert "app.py" in r2.stdout, r2.stdout
74
+
75
+
76
+ def test_deprioritized_means_source_wins(tmp_path):
77
+ """同一查询同时命中真源与噪声档时,真源必须排在前面。"""
78
+ if not _have("rg"):
79
+ import pytest
80
+ pytest.skip("rg 未安装")
81
+ (tmp_path / "src").mkdir()
82
+ (tmp_path / "src" / "real.py").write_text("import shared_token\n", encoding="utf-8")
83
+ (tmp_path / "repos").mkdir()
84
+ (tmp_path / "repos" / "copy.py").write_text("import shared_token\n", encoding="utf-8")
85
+ proj = str(tmp_path)
86
+
87
+ r = _run(["shared_token", "--path", proj, "--max", "1"], tmp_path)
88
+ assert "real.py" in r.stdout, r.stdout
89
+ assert "copy.py" not in r.stdout, r.stdout
90
+
91
+
92
+ def test_include_noise_flag_is_explicit(tmp_path):
93
+ if not _have("rg"):
94
+ import pytest
95
+ pytest.skip("rg 未安装")
96
+ _make_tree(tmp_path)
97
+ proj = str(tmp_path)
98
+ r = _run(["zzzarker_fixture", "--path", proj, "--include-noise"], tmp_path)
99
+ assert "test_x.py" in r.stdout, r.stdout
100
+ # 显式开启时不应再回落(那会多跑一趟)
101
+ assert "噪声档" not in r.stdout, r.stdout