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,728 @@
1
+ #!/usr/bin/env python3
2
+ """argo_paths.py — argo 本地状态目录的唯一来源。
3
+
4
+ 背景:此前 11 个模块各自拼 ~/.cache/unified-search,构造方式分裂成 4 种
5
+ (Path.home()/".cache"/...、expanduser("~/.cache/...")、字面量字符串、
6
+ config 默认值),导致 config.yaml 的 cache.db_path 管不住 quota.json、
7
+ health.db 等文件,测试也难以整体隔离。
8
+
9
+ 现在所有状态路径统一由本模块派生:
10
+ - 根目录可被 ARGO_STATE_DIR 覆盖(测试隔离 / 只读环境 / 显式换目录)
11
+ - 其次认 config.yaml 的 cache.db_path 所在目录,保持向后兼容
12
+ - 再次:历史目录已存在就继续用(不迁移),否则落**平台惯例**目录
13
+ (Windows %LOCALAPPDATA%/unified-search;POSIX 遵循 XDG_CACHE_HOME)
14
+ - 各模块只声明「文件名」,不再各自拼目录
15
+ - `python3 scripts/argo_paths.py [--json]` 可打印关键路径的实际来源
16
+
17
+ 注意:User-Agent 里的 unified-search@local 是邮箱标识,与状态目录无关,
18
+ 不在此处管理。
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import contextlib
24
+ import json
25
+ import os
26
+ import sys
27
+ import time
28
+ from pathlib import Path
29
+ from typing import Any, Iterable, Mapping
30
+ from cli_io import dumps, dumps_pretty
31
+
32
+ # 环境变量覆盖:优先级最高,用于测试隔离与只读环境
33
+ ENV_STATE_DIR = "ARGO_STATE_DIR"
34
+ # 平台惯例变量(只读,不新增 argo 私有名字)
35
+ ENV_XDG_CACHE = "XDG_CACHE_HOME"
36
+ ENV_LOCALAPPDATA = "LOCALAPPDATA"
37
+
38
+ # 历史默认目录(也是 config.yaml 中 db_path 的默认前缀)
39
+ _LEGACY_ROOT = "~/.cache/unified-search"
40
+
41
+ # 缓存配置段未就绪时的保底(config 不可用、PyYAML 缺失等场景)
42
+ _FALLBACK_ROOT = _LEGACY_ROOT
43
+
44
+ # 应用名(平台惯例目录里的子目录名)
45
+ _APP_DIRNAME = "unified-search"
46
+
47
+
48
+ def _platform_cache_root(env: Mapping[str, str] | None = None,
49
+ platform: str | None = None) -> Path | None:
50
+ """平台惯例的缓存根(不含应用名):Windows → %LOCALAPPDATA%;其余 → $XDG_CACHE_HOME。
51
+
52
+ 未设置对应变量时返回 None,由调用方回落到 Unix 惯例 ~/.cache。抽成纯函数
53
+ (可注入 env 与 platform)是为了能在 macOS 上直接单测 Windows 分支——否则
54
+ 「Windows 上取哪个目录」永远只有真机才能验证。
55
+ """
56
+ env = os.environ if env is None else env
57
+ platform = sys.platform if platform is None else platform
58
+ if platform.startswith("win"):
59
+ base = str(env.get(ENV_LOCALAPPDATA) or "").strip()
60
+ else:
61
+ base = str(env.get(ENV_XDG_CACHE) or "").strip()
62
+ return Path(os.path.expanduser(base)) if base else None
63
+
64
+
65
+ def platform_cache_default() -> Path:
66
+ """平台惯例的默认状态目录(不含覆盖与配置影响)。
67
+
68
+ Windows:%LOCALAPPDATA%\\unified-search;POSIX(Linux/BSD/macOS):遵循
69
+ XDG_CACHE_HOME,未设置时 ~/.cache/unified-search(XDG 默认值,与历史一致)。
70
+ """
71
+ root = _platform_cache_root()
72
+ if root is not None:
73
+ return root / _APP_DIRNAME
74
+ return Path(os.path.expanduser(_LEGACY_ROOT))
75
+
76
+
77
+ def state_root() -> Path:
78
+ """返回 argo 本地状态根目录(已 expanduser,不保证存在)。
79
+
80
+ 优先级(**存量优先**,避免静默搬家丢掉用户的缓存与配额计数):
81
+
82
+ 1. ARGO_STATE_DIR 环境变量(显式覆盖,测试隔离与只读环境都用它)
83
+ 2. config.yaml cache.db_path 的父目录(保证与主缓存同域)
84
+ 3. 历史目录 ~/.cache/unified-search(**已存在**时继续用;不迁移)
85
+ 4. 平台惯例目录:Windows → %LOCALAPPDATA%\\unified-search;
86
+ POSIX → $XDG_CACHE_HOME/unified-search(该变量设置时)
87
+ 5. 保底:~/.cache/unified-search(即 POSIX 惯例的 XDG 默认值)
88
+
89
+ 为什么「已存在的历史目录」排在平台惯例之前:平台变量(XDG_CACHE_HOME /
90
+ LOCALAPPDATA)在 Windows 上恒有值、在部分 Linux 桌面也常被设置,若它们无条件
91
+ 优先,存量用户升级后会发现搜索缓存、配额计数、准入记录全部"归零"——那是比
92
+ 「目录不够惯例」严重得多的问题。想让 argo 换目录的用户用 ARGO_STATE_DIR
93
+ 显式指定(或直接改 config.yaml 的 cache.db_path),一步到位且可预期。
94
+ """
95
+ override = os.environ.get(ENV_STATE_DIR, "").strip()
96
+ if override:
97
+ return Path(os.path.expanduser(override))
98
+
99
+ db_path = _config_db_path()
100
+ if db_path:
101
+ expanded = os.path.expanduser(db_path)
102
+ parent = os.path.dirname(expanded)
103
+ if parent:
104
+ return Path(parent)
105
+
106
+ legacy = Path(os.path.expanduser(_LEGACY_ROOT))
107
+ try:
108
+ if legacy.is_dir():
109
+ return legacy
110
+ except OSError:
111
+ pass
112
+ return platform_cache_default()
113
+
114
+
115
+ def resolved_paths() -> dict[str, str]:
116
+ """诊断用:把关键路径的**实际来源**摊开(`python3 scripts/argo_paths.py`)。
117
+
118
+ 支持类问题的第一步永远是「你到底在读哪个文件」;此前这些路径散在
119
+ state_root / config.peek_cache_db_path / engine_env._envfile_paths 三处,
120
+ 只能靠读代码推。这里只做只读汇报,不产生任何副作用。
121
+ """
122
+ out: dict[str, str] = {}
123
+ override = os.environ.get(ENV_STATE_DIR, "").strip()
124
+ if override:
125
+ out["state_source"] = f"{ENV_STATE_DIR}={override}"
126
+ elif _config_db_path():
127
+ out["state_source"] = "config.yaml cache.db_path"
128
+ elif Path(os.path.expanduser(_LEGACY_ROOT)).is_dir():
129
+ out["state_source"] = "历史默认目录(已存在,未迁移)"
130
+ else:
131
+ out["state_source"] = "平台惯例默认"
132
+ out["state_root"] = str(state_root())
133
+ out["platform_cache_default"] = str(platform_cache_default())
134
+ out["legacy_root"] = str(Path(os.path.expanduser(_LEGACY_ROOT)))
135
+ try:
136
+ import engine_env
137
+ env_paths = [str(p) for p in engine_env._envfile_paths()]
138
+ out["env_files"] = os.pathsep.join(env_paths)
139
+ out["env_file_in_use"] = next(
140
+ (p for p in env_paths if Path(p).is_file()), "(均不存在)")
141
+ except Exception as e: # engine_env 不可用不该让诊断本身失败
142
+ out["env_files"] = f"(不可用:{e})"
143
+ return out
144
+
145
+
146
+ def migrate_legacy_state(*, yes: bool = False, dry_run: bool = False) -> dict[str, Any]:
147
+ """把历史目录里的状态搬到**平台惯例目录**(可选命令,默认只报告不执行)。
148
+
149
+ 为什么需要它:解析顺序刻意让「已存在的历史目录」优先(避免升级后状态归零),
150
+ 代价是**存量安装永远留在历史路径**——Windows 用户想要 `%LOCALAPPDATA%`、
151
+ Linux 用户想要 `$XDG_CACHE_HOME` 时,此前只能设 `ARGO_STATE_DIR`(还要自己
152
+ 把旧状态搬过去)。这个命令把「搬 + 让惯例生效」合成一步。
153
+
154
+ 安全边界(这是会动用户数据的命令,默认什么都不做):
155
+
156
+ - 仅当根目录由**历史默认**决定时才可迁移。`ARGO_STATE_DIR` 或 config.yaml 的
157
+ `cache.db_path` 说了算时直接拒绝——那是用户明确指定的位置,不该被搬家。
158
+ - 目标目录已存在且有内容时拒绝(绝不覆盖)。
159
+ - 必须显式传 `yes=True`/`--yes` 才会动数据(不做 TTY 探测:那既不是可靠判据,
160
+ 也让脚本与交互两种场景行为不一致)。
161
+ - 搬完尝试 `rmdir` 历史目录(只在空时成功)。这一步是关键:留着空目录会让
162
+ `state_root()` 继续判「历史存在」而停在旧路径,用户会以为数据丢了。
163
+ 目录里还有别的文件(不是 argo 的)时 rmdir 失败,此时明确提示用户设
164
+ `ARGO_STATE_DIR` 或自行清理。
165
+ - 返回结构化结果(moved/skipped/errors),供 CLI 打印与测试断言。
166
+ """
167
+ legacy = Path(os.path.expanduser(_LEGACY_ROOT))
168
+ target = platform_cache_default()
169
+ result: dict[str, Any] = {
170
+ "legacy_root": str(legacy), "target": str(target),
171
+ "moved": [], "errors": [], "status": "", "dry_run": bool(dry_run),
172
+ }
173
+
174
+ if os.environ.get(ENV_STATE_DIR, "").strip():
175
+ result["status"] = f"跳过:{ENV_STATE_DIR} 已显式指定状态目录(那是权威,不改)"
176
+ return result
177
+ if _config_db_path():
178
+ result["status"] = "跳过:config.yaml 的 cache.db_path 已指定位置(那是权威,不改)"
179
+ return result
180
+ if legacy == target:
181
+ result["status"] = "无需迁移:历史目录就是平台惯例目录(POSIX 默认情形)"
182
+ return result
183
+ if not legacy.is_dir():
184
+ result["status"] = "无需迁移:历史目录不存在(新安装直接走平台惯例)"
185
+ return result
186
+
187
+ entries = sorted(p for p in legacy.iterdir() if p.name != "MIGRATED_TO.txt")
188
+ if not entries:
189
+ result["status"] = "无需迁移:历史目录是空的"
190
+ return result
191
+ target_occupied = target.is_dir() and any(target.iterdir())
192
+ if target_occupied:
193
+ result["status"] = f"拒绝:目标目录已有内容 {target}(不覆盖现有状态)"
194
+ return result
195
+ if dry_run:
196
+ # 预演是只读的,不需要 --yes:先看清楚会搬什么,再决定执不执行
197
+ result["status"] = "预演:以下内容将被移动(未执行)"
198
+ result["moved"] = [p.name for p in entries]
199
+ return result
200
+ if not yes:
201
+ # 不用 TTY 探测做交互确认:一来「是不是 tty」不是「要不要执行」的可靠判据
202
+ # (本仓有专门检查挡 isatty),二来数据搬迁命令在脚本里也该有完全一致的
203
+ # 行为——要么显式 --yes,要么不动。
204
+ result["status"] = "拒绝:需显式加 --yes(该命令会移动用户数据;--dry-run 可先看内容)"
205
+ return result
206
+
207
+ import shutil as _shutil
208
+ try:
209
+ target.mkdir(parents=True, exist_ok=True)
210
+ except OSError as e:
211
+ result["status"] = f"失败:无法创建目标目录 {target}({e})"
212
+ return result
213
+
214
+ for p in entries:
215
+ try:
216
+ _shutil.move(str(p), str(target / p.name)) # 跨设备时自动退化复制+删除
217
+ result["moved"].append(p.name)
218
+ except (OSError, _shutil.Error) as e:
219
+ result["errors"].append(f"{p.name}: {e}")
220
+
221
+ try:
222
+ legacy.rmdir() # 仅在空了之后成功——留着会让惯例不生效
223
+ result["legacy_removed"] = True
224
+ except OSError:
225
+ result["legacy_removed"] = False
226
+ try:
227
+ (legacy / "MIGRATED_TO.txt").write_text(
228
+ f"argos 状态已迁移到:{target}\n本目录剩余内容不属于 argo 或无法移动;"
229
+ f"可自行清理,或用 {ENV_STATE_DIR} 显式指定状态目录。\n",
230
+ encoding="utf-8")
231
+ except OSError:
232
+ pass
233
+
234
+ if result["errors"]:
235
+ result["status"] = f"部分完成:{len(result['moved'])} 项已移动,{len(result['errors'])} 项失败"
236
+ else:
237
+ result["status"] = (
238
+ f"完成:{len(result['moved'])} 项已移动到 {target}"
239
+ + ("" if result.get("legacy_removed") else "(历史目录未清空,见其中的 MIGRATED_TO.txt)"))
240
+ return result
241
+
242
+
243
+ def _lock_impl_name() -> str:
244
+ """当前平台实际可用的锁实现——真机验证时第一眼要看的东西。"""
245
+ try:
246
+ import fcntl
247
+ if hasattr(fcntl, "flock"):
248
+ return "flock(POSIX)"
249
+ except ImportError:
250
+ pass
251
+ try:
252
+ import msvcrt
253
+ if hasattr(msvcrt, "locking"):
254
+ return "msvcrt(Windows)"
255
+ except ImportError:
256
+ pass
257
+ return "无(fail-open:不阻断主路径,但多进程状态下会有丢更新)"
258
+
259
+
260
+ def _check_lock_roundtrip(hold_s: float) -> tuple[str, str]:
261
+ """真起一个子进程握住锁,父进程再抢一次——验证互斥真的生效。
262
+
263
+ 这是平台相关的关键自检:Windows 的 msvcrt 分支在 macOS 上只能用假模块测契约,
264
+ 真机行为(锁区间语义、跨进程可见性)只有在目标机器上跑一次才算验过。
265
+ """
266
+ import subprocess
267
+ import tempfile as _tempfile
268
+ lock_path = state_path(".lock-check")
269
+ # 锁文件建不出来时(file_lock 会按设计 fail-open)必须说清原因,否则这条自检
270
+ # 看起来像"锁的实现坏了",而真正的问题在目录可写性上。
271
+ try:
272
+ lock_path.parent.mkdir(parents=True, exist_ok=True)
273
+ lock_path.touch()
274
+ except OSError as e:
275
+ return "warn", (f"锁文件无法创建({lock_path.parent} 不可写:{e})——"
276
+ f"按设计 fail-open,先解决状态目录可写性再看这项")
277
+ child = _tempfile.NamedTemporaryFile("w", suffix=".py", delete=False, encoding="utf-8")
278
+ child.write(
279
+ "import sys, time\n"
280
+ f"sys.path.insert(0, {str(Path(__file__).parent)!r})\n"
281
+ "from pathlib import Path\n"
282
+ "import argo_paths\n"
283
+ f"with argo_paths.file_lock(Path({str(lock_path)!r}), timeout=5.0):\n"
284
+ " print('LOCKED', flush=True)\n"
285
+ f" time.sleep({hold_s!r})\n")
286
+ child.close()
287
+ proc = None
288
+ try:
289
+ proc = subprocess.Popen([sys.executable, child.name],
290
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE,
291
+ text=True, encoding="utf-8", errors="replace")
292
+ line = proc.stdout.readline().strip() if proc.stdout else ""
293
+ if line != "LOCKED":
294
+ err = (proc.stderr.read() if proc.stderr else "").strip()[:200]
295
+ return "fail", f"子进程没能持锁(stdout={line!r} stderr={err!r})"
296
+ t0 = time.monotonic()
297
+ with file_lock(lock_path, timeout=3.0):
298
+ waited = (time.monotonic() - t0) * 1000
299
+ if waited < 100:
300
+ return "fail", f"第二个持有者没被挡住({waited:.0f} ms 就拿到锁)"
301
+ return "pass", f"互斥生效:第二个持有者等待 {waited:.0f} ms"
302
+ finally:
303
+ if proc is not None:
304
+ try:
305
+ proc.wait(timeout=5)
306
+ except subprocess.TimeoutExpired:
307
+ proc.kill()
308
+ for p in (child.name, str(lock_path),
309
+ # file_lock 的真身是 sidecar(.{name}.lock,见 file_lock
310
+ # 实现):只删 lock_path 本体会每次 --check 泄漏一个
311
+ # ..lock-check.lock(2026-10-17 审查发现 6,现场实证)。
312
+ str(lock_path.parent / f".{lock_path.name}.lock")):
313
+ try:
314
+ os.unlink(p)
315
+ except OSError:
316
+ pass
317
+
318
+
319
+ def run_checks(lock_hold_s: float = 0.6) -> list[dict[str, Any]]:
320
+ """在**当前机器**上实测一遍路径解析与关键能力(`argo paths --check`)。
321
+
322
+ 为什么要有它:跨平台分支(Windows 用哪个目录、msvcrt 锁是否真互斥、解释器候选
323
+ 最终落到谁)在开发机上只能做契约级测试,真机验证此前得靠人肉翻代码。这里把
324
+ 「这台机器上实际发生了什么」一次打印清楚——任何平台一条命令自行验证。
325
+ """
326
+ checks: list[dict[str, Any]] = []
327
+
328
+ def add(name: str, status: str, detail: str) -> None:
329
+ checks.append({"check": name, "status": status, "detail": detail})
330
+
331
+ info = resolved_paths()
332
+ add("平台", "info",
333
+ f"{sys.platform} | 状态目录来源:{info['state_source']} | 锁实现:{_lock_impl_name()}")
334
+
335
+ root = state_root()
336
+ add("状态目录解析", "pass", f"{root}(来源:{info['state_source']})")
337
+ if (str(root) == info["legacy_root"]
338
+ and str(root) != info["platform_cache_default"]):
339
+ add("惯例提示", "warn",
340
+ f"当前用历史目录,平台惯例目录是 {info['platform_cache_default']};"
341
+ f"想切换先 `argo paths --migrate --dry-run` 看内容")
342
+
343
+ probe = root / ".write-check"
344
+ try:
345
+ root.mkdir(parents=True, exist_ok=True)
346
+ probe.write_text("ok", encoding="utf-8")
347
+ probe.unlink()
348
+ add("状态目录可写", "pass", str(root))
349
+ except OSError as e:
350
+ add("状态目录可写", "fail",
351
+ f"{root} 不可写:{e}(可设 ARGO_STATE_DIR 指向可写目录)")
352
+
353
+ env_files = [p for p in info["env_files"].split(os.pathsep) if p]
354
+ if any(Path(p).is_file() for p in env_files):
355
+ try:
356
+ import engine_env
357
+ keys = engine_env._envfile_load()
358
+ add("密钥文件", "pass",
359
+ f"在读 {info['env_file_in_use']}({len(keys)} 个键;值不回显)")
360
+ except Exception as e:
361
+ add("密钥文件", "fail", f"{info['env_file_in_use']} 解析失败:{e}")
362
+ else:
363
+ add("密钥文件", "warn",
364
+ f"候选均不存在:{'、'.join(env_files)}(未配置密钥时属正常)")
365
+
366
+ try:
367
+ from config import load_config
368
+ cfg = load_config()
369
+ n = len([k for k, v in (cfg.get("engines") or {}).items() if isinstance(v, dict)])
370
+ add("配置加载", "pass", f"engines={n}")
371
+ except Exception as e:
372
+ add("配置加载", "fail", f"load_config 失败:{e}")
373
+
374
+ try:
375
+ from importlib.machinery import SourceFileLoader
376
+ import importlib.util as _ilu
377
+ bin_path = Path(__file__).parent.parent / "bin" / "argo"
378
+ spec = _ilu.spec_from_file_location(
379
+ "argo_bin_check", bin_path,
380
+ loader=SourceFileLoader("argo_bin_check", str(bin_path)))
381
+ mod = _ilu.module_from_spec(spec)
382
+ spec.loader.exec_module(mod)
383
+ if mod._self_capable():
384
+ add("解释器", "pass", f"当前解释器就地执行({sys.executable})")
385
+ else:
386
+ picked = mod._pick_python()
387
+ add("解释器", "pass" if picked else "fail",
388
+ f"探测结果:{picked or '未找到可用解释器'}")
389
+ except Exception as e:
390
+ add("解释器", "warn", f"未能检查({e})")
391
+
392
+ try:
393
+ status, detail = _check_lock_roundtrip(lock_hold_s)
394
+ add("跨进程锁", status, detail)
395
+ except Exception as e:
396
+ add("跨进程锁", "fail", f"自检异常:{e}")
397
+
398
+ return checks
399
+
400
+
401
+ def _cli() -> int:
402
+ import argparse
403
+ parser = argparse.ArgumentParser(
404
+ description="argo 路径诊断:状态目录与密钥文件到底解析到了哪里")
405
+ parser.add_argument("--json", action="store_true", help="机器可读输出")
406
+ parser.add_argument("--check", action="store_true",
407
+ help="实测本机解析与能力(目录可写性 / 跨进程锁 / 配置 / 解释器)")
408
+ parser.add_argument("--migrate", action="store_true",
409
+ help="把历史状态目录搬到平台惯例目录(默认只报告)")
410
+ parser.add_argument("--yes", action="store_true",
411
+ help="配合 --migrate:确认执行(该命令会移动数据)")
412
+ parser.add_argument("--dry-run", action="store_true",
413
+ help="配合 --migrate:只列出将移动的内容")
414
+ args = parser.parse_args()
415
+
416
+ if args.check:
417
+ checks = run_checks()
418
+ if args.json:
419
+ print(dumps(checks))
420
+ else:
421
+ marks = {"pass": "通过", "fail": "失败", "warn": "注意", "info": "信息"}
422
+ print(f"argo 路径自检({sys.platform})")
423
+ for c in checks:
424
+ print(f" [{marks.get(c['status'], c['status'])}] {c['check']}:{c['detail']}")
425
+ return 0 if all(c["status"] != "fail" for c in checks) else 1
426
+
427
+ if args.migrate:
428
+ result = migrate_legacy_state(yes=args.yes, dry_run=args.dry_run)
429
+ if args.json:
430
+ print(dumps(result))
431
+ else:
432
+ print(f"状态:{result['status']}")
433
+ for name in result["moved"]:
434
+ print(f" 移动 {name} → {result['target']}")
435
+ for err in result["errors"]:
436
+ print(f" 失败 {err}")
437
+ if not args.dry_run and result["moved"]:
438
+ print(f"此后 state_root 解析为:{state_root()}")
439
+ return 0 if not result["errors"] else 1
440
+
441
+ info = resolved_paths()
442
+ if args.json:
443
+ print(dumps(info))
444
+ else:
445
+ for k, v in info.items():
446
+ print(f"{k:22} {v}")
447
+ return 0
448
+
449
+
450
+ def _config_db_path() -> str | None:
451
+ """从 config.yaml 读 cache.db_path;不可用时返回 None。
452
+
453
+ config 模块本身可能不可用(PyYAML 缺失 / 配置文件损坏),
454
+ 此处必须 fail-open,否则路径派生会连带崩溃。
455
+
456
+ 走 peek_cache_db_path() 轻量读取:get_cache_config() 会触发 load_config()
457
+ 合并全部外置引擎 spec,而 import cache 时就会调到本函数——为一个标量付
458
+ 整轮合并不值得。
459
+
460
+ (勘误 2026-09-15:此处的「约 1.7s」无法复现。实测 load_config() 为
461
+ 纯 Python loader 107 ms / C 版 15 ms;真实问题是 import 链上这条派生被
462
+ 连调 4 次,且每次都用慢的 loader 解析 123 KB 配置。)
463
+ """
464
+ try:
465
+ from config import peek_cache_db_path
466
+ return peek_cache_db_path()
467
+ except Exception:
468
+ return None
469
+
470
+
471
+ def state_path(*parts: str) -> Path:
472
+ """返回状态根目录下的路径(不自动创建目录)。
473
+
474
+ state_path("health.db") → <root>/health.db
475
+ state_path("admission", "x.json") → <root>/admission/x.json
476
+ """
477
+ return state_root().joinpath(*parts)
478
+
479
+
480
+ def ensure_state_dir(*parts: str) -> Path:
481
+ """返回状态根目录下的子目录,并确保其存在。
482
+
483
+ 目录不可创建时(只读挂载 / 权限受限)fail-open 返回目标路径——
484
+ 由调用方在写入时处理,避免 import 期就崩。
485
+ """
486
+ d = state_path(*parts)
487
+ try:
488
+ d.mkdir(parents=True, exist_ok=True)
489
+ except OSError:
490
+ pass
491
+ return d
492
+
493
+
494
+ def legacy_root() -> Path:
495
+ """历史默认目录(仅用于迁移/兼容判断,新代码请用 state_root)。"""
496
+ return Path(os.path.expanduser(_LEGACY_ROOT))
497
+
498
+
499
+ @contextlib.contextmanager
500
+ def file_lock(path: Path, *, timeout: float = 10.0):
501
+ """跨进程排他锁(阻塞获取;超时按 fail-open 放行,见下方「fail-open」段)。
502
+
503
+ 为什么需要:状态文件的「读-改-写」序列只在**进程内**加锁,
504
+ CLI / MCP server / 评测脚本三者并行时,进程 A 读到旧状态、
505
+ 进程 B 也读到旧状态,各自 +1 后依次覆盖,后写者抹掉前者的增量
506
+ (实测 6 进程 × 60 次 record 状态丢失 77%)。
507
+
508
+ 实现:flock 锁在独立的 .lock 文件上,不与被保护的数据文件共享
509
+ inode——数据文件靠 os.replace 整体替换,若锁与数据同 inode,
510
+ 替换后新进程会锁到另一个 inode 而形同无锁。
511
+
512
+ fail-open:两种情况都会直接放行、绝不因观测层问题阻断搜索主路径——
513
+ (1) 平台不支持任何锁实现;(2) 超时抢不到锁(此时会 log warning 到
514
+ `argo.file_lock`,让运维看得到「无锁执行」发生过,但不抛异常)。
515
+ 契约由 tests/test_argo_paths.py::TestFileLock::test_timeout_fails_open 锁死。
516
+ """
517
+ lock_path = path.parent / f".{path.name}.lock"
518
+ try:
519
+ lock_path.parent.mkdir(parents=True, exist_ok=True)
520
+ except OSError:
521
+ yield
522
+ return
523
+
524
+ fd = None
525
+ try:
526
+ fd = os.open(str(lock_path), os.O_CREAT | os.O_RDWR, 0o600)
527
+ except OSError:
528
+ yield
529
+ return
530
+
531
+ try:
532
+ import fcntl # POSIX
533
+ except ImportError:
534
+ fcntl = None
535
+
536
+ lock_impl = _make_lock_impl(fcntl)
537
+ if lock_impl is None:
538
+ # 两个平台都没有可用锁(不认识的系统)→ fail-open,绝不阻断主路径
539
+ try:
540
+ yield
541
+ finally:
542
+ os.close(fd)
543
+ return
544
+
545
+ acquire, release = lock_impl
546
+ deadline = time.monotonic() + timeout
547
+ acquired = False
548
+ while True:
549
+ try:
550
+ acquire(fd)
551
+ acquired = True
552
+ break
553
+ except OSError:
554
+ if time.monotonic() >= deadline:
555
+ # 有意 fail-open:锁是保护层,不该成为单点故障(见
556
+ # tests/test_argo_paths.py::TestFileLock::test_timeout_fails_open)。
557
+ # 但也不能纯静默——让运维看得到「无锁执行」发生过。
558
+ import logging
559
+ logging.getLogger("argo.file_lock").warning(
560
+ "file_lock(%s) timed out after %.1fs; proceeding without lock "
561
+ "(fail-open by design; read-modify-write races possible)",
562
+ path, timeout,
563
+ )
564
+ break
565
+ time.sleep(0.002)
566
+ try:
567
+ yield
568
+ finally:
569
+ if acquired:
570
+ try:
571
+ release(fd)
572
+ except OSError:
573
+ pass
574
+ os.close(fd)
575
+
576
+
577
+ def _make_lock_impl(fcntl: Any):
578
+ """选一套可用的文件锁实现 → (acquire(fd), release(fd));都不可用返回 None。
579
+
580
+ 为什么要两套:Windows 没有 `fcntl`,此前这里直接 fail-open——于是
581
+ 「6 进程 × 60 次 record 状态丢失 77%」这类丢更新在 Windows 上原样复现,
582
+ 而调用方完全看不出来(没有任何日志,锁看起来"加了")。Windows 上的对应
583
+ 设施是 `msvcrt.locking`:对文件的一段字节区间加锁,语义与 flock 足够接近。
584
+
585
+ 差异点(都在这里吸收掉,不让调用方感知):
586
+ - msvcrt 锁的是**相对当前文件位置**的字节区间 → 每次加/解锁前 seek(0);
587
+ - 空文件上锁区间为空,先写一个字节把它变成合法区间(内容是占位,无人读);
588
+ - msvcrt 的 LK_NBLCK 抢不到时抛 OSError,与 flock 的 LOCK_NB 一致,
589
+ 所以外层重试循环两种实现可以共用。
590
+ """
591
+ if fcntl is not None and hasattr(fcntl, "flock"):
592
+ def _acq(fd: int) -> None:
593
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
594
+
595
+ def _rel(fd: int) -> None:
596
+ fcntl.flock(fd, fcntl.LOCK_UN)
597
+
598
+ return _acq, _rel
599
+
600
+ try:
601
+ import msvcrt # Windows
602
+ except ImportError:
603
+ return None
604
+
605
+ def _acq_win(fd: int) -> None:
606
+ if os.fstat(fd).st_size == 0:
607
+ os.write(fd, b"\0") # 让锁区间非空
608
+ os.lseek(fd, 0, os.SEEK_SET)
609
+ msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
610
+
611
+ def _rel_win(fd: int) -> None:
612
+ os.lseek(fd, 0, os.SEEK_SET)
613
+ msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
614
+
615
+ return _acq_win, _rel_win
616
+
617
+
618
+ def atomic_write_text(path: Path, text: str, *, mode: int | None = None) -> None:
619
+ """原子写文本文件的唯一来源(任意内容,非仅 JSON)。
620
+
621
+ 临时文件用 mkstemp 取**进程内唯一**名字(同目录,保证同文件系统
622
+ rename 语义),失败路径只清理自己的 tmp。`mode` 非空时对最终文件
623
+ 收紧权限(密钥类配置写 0600)。
624
+ """
625
+ import tempfile # 延迟导入:tempfile→shutil→random 子树实测 ~6ms,
626
+ # 纯缓存命中的搜索一次原子写都不做,没理由为它每次进程启动都付这笔钱
627
+ # (与 config.py 的惰性 logging 同一范式;_check_lock_roundtrip 已在用)。
628
+ path = Path(path)
629
+ path.parent.mkdir(parents=True, exist_ok=True)
630
+ fd, tmp_name = tempfile.mkstemp(
631
+ dir=str(path.parent), prefix=f".{path.name}.", suffix=".tmp")
632
+ try:
633
+ with os.fdopen(fd, "w", encoding="utf-8") as f:
634
+ f.write(text)
635
+ if mode is not None:
636
+ os.chmod(tmp_name, mode)
637
+ os.replace(tmp_name, path)
638
+ except BaseException:
639
+ # 只清理自己创建的 tmp;别人的 tmp 不归本进程管
640
+ try:
641
+ os.unlink(tmp_name)
642
+ except OSError:
643
+ pass
644
+ raise
645
+
646
+
647
+ def atomic_write_json(path: Path, payload: Any, *, indent: int | None = 2) -> None:
648
+ """原子写 JSON 状态的唯一来源。
649
+
650
+ why:此前 quota / circuit_breaker / lang_pref / v2ex_nodes / job /
651
+ fetch_v3 / redskill 多处各自手写 `p.with_suffix(".tmp")` + replace,
652
+ **临时文件名固定**。多进程(CLI 与 MCP server 并行、或评测脚本)
653
+ 同时写同一个文件时,A 的 replace 会把 B 的 tmp 一起搬走/删掉,B 再
654
+ replace 就抛 FileNotFoundError。实测 6 进程 × 60 次 record:崩溃 235
655
+ 次、成功仅 125 次、状态丢失 68%——配额计数因此系统性偏低,且
656
+ errors > used 的反常正是这么来的。
657
+
658
+ 修法:临时文件用 mkstemp 取**进程内唯一**名字(同目录,保证同文件
659
+ 系统 rename 语义),失败路径清理自己的 tmp,绝不触碰别人的。
660
+ 实现在 atomic_write_text;本函数只负责序列化。
661
+ """
662
+ content = dumps_pretty(payload) if indent else dumps(payload)
663
+ atomic_write_text(path, content)
664
+
665
+
666
+ # WAL 回缩上限与自动检查点页数:argo 状态库的统一 WAL 策略。
667
+ #
668
+ # 为什么需要(2026-09-17 实测):SQLite 默认 `journal_size_limit=-1`,检查点后
669
+ # 不回缩 `-wal`,文件长期停在自动检查点阈值(1000 页 ≈ 3.94 MB)上。实测
670
+ # 「limit=1 MB + autocheckpoint=256 页」把稳态压到 0.95 MB;只设 limit 不降
671
+ # 检查点阈值无效(仍 2.84 MB)——**真正起作用的是检查点频率**。
672
+ # 本机 cache.db + adaptive.db 两库合计省约 6 MB。
673
+ WAL_SIZE_LIMIT_BYTES = 1024 * 1024
674
+ WAL_AUTOCHECKPOINT_PAGES = 256
675
+
676
+
677
+ def apply_state_pragmas(conn: Any) -> None:
678
+ """argo 状态库连接的统一设置:WAL + 回缩上限 + 检查点阈值。
679
+
680
+ why:此前 cache.py / adaptive.py / health_probe.py 各自写一份
681
+ `PRAGMA journal_mode=WAL`,三份必然漂移——新增状态库时只会照抄
682
+ journal_mode,漏掉回缩上限;改策略时也要改三处。收敛到一处的代价是
683
+ 多一次函数调用,收益是「WAL 策略」这件事只有一个定义点。
684
+
685
+ 只改 PRAGMA,不碰连接生命周期(各模块的连接复用方式不同,刻意不统一)。
686
+ """
687
+ conn.execute("PRAGMA journal_mode=WAL")
688
+ conn.execute(f"PRAGMA journal_size_limit={WAL_SIZE_LIMIT_BYTES}")
689
+ conn.execute(f"PRAGMA wal_autocheckpoint={WAL_AUTOCHECKPOINT_PAGES}")
690
+
691
+
692
+ def isolate_state_dir(tag: str = "argo-dev") -> Path:
693
+ """把状态目录重定向到独立临时目录,并返回该目录。
694
+
695
+ **必须在 import 任何 argo 状态模块之前调用**——quota / circuit_breaker /
696
+ cache / adaptive 等在模块级就按当时的 ARGO_STATE_DIR 定下路径常量,
697
+ 之后再改环境变量不生效。
698
+
699
+ 为什么需要:开发/评测脚本(ab_eval、matrix_search_eval、benchmark…)
700
+ 会走真实搜索路径,从而写生产状态。实测污染后果——真实
701
+ circuit_breaker.json 里混进 190 个 `eng_<8hex>` 夹具条目、`probe…`、
702
+ `bad` 等测试引擎,熔断统计被稀释,且这些脏条目永久留在生产状态里。
703
+ """
704
+ import tempfile # 延迟导入(同 atomic_write_text):隔离状态目录才需要
705
+ d = Path(tempfile.mkdtemp(prefix=f"{tag}-state-"))
706
+ os.environ[ENV_STATE_DIR] = str(d)
707
+ return d
708
+
709
+
710
+ def db_path() -> Path:
711
+ """主缓存库路径。
712
+
713
+ 与 state_path("cache.db") 的差别只在「config.yaml 里用户显式改写了
714
+ db_path」这一种情况:此时尊重用户的显式配置,不放回状态根目录。
715
+
716
+ ARGO_STATE_DIR 一旦设置,一律优先——它是测试隔离与只读环境的硬开关,
717
+ 不能被磁盘上的 config.yaml 盖掉(否则 env 形同虚设)。
718
+ """
719
+ if os.environ.get(ENV_STATE_DIR, "").strip():
720
+ return state_path("cache.db")
721
+ raw = _config_db_path()
722
+ if raw:
723
+ return Path(os.path.expanduser(raw))
724
+ return state_path("cache.db")
725
+
726
+
727
+ if __name__ == "__main__":
728
+ raise SystemExit(_cli())