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,136 @@
1
+ #!/usr/bin/env python3
2
+ """pcre2 判据回归测试(2026-09-27)。
3
+
4
+ 守的缺陷:`pcre2_supported()` 曾用「跑一次匹配看返回码」判定 rg 是否支持 PCRE2:
5
+
6
+ proc = run(["rg", "--pcre2", "-e", "x", os.devnull])
7
+ _pcre2_ok = proc is not None and proc.returncode == 0
8
+
9
+ `/dev/null` 永远没有匹配,rg 无匹配时返回 1,于是判据恒为 False——本机
10
+ rg 明明带 +pcre2,却永远被判为「未编译 PCRE2」,所有 look-around /
11
+ 反向引用查询被拒绝(用户可见的报错:「请简化查询」)。
12
+
13
+ 根因是**判据的观察量与被判定的性质无关**:探测目标的匹配结果说明不了
14
+ 二进制是否带某个特性。修法改读 `rg --version` 的 features 行。
15
+
16
+ 这组测试把「返回码语义」钉死:只 mock run() 的返回值,覆盖
17
+ - features 行含 +pcre2 → True
18
+ - features 行是 -pcre2 → False
19
+ - rg 不存在(run 返回 None)→ False
20
+ - 只判 features 行,不被「匹配有无」影响(原缺陷的形态)
21
+
22
+ 运行:
23
+ cd ~/.agents/skills/argo
24
+ python3 -m pytest sub-skills/local-seek/tests/test_pcre2_probe.py -q
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import subprocess
30
+ import sys
31
+ import unittest
32
+ from pathlib import Path
33
+ from unittest import mock
34
+
35
+ SCRIPTS = Path(__file__).resolve().parent.parent / "scripts"
36
+ sys.path.insert(0, str(SCRIPTS))
37
+
38
+ import seek as s # noqa: E402
39
+
40
+
41
+ def _proc(stdout: str, returncode: int = 0):
42
+ p = mock.Mock()
43
+ p.stdout = stdout
44
+ p.returncode = returncode
45
+ return p
46
+
47
+
48
+ class TestPcre2Probe(unittest.TestCase):
49
+ def setUp(self):
50
+ # 模块级缓存必须在每个用例前清掉,否则第一个用例的结果会被后续复用
51
+ self._saved = s._pcre2_ok
52
+ s._pcre2_ok = None
53
+ self.addCleanup(lambda: setattr(s, "_pcre2_ok", self._saved))
54
+
55
+ def test_features_line_with_pcre2_is_true(self):
56
+ """正面:features:+pcre2 → 支持(本机 rg 15.0.0 的真实形态)。"""
57
+ out = "ripgrep 15.0.0 (rev 3a612f88b8)\n\nfeatures:+pcre2\nsimd(compile):+NEON\n"
58
+ with mock.patch.object(s, "run", return_value=_proc(out)):
59
+ self.assertTrue(s.pcre2_supported())
60
+
61
+ def test_features_line_without_pcre2_is_false(self):
62
+ """反面:features 行明确是 -pcre2 → 不支持,走 grep 回退。"""
63
+ out = "ripgrep 13.0.0\n\nfeatures:-pcre2\n"
64
+ with mock.patch.object(s, "run", return_value=_proc(out)):
65
+ self.assertFalse(s.pcre2_supported())
66
+
67
+ def test_missing_rg_is_false(self):
68
+ """rg 不存在时 run() 返回 None → False(不得抛异常)。"""
69
+ with mock.patch.object(s, "run", return_value=None):
70
+ self.assertFalse(s.pcre2_supported())
71
+
72
+ def test_does_not_depend_on_match_result(self):
73
+ """核心回归:判据不得再依赖「有没有匹配」。
74
+
75
+ 原缺陷的形态是 `returncode == 0`——把「无匹配(1)」误读成「不支持」。
76
+ 这里给一个**无匹配语义**的返回码 1 但 features 行含 +pcre2,
77
+ 新判据必须仍判 True。若有人把它改回看返回码,本用例会红。
78
+ """
79
+ out = "ripgrep 15.0.0\n\nfeatures:+pcre2\n"
80
+ with mock.patch.object(s, "run", return_value=_proc(out, returncode=1)):
81
+ self.assertTrue(s.pcre2_supported(),
82
+ "判据又依赖返回码了:无匹配(rc=1)被误读成不支持 PCRE2")
83
+
84
+ def test_probe_targets_version_not_a_match_run(self):
85
+ """确认探测命令本身是 --version,而不是「跑一次匹配」。"""
86
+ captured = {}
87
+
88
+ def _fake(cmd, **kwargs):
89
+ captured["cmd"] = cmd
90
+ return _proc("ripgrep 15.0.0\nfeatures:+pcre2\n")
91
+
92
+ with mock.patch.object(s, "run", side_effect=_fake):
93
+ s.pcre2_supported()
94
+ self.assertIn("--version", captured["cmd"],
95
+ "探测命令必须是 rg --version(读特性声明),不是匹配探测")
96
+
97
+ def test_module_cache_avoids_repeated_probe(self):
98
+ """缓存语义:只探一次,第二次不再调 run()。"""
99
+ calls = []
100
+
101
+ def _fake(cmd, **kwargs):
102
+ calls.append(cmd)
103
+ return _proc("ripgrep 15.0.0\nfeatures:+pcre2\n")
104
+
105
+ with mock.patch.object(s, "run", side_effect=_fake):
106
+ s.pcre2_supported()
107
+ s.pcre2_supported()
108
+ s.pcre2_supported()
109
+ self.assertEqual(len(calls), 1, "模块级缓存失效,重复探测")
110
+
111
+
112
+ class TestPcre2EndToEnd(unittest.TestCase):
113
+ """真机对照:本机 rg 带 +pcre2 时,look-around 查询真的能出结果。
114
+
115
+ 不断言「一定支持」(别的机器可能没有),而是断言**判据与真机能力一致**:
116
+ 先读 rg --version 拿真值,再要求 pcre2_supported() 与之一致。
117
+ """
118
+
119
+ def test_agrees_with_real_rg(self):
120
+ try:
121
+ real = subprocess.run(["rg", "--version"], capture_output=True,
122
+ text=True, timeout=10)
123
+ except (FileNotFoundError, subprocess.TimeoutExpired):
124
+ self.skipTest("本机无 rg,跳过真机对照")
125
+ truth = "+pcre2" in (real.stdout or "")
126
+ saved = s._pcre2_ok
127
+ s._pcre2_ok = None
128
+ try:
129
+ self.assertEqual(s.pcre2_supported(), truth,
130
+ "判据与 rg 自报的特性不一致")
131
+ finally:
132
+ s._pcre2_ok = saved
133
+
134
+
135
+ if __name__ == "__main__":
136
+ unittest.main()
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env python3
2
+ """local-seek 文件名相关性排序 + 拼音首字母单测(离线)
3
+
4
+ 覆盖:
5
+ 1. pinyin_initials:中文→拼音首字母(pypinyin 优先 / GB2312 保底)
6
+ 2. _fzf_score:fzf 式文件名评分(smart case / 连续 / 边界 / 全等)
7
+ 3. _file_pinyin_bonus:正向(中文→缩写)与反向(缩写→中文文件名)加分
8
+ 4. _rank_path_results:按相关性 + mtime 排序
9
+ 5. _looks_like_pinyin_abbrev:拼音缩写判定
10
+
11
+ 运行:
12
+ cd ~/.agents/skills/argo
13
+ python3.14 -m pytest sub-skills/local-seek/tests/test_rank_pinyin.py -q
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import sys
19
+ import unittest
20
+ from pathlib import Path
21
+
22
+ SCRIPTS = Path(__file__).resolve().parent.parent / "scripts"
23
+ sys.path.insert(0, str(SCRIPTS))
24
+
25
+ import seek as s # noqa: E402
26
+
27
+
28
+ class TestPinyin(unittest.TestCase):
29
+ def test_zh_initials(self):
30
+ self.assertEqual(s.pinyin_initials("新建夹"), "xjj")
31
+
32
+ def test_non_cjk_kept(self):
33
+ self.assertIn("report", s.pinyin_initials("report_v2.md"))
34
+
35
+ def test_empty(self):
36
+ self.assertEqual(s.pinyin_initials(""), "")
37
+
38
+
39
+ class TestFzfScore(unittest.TestCase):
40
+ def test_match_boundary(self):
41
+ self.assertGreater(s._fzf_score("新建夹.pdf", "新"), 0)
42
+
43
+ def test_continuous(self):
44
+ self.assertGreater(s._fzf_score("report_v2.md", "rep"), 0)
45
+
46
+ def test_no_match(self):
47
+ self.assertEqual(s._fzf_score("zzz.txt", "rep"), 0)
48
+
49
+ def test_case_insensitive(self):
50
+ self.assertGreater(s._fzf_score("Report_v2.md", "report"), 0)
51
+
52
+
53
+ class TestPinyinBonus(unittest.TestCase):
54
+ def test_forward(self):
55
+ self.assertEqual(s._file_pinyin_bonus("新建夹.pdf", "新建夹"), 50)
56
+
57
+ def test_reverse(self):
58
+ self.assertEqual(s._file_pinyin_bonus("新建夹.pdf", "xjj"), 8)
59
+
60
+ def test_none(self):
61
+ self.assertEqual(s._file_pinyin_bonus("other.txt", "xjj"), 0)
62
+
63
+
64
+ class TestRank(unittest.TestCase):
65
+ def test_rank_top_by_relevance(self):
66
+ r = [("a/zzz.txt", 0, ""), ("b/新建夹.pdf", 0, ""), ("c/report_v2.md", 0, "")] # noqa: E741
67
+ ranked = s._rank_path_results(r, "新")
68
+ self.assertEqual(ranked[0][0], "b/新建夹.pdf")
69
+
70
+ def test_abbrev_look(self):
71
+ self.assertTrue(s._looks_like_pinyin_abbrev("xjj"))
72
+ self.assertFalse(s._looks_like_pinyin_abbrev("新"))
73
+ self.assertFalse(s._looks_like_pinyin_abbrev("report"))
74
+
75
+
76
+ if __name__ == "__main__":
77
+ unittest.main()
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env python3
2
+ """2026-10-03 审计轮回归测试:四类假阴性 + run_query 契约 + timeout 下传。
3
+
4
+ 守的四个实测缺陷(全部先在真机复现再修):
5
+ 1. run_query 目录不存在分支 print + 裸 return 1,违反 (text, rc) 契约,
6
+ MCP/include-local 进程内调用方解包直接 TypeError。
7
+ 2. --since/--until 在 max 截断之后过滤:旧文件占满截断池,新命中被吃掉
8
+ (实测 20 个新文件只报出 8 个)。
9
+ 3. build_patterns 中文分支不拆英文多词:「性能 asyncio tutorial」的英文段
10
+ 整段化作固定短语,单词条目全部漏掉(eb0ed38 只修了纯英文分支)。
11
+ 4. --scope doc 把 md/txt 从 exts 减掉,文档场景恰好漏掉最常搜的笔记。
12
+
13
+ 运行:
14
+ cd ~/.agents/skills/argo
15
+ python3 -m pytest sub-skills/local-seek/tests/test_regression_20261003.py -q
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import io
21
+ import json
22
+ import os
23
+ import sys
24
+ import tempfile
25
+ import unittest
26
+ from contextlib import redirect_stdout
27
+ from pathlib import Path
28
+ from unittest import mock
29
+
30
+ SCRIPTS = Path(__file__).resolve().parent.parent / "scripts"
31
+ sys.path.insert(0, str(SCRIPTS))
32
+
33
+ import seek as s # noqa: E402
34
+
35
+
36
+ def _run_main(argv):
37
+ """跑 seek.main(),捕获 stdout,返回 (exit_code, stdout)。"""
38
+ with mock.patch.object(sys, "argv", ["seek.py"] + argv):
39
+ buf = io.StringIO()
40
+ try:
41
+ with redirect_stdout(buf):
42
+ code = s.main()
43
+ except SystemExit as e:
44
+ code = e.code or 0
45
+ return code or 0, buf.getvalue()
46
+
47
+
48
+ class TestBuildPatterns(unittest.TestCase):
49
+ """中英混排与纯英文分词(缺陷 3)。"""
50
+
51
+ def test_mixed_cjk_english_splits_words(self):
52
+ patterns, fixed = s.build_patterns("性能 asyncio tutorial")
53
+ self.assertIn("asyncio", patterns, "英文段必须按词拆开")
54
+ self.assertIn("tutorial", patterns)
55
+ self.assertIn("性能", patterns)
56
+ self.assertTrue(fixed)
57
+
58
+ def test_pure_english_multiword_unchanged(self):
59
+ patterns, _ = s.build_patterns("Python asyncio tutorial")
60
+ self.assertEqual(patterns, ["Python", "asyncio", "tutorial"])
61
+
62
+ def test_space_never_a_pattern(self):
63
+ patterns, _ = s.build_patterns("樯橹 灰飞烟灭")
64
+ self.assertNotIn("", patterns)
65
+ self.assertNotIn(" ", patterns)
66
+
67
+ def test_cjk_2gram_extension_kept(self):
68
+ patterns, _ = s.build_patterns("数据抓取")
69
+ self.assertIn("数据抓取", patterns)
70
+ self.assertIn("抓取", patterns)
71
+
72
+
73
+ class TestSinceFilterBeforeTruncation(unittest.TestCase):
74
+ """--since 过滤必须发生在截断之前(缺陷 2,真实 rg 端到端)。"""
75
+
76
+ def test_new_files_not_swallowed_by_old(self):
77
+ if not s.tool_exists("rg"):
78
+ self.skipTest("本机无 rg")
79
+ with tempfile.TemporaryDirectory() as tmp:
80
+ ancient = 900000000 # 1998 年,稳落任何时间窗之外
81
+ for i in range(40):
82
+ p = Path(tmp) / f"old_{i:02d}.txt"
83
+ p.write_text("目标词命中 here\n")
84
+ os.utime(p, (ancient, ancient))
85
+ for i in range(20):
86
+ (Path(tmp) / f"new_{i:02d}.txt").write_text("目标词命中 here\n")
87
+ code, out = _run_main(
88
+ ["目标词", "--path", tmp, "--since", "1d", "--max", "30", "--json"])
89
+ self.assertEqual(code, 0)
90
+ self.assertEqual(json.loads(out)["count"], 20,
91
+ "20 个新文件全部在窗口内,一个都不能被旧文件挤掉")
92
+
93
+ def test_no_window_returns_all(self):
94
+ if not s.tool_exists("rg"):
95
+ self.skipTest("本机无 rg")
96
+ with tempfile.TemporaryDirectory() as tmp:
97
+ for i in range(35):
98
+ (Path(tmp) / f"f_{i:02d}.txt").write_text("目标词命中 here\n")
99
+ code, out = _run_main(
100
+ ["目标词", "--path", tmp, "--max", "30", "--json"])
101
+ self.assertEqual(json.loads(out)["count"], 30)
102
+
103
+
104
+ class TestScopeDocIncludesMd(unittest.TestCase):
105
+ """--scope doc 必须搜到 md(缺陷 4)。"""
106
+
107
+ def test_md_hit_in_doc_scope(self):
108
+ if not s.tool_exists("rg"):
109
+ self.skipTest("本机无 rg")
110
+ with tempfile.TemporaryDirectory() as tmp:
111
+ (Path(tmp) / "notes.md").write_text("检索词在这里\n")
112
+ code, out = _run_main(
113
+ ["检索词", "--path", tmp, "--scope", "doc", "--json"])
114
+ self.assertEqual(code, 0, f"--scope doc 应命中 md,输出:{out}")
115
+ data = json.loads(out)
116
+ self.assertTrue(any("notes.md" in r["path"] for r in data["results"]))
117
+
118
+
119
+ class TestRunQueryContract(unittest.TestCase):
120
+ """run_query 必须始终返回 (text, rc)(缺陷 1)。"""
121
+
122
+ def test_nonexistent_dir_returns_tuple(self):
123
+ result = s.run_query(["x", "--path", "/nonexistent-seek-test-xyz"])
124
+ self.assertIsInstance(result, tuple, "返回裸 int 会让进程内调用方解包崩溃")
125
+ text, rc = result
126
+ self.assertIsInstance(text, str)
127
+ self.assertEqual(rc, 1)
128
+ self.assertIn("目录不存在", text)
129
+
130
+ def test_cli_still_prints_message(self):
131
+ code, out = _run_main(["x", "--path", "/nonexistent-seek-test-xyz"])
132
+ self.assertEqual(code, 1)
133
+ self.assertIn("目录不存在", out)
134
+
135
+
136
+ class TestTimeoutDownpass(unittest.TestCase):
137
+ """structural/git 路径必须把 time_budget 下传给 run(缺陷 5)。"""
138
+
139
+ def test_structural_passes_timeout(self):
140
+ captured = {}
141
+ def fake_run(cmd, timeout=None, **kw):
142
+ captured["timeout"] = timeout
143
+ proc = mock.Mock()
144
+ proc.returncode = 1 # 无命中
145
+ proc.stdout = ""
146
+ return proc
147
+ with mock.patch.object(s, "run", side_effect=fake_run):
148
+ s.structural_search("裸except", "/tmp", [], 5, timeout=5.5)
149
+ self.assertEqual(captured.get("timeout"), 5.5,
150
+ "structural_search 必须把 timeout 传给 run")
151
+
152
+ def test_git_log_passes_timeout(self):
153
+ captured = {}
154
+ def fake_run(cmd, timeout=None, **kw):
155
+ captured["timeout"] = timeout
156
+ proc = mock.Mock()
157
+ proc.returncode = 1
158
+ proc.stdout = ""
159
+ return proc
160
+ with tempfile.TemporaryDirectory() as tmp:
161
+ f = Path(tmp) / "a.txt"
162
+ f.write_text("x\n")
163
+ with mock.patch.object(s, "run", side_effect=fake_run):
164
+ s.git_log(str(f), timeout=7.5)
165
+ self.assertEqual(captured.get("timeout"), 7.5)
166
+
167
+
168
+ if __name__ == "__main__":
169
+ unittest.main()
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env python3
2
+ """2026-10-06 性能轮回归测试:噪声档判定去 syscall + read_lines 单遍读取。
3
+
4
+ 实测背景(cProfile,Documents/GPT 搜 "import" --count,8930 个文件):
5
+ _is_noise_path 独占 1.52s / 1.91s(80%),内含 35727 次 posix.getcwd、
6
+ 103266 次 posix.lstat、35720 次 realpath。根因是每行都做
7
+ `Path(fp).resolve().relative_to(Path(root).resolve())`——resolve() 对每个
8
+ 路径段 lstat 且反复 getcwd;而 _apply_noise_floor 还把根 resolve 了两遍、
9
+ 每行判了两遍。
10
+
11
+ 修法:纯字符串归一化(normpath + 前缀比较),根只算一次、每行只判一次,
12
+ 全程零 syscall。本文件锁定两件事:
13
+ 1. 语义不变——相对/绝对根、根自己叫 tests/、repos/、日期归档、越界路径
14
+ 的判定结果与原实现一致;
15
+ 2. 不得回退到 syscall——_is_noise_path 再调 Path.resolve / realpath / lstat
16
+ 即报红(这是本轮性能收益的守门用例,比计时断言稳定)。
17
+
18
+ read_lines 原实现先 open 数行、再 open 读区间(同一文件开两遍,大文件 IO
19
+ 翻倍),改为单遍流式;本文件锁定区间/越界/空文件三种行为不变。
20
+
21
+ 运行:
22
+ cd ~/.agents/skills/argo
23
+ python3 -m pytest sub-skills/local-seek/tests/test_regression_20261006.py -q
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import os
29
+ import sys
30
+ import tempfile
31
+ import unittest
32
+ from pathlib import Path
33
+ from unittest import mock
34
+
35
+ SCRIPTS = Path(__file__).resolve().parent.parent / "scripts"
36
+ sys.path.insert(0, str(SCRIPTS))
37
+
38
+ import seek as s # noqa: E402
39
+
40
+
41
+ class TestNoisePathSemantics(unittest.TestCase):
42
+ """去 syscall 之后,噪声档判定语义必须与原实现逐条一致。"""
43
+
44
+ def test_root_named_tests_is_not_noise(self):
45
+ # 搜索根自己叫 tests/ 时整棵树不得被判噪声(原实现的核心理由)
46
+ self.assertFalse(s._is_noise_path("/x/tests/a.py", "/x/tests"))
47
+
48
+ def test_src_under_root_is_clean(self):
49
+ self.assertFalse(s._is_noise_path("/x/src/app.py", "/x"))
50
+
51
+ def test_repos_under_root_is_noise(self):
52
+ self.assertTrue(s._is_noise_path("/x/repos/lib.py", "/x"))
53
+
54
+ def test_date_archive_dir_is_noise(self):
55
+ self.assertTrue(s._is_noise_path("/x/2026-09-26_吸纳/a.md", "/x"))
56
+
57
+ def test_relative_root(self):
58
+ # 默认根 "." —— rg 输出相对路径时的常见形态
59
+ self.assertTrue(s._is_noise_path("repos/lib.py", "."))
60
+ self.assertFalse(s._is_noise_path("src/app.py", "."))
61
+
62
+ def test_out_of_root_falls_back_to_full_path(self):
63
+ # 不在根之下(软链/越界):退回整条路径判定
64
+ self.assertTrue(s._is_noise_path("/other/repos/lib.py", "/x"))
65
+ self.assertFalse(s._is_noise_path("/other/src/lib.py", "/x"))
66
+
67
+ def test_noise_floor_clean_first(self):
68
+ rows = [("/x/repos/a.py", 0, ""), ("/x/src/b.py", 0, "")]
69
+ out = s._apply_noise_floor(rows, "/x", 30)
70
+ self.assertEqual(out[0][0], "/x/src/b.py",
71
+ "真源必须排在噪声档之前")
72
+
73
+ def test_noise_floor_backfills_when_clean_short(self):
74
+ # 真源不足时噪声档补满(不是排除,保证仍可达)
75
+ rows = [("/x/repos/a.py", 0, ""), ("/x/src/b.py", 0, "")]
76
+ out = s._apply_noise_floor(rows, "/x", 2)
77
+ self.assertEqual(len(out), 2)
78
+
79
+
80
+ class TestNoisePathNoSyscall(unittest.TestCase):
81
+ """性能守门:判定路径不得再触发任何文件系统解析调用。"""
82
+
83
+ def test_does_not_call_path_resolve(self):
84
+ with mock.patch.object(Path, "resolve",
85
+ side_effect=AssertionError("又调 Path.resolve() 了")):
86
+ rows = [(f"/root/repos/f{i}.py", 0, "") for i in range(100)]
87
+ s._apply_noise_floor(rows, "/root", 30)
88
+
89
+ def test_does_not_call_realpath_or_lstat(self):
90
+ with mock.patch.object(os.path, "realpath",
91
+ side_effect=AssertionError("又调 realpath() 了")), \
92
+ mock.patch.object(os, "lstat",
93
+ side_effect=AssertionError("又调 lstat() 了")):
94
+ rows = [(f"/root/repos/f{i}.py", 0, "") for i in range(100)]
95
+ s._apply_noise_floor(rows, "/root", 30)
96
+
97
+
98
+ class TestReadLinesSinglePass(unittest.TestCase):
99
+ """read_lines 单遍读取:区间/夹取/越界/空文件行为与原实现一致。"""
100
+
101
+ def _write(self, tmp, text):
102
+ f = Path(tmp) / "a.txt"
103
+ f.write_text(text, encoding="utf-8")
104
+ return str(f)
105
+
106
+ def test_range_slice(self):
107
+ with tempfile.TemporaryDirectory() as tmp:
108
+ fp = self._write(tmp, "\n".join(f"L{i}" for i in range(1, 11)) + "\n")
109
+ text, rc = s.read_lines(fp, "3-5")
110
+ self.assertEqual(rc, 0)
111
+ self.assertIn("3: L3", text)
112
+ self.assertIn("5: L5", text)
113
+ self.assertNotIn("6: L6", text)
114
+ self.assertIn("共 10 行", text)
115
+
116
+ def test_clamped_to_total(self):
117
+ with tempfile.TemporaryDirectory() as tmp:
118
+ fp = self._write(tmp, "L1\nL2\n")
119
+ text, rc = s.read_lines(fp, "1-99")
120
+ self.assertEqual(rc, 0)
121
+ self.assertIn("第 1-2 行", text)
122
+
123
+ def test_start_past_end(self):
124
+ with tempfile.TemporaryDirectory() as tmp:
125
+ fp = self._write(tmp, "L1\n")
126
+ text, rc = s.read_lines(fp, "5-6")
127
+ self.assertEqual(rc, 1)
128
+ self.assertIn("只有 1 行", text)
129
+
130
+ def test_empty_file(self):
131
+ with tempfile.TemporaryDirectory() as tmp:
132
+ fp = self._write(tmp, "")
133
+ text, rc = s.read_lines(fp, "1-2")
134
+ self.assertEqual(rc, 1)
135
+ self.assertIn("只有 0 行", text)
136
+
137
+ def test_bad_spec(self):
138
+ with tempfile.TemporaryDirectory() as tmp:
139
+ fp = self._write(tmp, "L1\n")
140
+ text, rc = s.read_lines(fp, "abc")
141
+ self.assertEqual(rc, 1)
142
+ self.assertIn("N-M", text)
143
+
144
+
145
+ if __name__ == "__main__":
146
+ unittest.main()
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env python3
2
+ """local-seek 降级链与 --count 排序回归测试(P0-2 / P0-4,2026-09-13)。
3
+
4
+ 守的两个缺陷:
5
+ 1. 降级链曾把「工具缺失」伪装成「搜索结论」:scope=code 且缺 rg 时落
6
+ mdfind(Spotlight 不索引源码内容)→ 搜 argo 自身符号返回 24 条外部
7
+ 陈旧副本、漏掉正主,还报「未找到匹配(mdfind)」。修复后:缺 rg 直落
8
+ grep,两者皆无才显式报错。
9
+ 2. --count 曾按 rg 原始输出顺序(目录遍历序)截断,「哪些文件命中最多」
10
+ 系统性答错。修复后:全量收集→按命中数降序→取前 N。
11
+
12
+ 运行:
13
+ cd ~/.agents/skills/argo
14
+ python3 -m pytest sub-skills/local-seek/tests/test_seek_degradation.py -q
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import io
20
+ import json
21
+ import sys
22
+ import tempfile
23
+ import unittest
24
+ from contextlib import redirect_stdout
25
+ from pathlib import Path
26
+ from unittest import mock
27
+
28
+ SCRIPTS = Path(__file__).resolve().parent.parent / "scripts"
29
+ sys.path.insert(0, str(SCRIPTS))
30
+
31
+ import seek as s # noqa: E402
32
+
33
+
34
+ def _run_main(argv):
35
+ """跑 seek.main(),捕获 stdout,返回 (exit_code, stdout)。"""
36
+ with mock.patch.object(sys, "argv", ["seek.py"] + argv):
37
+ buf = io.StringIO()
38
+ try:
39
+ with redirect_stdout(buf):
40
+ code = s.main()
41
+ except SystemExit as e:
42
+ code = e.code or 0
43
+ return code or 0, buf.getvalue()
44
+
45
+
46
+ class TestDegradationChain(unittest.TestCase):
47
+ """scope=code 缺 rg 时的降级(全部 mock,不依赖本机装了什么)。"""
48
+
49
+ def _argv(self, tmp: str) -> list[str]:
50
+ return ["def needle", "--scope", "code", "--path", tmp, "--json"]
51
+
52
+ def test_rg_missing_falls_to_grep(self):
53
+ with tempfile.TemporaryDirectory() as tmp:
54
+ (Path(tmp) / "a.py").write_text("def needle(x):\n return x\n")
55
+ with mock.patch.object(s, "tool_exists",
56
+ lambda name: name in ("grep", "mdfind")), \
57
+ mock.patch.object(s, "resolve_grep", lambda: "/usr/bin/grep"), \
58
+ mock.patch.object(s, "grep_search",
59
+ lambda *a, **k: ([(str(Path(tmp) / "a.py"), 1, "def needle(x)")], None)):
60
+ code, out = _run_main(self._argv(tmp))
61
+ self.assertEqual(code, 0)
62
+ data = json.loads(out)
63
+ self.assertEqual(data["engine"], "grep",
64
+ "缺 rg 时必须落 grep,不能落 mdfind")
65
+
66
+ def test_rg_and_grep_missing_reports_tools_not_nomatch(self):
67
+ with tempfile.TemporaryDirectory() as tmp:
68
+ with mock.patch.object(s, "tool_exists", lambda name: False), \
69
+ mock.patch.object(s, "resolve_grep", lambda: None):
70
+ code, out = _run_main(self._argv(tmp))
71
+ self.assertEqual(code, 1)
72
+ self.assertIn("rg", out)
73
+ self.assertIn("grep", out)
74
+ self.assertNotIn("未找到匹配", out,
75
+ "工具缺失不得伪装成搜索结论")
76
+
77
+ def test_real_rg_still_used(self):
78
+ """rg 在位时行为不变(防把好路径改坏)。"""
79
+ with tempfile.TemporaryDirectory() as tmp:
80
+ (Path(tmp) / "a.py").write_text("def needle(x):\n return x\n")
81
+ code, out = _run_main(self._argv(tmp))
82
+ self.assertEqual(code, 0)
83
+ self.assertEqual(json.loads(out)["engine"], "rg")
84
+
85
+
86
+ class TestCountOrdering(unittest.TestCase):
87
+ """--count 按命中数降序(真实 rg 端到端)。"""
88
+
89
+ def test_most_hits_first(self):
90
+ if not s.tool_exists("rg"):
91
+ self.skipTest("本机无 rg")
92
+ with tempfile.TemporaryDirectory() as tmp:
93
+ # 遍历序(字典序)在前面的文件命中少、后面的命中多:
94
+ # 旧实现按遍历序截断会答错。
95
+ (Path(tmp) / "aaa.txt").write_text("needle\n") # 1 次
96
+ (Path(tmp) / "zzz.txt").write_text("needle\nneedle\nneedle\n") # 3 次
97
+ code, out = _run_main(
98
+ ["needle", "--scope", "code", "--path", tmp, "--count",
99
+ "--json"])
100
+ self.assertEqual(code, 0)
101
+ data = json.loads(out)
102
+ results = data.get("results") or []
103
+ files = [r.get("path") if isinstance(r, dict) else r[0]
104
+ for r in results]
105
+ self.assertTrue(files, "无结果")
106
+ self.assertIn("zzz.txt", files[0],
107
+ f"命中最多的文件必须排第一,实际 {files}")
108
+
109
+
110
+ if __name__ == "__main__":
111
+ unittest.main()