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,591 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ archive_run.py — 搜索/研究 run 工作区归档
4
+
5
+ 职责边界:
6
+ - 搜索只做发现;本模块把**当次 envelope** 写入文件,便于复用与分析
7
+ - **不**抓取正文、**不**下载媒体、**不**覆盖旧 run
8
+ - known-url / 正文吸收仍走 fetch / extract / 其他打包工具
9
+
10
+ 目录约定(默认)::
11
+
12
+ <root>/
13
+ index.jsonl # 全局索引(一行一个 run)
14
+ runs/
15
+ YYYY-MM-DD/
16
+ <run_id>/
17
+ run-summary.json # 元数据 + 计数 + 路径
18
+ envelope.json # 完整可公开搜索结果(去 _ 私有键)
19
+ candidates.jsonl # 候选一行一条
20
+ results.jsonl # 原始 results 一行一条
21
+ coverage.json # 后端覆盖
22
+ INDEX.md # 人读摘要
23
+
24
+ 环境变量:
25
+ ARGO_ARCHIVE_ROOT 覆盖默认归档根目录
26
+
27
+ 默认根目录解析顺序:
28
+ 1. ARGO_ARCHIVE_ROOT
29
+ 2. 工作区存在 AGENTS.md 时 → <workspace>/数据/argo-search-archive
30
+ 3. ./数据/argo-search-archive(若父级 数据/ 可建)
31
+ 4. ./argo-search-archive
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import argparse
37
+ import hashlib
38
+ import json
39
+ import os
40
+ import re
41
+ import sys
42
+ from datetime import datetime, timezone, timedelta
43
+ from pathlib import Path
44
+ from typing import Any
45
+ from cli_io import dumps, dumps_pretty
46
+
47
+ _TZ_CN = timezone(timedelta(hours=8))
48
+ SCHEMA = "argo-search-archive/1.0"
49
+
50
+
51
+ def _now() -> datetime:
52
+ return datetime.now(_TZ_CN)
53
+
54
+
55
+ def _slug(text: str, max_len: int = 48) -> str:
56
+ s = re.sub(r"\s+", " ", (text or "").strip())
57
+ s = re.sub(r"[^\w\u4e00-\u9fff.\-]+", "_", s, flags=re.U)
58
+ s = s.strip("._") or "query"
59
+ return s[:max_len]
60
+
61
+
62
+ def _public_payload(result: dict[str, Any]) -> dict[str, Any]:
63
+ """去掉私有键与过大字段,避免凭证泄漏。"""
64
+ out: dict[str, Any] = {}
65
+ for k, v in result.items():
66
+ if k.startswith("_"):
67
+ continue
68
+ if k in {"raw_html", "headers", "cookie", "cookies", "authorization"}:
69
+ continue
70
+ out[k] = v
71
+ return out
72
+
73
+
74
+ # ── 凭证值级脱敏(写入时生效)────────────────────────────────────────────────
75
+ # _public_payload 删的是「整个字段」(headers/cookie);这里补的是「值里的片段」:
76
+ # URL 查询串里嵌的密钥(很多引擎把 key 放 query)、正文里贴的凭证。脱敏必须
77
+ # 发生在写入时,而不是读取/导出时——密钥一旦以明文写入文件,就已经泄漏给所有
78
+ # 能读到该文件的人,事后清理依赖「有人记得在出口调用」,不可靠。
79
+ _SECRET_QUERY_KEYS = (
80
+ "key", "keys", "api_key", "apikey", "api-key", "access_key",
81
+ "access_token", "token", "auth", "authorization", "credential",
82
+ "password", "passwd", "pwd", "secret", "sig", "signature", "sign",
83
+ "session", "sessionid", "sessdata", "cookie", "bili_jct", "d_c0",
84
+ )
85
+ _SECRET_QUERY_RE = re.compile(
86
+ r"((?:[?&;]|\b)(?:%s)=)([^&;\s\"']+)" % "|".join(
87
+ re.escape(k) for k in _SECRET_QUERY_KEYS), re.IGNORECASE)
88
+ _BEARER_RE = re.compile(r"(bearer\s+)[A-Za-z0-9._\-]+", re.IGNORECASE)
89
+ # 厂商密钥前缀:不少上游在错误体里回显密钥片段
90
+ # (如 OpenAI `Incorrect API key provided: sk-...`),而它既不是 query 参数
91
+ # 也不是 Bearer 头,前两类规则都漏。前缀取主流按段可辨识者。
92
+ # Google/AWS/Slack 三家的 key 不带统一分隔符(实测 AIzaSy…、AKIA…、xoxb-…
93
+ # 在上面「前缀+分隔符」规则下全部漏过),按各家官方形态单列:AIza 起头总长
94
+ # 39 字符、AKIA 后接 16 位大写数字、xoxb- 后接数字段。同一文本里兄弟 key
95
+ # 各自独立命中,re.sub 全量替换。
96
+ _VENDOR_KEY_RE = re.compile(
97
+ r"\b(?:sk|rk|pk|gsk|xai|hf|ghp|gho|github_pat|glpat|ya29)"
98
+ r"[-_][A-Za-z0-9._\-]{8,}"
99
+ r"|\bAIza[A-Za-z0-9_\-]{30,}"
100
+ r"|\bAKIA[A-Z0-9]{16}"
101
+ r"|\bxoxb-[A-Za-z0-9\-]{20,}")
102
+ _HOME_PATH_RE = re.compile(
103
+ r"([A-Za-z]:\\Users\\[^\\\s:\"]+" # Windows:C:\Users\<name>
104
+ r"|/Users/[^/\s:\"]+" # macOS
105
+ r"|/home/[^/\s:\"]+)") # Linux / BSD
106
+
107
+
108
+ def redact_secrets(text: str) -> str:
109
+ """字符串里的凭证值改写为 [REDACTED],家目录改写为 ~。
110
+
111
+ 按类别正则实现,逐类有测试锁定;不加白名单——宁可多脱一个无害参数
112
+ (多脱只损失一点调试信息),不可漏一个真密钥。
113
+ """
114
+ if not text:
115
+ return text
116
+ out = _SECRET_QUERY_RE.sub(r"\1[REDACTED]", text)
117
+ out = _BEARER_RE.sub(r"\1[REDACTED]", out)
118
+ out = _VENDOR_KEY_RE.sub("[REDACTED]", out)
119
+ out = _HOME_PATH_RE.sub("~", out)
120
+ return out
121
+
122
+
123
+ def _redact_deep(obj: Any) -> Any:
124
+ """递归脱敏:dict/list 逐值处理,str 应用规则,其余原样。"""
125
+ if isinstance(obj, dict):
126
+ return {k: _redact_deep(v) for k, v in obj.items()}
127
+ if isinstance(obj, (list, tuple)):
128
+ return [_redact_deep(v) for v in obj]
129
+ if isinstance(obj, str):
130
+ return redact_secrets(obj)
131
+ return obj
132
+
133
+
134
+ def resolve_archive_root(explicit: str | Path | None = None) -> Path:
135
+ if explicit:
136
+ return Path(explicit).expanduser().resolve()
137
+ env = os.environ.get("ARGO_ARCHIVE_ROOT", "").strip()
138
+ if env:
139
+ return Path(env).expanduser().resolve()
140
+
141
+ # 向上找带 AGENTS.md 的工作区
142
+ cwd = Path.cwd().resolve()
143
+ for p in [cwd, *cwd.parents]:
144
+ if (p / "AGENTS.md").is_file():
145
+ root = p / "数据" / "argo-search-archive"
146
+ return root
147
+ if p == p.parent:
148
+ break
149
+
150
+ data_local = cwd / "数据" / "argo-search-archive"
151
+ return data_local
152
+
153
+
154
+ def make_run_id(query: str, when: datetime | None = None) -> str:
155
+ when = when or _now()
156
+ stamp = when.strftime("%Y%m%dT%H%M%S")
157
+ qh = hashlib.sha256((query or "").encode("utf-8")).hexdigest()[:8]
158
+ return f"{stamp}_{_slug(query)}_{qh}"
159
+
160
+
161
+ def search_archive(query: str, root: Path | None = None,
162
+ since: str | None = None, until: str | None = None,
163
+ limit: int = 10) -> list[dict[str, Any]]:
164
+ """检索归档索引(成果复用):主题词命中的历史研究/搜索 run。
165
+
166
+ - 数据源:{root}/index.jsonl(无索引时返回空)
167
+ - 过滤:query/tag/note 子串命中(query 必需);packed_at 日期窗可选
168
+ - 返回:run_id / packed_at / query / tag / source / counts / run_dir
169
+ """
170
+ archive_root = (root or resolve_archive_root()).expanduser().resolve()
171
+ index_path = archive_root / "index.jsonl"
172
+ if not index_path.exists():
173
+ return []
174
+ kw = (query or "").strip().lower()
175
+ out: list[dict[str, Any]] = []
176
+ try:
177
+ with index_path.open(encoding="utf-8") as f:
178
+ for line in f:
179
+ line = line.strip()
180
+ if not line:
181
+ continue
182
+ try:
183
+ row = json.loads(line)
184
+ except ValueError:
185
+ continue
186
+ hay = " ".join([
187
+ str(row.get("query") or ""),
188
+ str(row.get("tag") or ""),
189
+ str(row.get("note") or ""),
190
+ ]).lower()
191
+ if kw and kw not in hay:
192
+ continue
193
+ packed = str(row.get("packed_at") or "")
194
+ day = packed[:10]
195
+ if since and day and day < since:
196
+ continue
197
+ if until and day and day > until:
198
+ continue
199
+ out.append({
200
+ "run_id": row.get("run_id"),
201
+ "packed_at": packed,
202
+ "query": row.get("query"),
203
+ "tag": row.get("tag"),
204
+ "source": row.get("source"),
205
+ "status": row.get("status"),
206
+ "counts": row.get("counts") or {},
207
+ "run_dir": row.get("run_dir"),
208
+ })
209
+ if len(out) >= limit:
210
+ break
211
+ except OSError:
212
+ return []
213
+ return out
214
+
215
+
216
+ def write_search_archive(
217
+ result: dict[str, Any],
218
+ *,
219
+ root: Path | None = None,
220
+ tag: str | None = None,
221
+ note: str | None = None,
222
+ source: str = "argo_search",
223
+ ) -> dict[str, Any]:
224
+ """将搜索 envelope 写入新 run 目录。永不覆盖已有路径。
225
+
226
+ Returns:
227
+ handoff 摘要:{run_id, root, paths, counts, schema_version}
228
+ """
229
+ archive_root = root or resolve_archive_root()
230
+ archive_root = archive_root.expanduser().resolve()
231
+ when = _now()
232
+ query = str(result.get("query") or result.get("query_original") or "")
233
+ run_id = make_run_id(query, when)
234
+ day = when.strftime("%Y-%m-%d")
235
+ run_dir = archive_root / "runs" / day / run_id
236
+ if run_dir.exists():
237
+ # 碰撞:追加序号
238
+ for i in range(2, 100):
239
+ cand = archive_root / "runs" / day / f"{run_id}-r{i}"
240
+ if not cand.exists():
241
+ run_dir = cand
242
+ run_id = run_dir.name
243
+ break
244
+ else:
245
+ raise RuntimeError(f"无法分配 run 目录: {run_dir}")
246
+
247
+ run_dir.mkdir(parents=True, exist_ok=False)
248
+ public = _public_payload(result)
249
+ # 凭证脱敏在写入时生效:URL 查询串密钥 / Bearer 头 / 家目录路径,
250
+ # 覆盖 envelope 与三类 jsonl 的全部下游数据
251
+ public = _redact_deep(public)
252
+ candidates = public.get("candidates") if isinstance(public.get("candidates"), list) else []
253
+ results = public.get("results") if isinstance(public.get("results"), list) else []
254
+ coverage = public.get("coverage") if isinstance(public.get("coverage"), list) else []
255
+ sources = public.get("sources") if isinstance(public.get("sources"), list) else []
256
+ if not sources and results:
257
+ # 归档时补全 sources,与 search.build_sources 语义一致
258
+ try:
259
+ from search import build_sources # type: ignore
260
+ sources = build_sources(results)
261
+ public["sources"] = sources
262
+ except Exception:
263
+ sources = [
264
+ {
265
+ "ref": i + 1,
266
+ "title": (r.get("title") or "")[:160],
267
+ "url": r.get("url"),
268
+ "engine": r.get("source"),
269
+ }
270
+ for i, r in enumerate(results)
271
+ if isinstance(r, dict) and r.get("url")
272
+ ]
273
+ public["sources"] = sources
274
+
275
+ # 若无 candidates 但有 results,做最小投影(不 import 循环依赖时的保底)
276
+ if not candidates and results:
277
+ try:
278
+ from candidate_envelope import result_to_candidate # type: ignore
279
+ candidates = [
280
+ result_to_candidate(r, query, rank=i + 1)
281
+ for i, r in enumerate(results)
282
+ if isinstance(r, dict) and "error" not in r
283
+ ]
284
+ except Exception:
285
+ # 兜底投影:candidate_envelope 不可导入时才会走到。verification 的
286
+ # 判据**只有一处定义**(candidate_envelope.verification_of),这里
287
+ # 单独再试一次导入;连它都拿不到就取保守值(candidate / 未打开)——
288
+ # 判据不可得时不能凭空断言「已打开原文」。此前这里硬编码 False,
289
+ # 于是在「已抓过正文」的行上与本文件同源的 results 自相矛盾。
290
+ try:
291
+ from candidate_envelope import verification_of # type: ignore
292
+ except Exception:
293
+ def verification_of(_item): # type: ignore[misc]
294
+ return {"status": "candidate", "opened_original": False,
295
+ "checked_at": None}
296
+ candidates = [
297
+ {
298
+ "candidate_id": f"web:rank-{i+1}",
299
+ "query": query,
300
+ "title": r.get("title"),
301
+ "url": r.get("url"),
302
+ "snippet": (r.get("snippet") or "")[:300],
303
+ "backend": r.get("source") or r.get("_engine"),
304
+ "rank": i + 1,
305
+ "verification": verification_of(r),
306
+ }
307
+ for i, r in enumerate(results)
308
+ if isinstance(r, dict)
309
+ ]
310
+
311
+ envelope_path = run_dir / "envelope.json"
312
+ candidates_path = run_dir / "candidates.jsonl"
313
+ results_path = run_dir / "results.jsonl"
314
+ sources_path = run_dir / "sources.jsonl"
315
+ coverage_path = run_dir / "coverage.json"
316
+ summary_path = run_dir / "run-summary.json"
317
+ index_md_path = run_dir / "INDEX.md"
318
+
319
+ envelope_path.write_text(
320
+ dumps_pretty(public) + "\n",
321
+ encoding="utf-8",
322
+ )
323
+ with candidates_path.open("w", encoding="utf-8") as f:
324
+ for c in candidates:
325
+ f.write(json.dumps(c, ensure_ascii=False) + "\n")
326
+ with results_path.open("w", encoding="utf-8") as f:
327
+ for r in results:
328
+ f.write(json.dumps(r, ensure_ascii=False) + "\n")
329
+ with sources_path.open("w", encoding="utf-8") as f:
330
+ for s in sources:
331
+ f.write(json.dumps(s, ensure_ascii=False) + "\n")
332
+ coverage_path.write_text(
333
+ dumps_pretty(coverage) + "\n",
334
+ encoding="utf-8",
335
+ )
336
+
337
+ engines = public.get("engines_used") or public.get("engines_combo") or public.get("engines") or []
338
+ if isinstance(engines, str):
339
+ engines = [engines]
340
+
341
+ summary = {
342
+ "schema_version": SCHEMA,
343
+ "run_id": run_id,
344
+ "source": source,
345
+ "packed_at": when.isoformat(),
346
+ "query": query,
347
+ "query_original": public.get("query_original"),
348
+ "input_kind": public.get("input_kind"),
349
+ "status": public.get("status") or "completed",
350
+ "mode": public.get("mode"),
351
+ "depth": public.get("depth"),
352
+ "engine": public.get("engine"),
353
+ "engines": engines,
354
+ "domain": public.get("domain"),
355
+ "tag": tag,
356
+ "note": note,
357
+ "counts": {
358
+ "results": len(results),
359
+ "candidates": len(candidates),
360
+ "sources": len(sources),
361
+ "coverage_backends": len(coverage),
362
+ "errors": len(public.get("errors") or []),
363
+ },
364
+ "limitations": public.get("limitations") or [],
365
+ "paths": {
366
+ "run_dir": str(run_dir),
367
+ "envelope": str(envelope_path),
368
+ "candidates": str(candidates_path),
369
+ "results": str(results_path),
370
+ "sources": str(sources_path),
371
+ "coverage": str(coverage_path),
372
+ "summary": str(summary_path),
373
+ "index_md": str(index_md_path),
374
+ },
375
+ "reuse": {
376
+ "candidates_jsonl": str(candidates_path),
377
+ "sources_jsonl": str(sources_path),
378
+ "load_hint": "sources.jsonl 为编号信源;candidates.jsonl 为完整候选;snippet 非正文",
379
+ "analysis_hint": "可按 platform/backend/canonical_url 聚合;勿把 snippet 当正文事实",
380
+ },
381
+ "boundaries": {
382
+ "discovery_only": True,
383
+ "body_fetched": False,
384
+ "media_downloaded": False,
385
+ "overwrite": False,
386
+ },
387
+ }
388
+ summary_path.write_text(
389
+ dumps_pretty(summary) + "\n",
390
+ encoding="utf-8",
391
+ )
392
+
393
+ # 人读 INDEX
394
+ lines = [
395
+ f"# 搜索归档 · {run_id}",
396
+ "",
397
+ f"- **时间**: {when.isoformat()}",
398
+ f"- **查询**: {query}",
399
+ f"- **状态**: {summary['status']}",
400
+ f"- **引擎**: {public.get('engine')} / {engines}",
401
+ f"- **结果**: results={len(results)} candidates={len(candidates)}",
402
+ f"- **模式**: mode={public.get('mode')} depth={public.get('depth')} kind={public.get('input_kind')}",
403
+ ]
404
+ if tag:
405
+ lines.append(f"- **标签**: {tag}")
406
+ if note:
407
+ lines.append(f"- **备注**: {note}")
408
+ lines += ["", "## 边界", "", "- 本包仅为**发现候选**,未抓正文、未下媒体",
409
+ "- 引用前应对 candidates/sources 做 fetch/extract 核验",
410
+ "- `verification.status=candidate` 时禁止把 snippet 当事实",
411
+ "", "## 相关信源", ""]
412
+ preview = sources[:20] if sources else candidates[:15]
413
+ for i, c in enumerate(preview, 1):
414
+ if not isinstance(c, dict):
415
+ continue
416
+ ref = c.get("ref") or i
417
+ title = (c.get("title") or "")[:80]
418
+ url = c.get("url") or ""
419
+ backend = c.get("engine") or c.get("backend") or ""
420
+ lines.append(f"[{ref}] [{backend}] {title}" if backend else f"[{ref}] {title}")
421
+ if url:
422
+ lines.append(f" {url}")
423
+ extra_n = max(0, (len(sources) if sources else len(candidates)) - len(preview))
424
+ if extra_n:
425
+ lines.append(f"\n… 另有 {extra_n} 条,见 sources.jsonl / candidates.jsonl")
426
+ lims = summary.get("limitations") or []
427
+ if lims:
428
+ lines += ["", "## limitations", ""]
429
+ for lim in lims[:8]:
430
+ lines.append(f"- {lim}")
431
+ lines.append("")
432
+ index_md_path.write_text("\n".join(lines), encoding="utf-8")
433
+
434
+ # 全局 index.jsonl 追加
435
+ index_path = archive_root / "index.jsonl"
436
+ archive_root.mkdir(parents=True, exist_ok=True)
437
+ index_row = {
438
+ "schema_version": SCHEMA,
439
+ "run_id": run_id,
440
+ "packed_at": when.isoformat(),
441
+ "query": query,
442
+ "tag": tag,
443
+ "status": summary["status"],
444
+ "engine": public.get("engine"),
445
+ "counts": summary["counts"],
446
+ "run_dir": str(run_dir),
447
+ "source": source,
448
+ }
449
+ with index_path.open("a", encoding="utf-8") as f:
450
+ f.write(json.dumps(index_row, ensure_ascii=False) + "\n")
451
+
452
+ # 附回结果对象(不破坏调用方)
453
+ result["archive"] = {
454
+ "schema_version": SCHEMA,
455
+ "run_id": run_id,
456
+ "root": str(archive_root),
457
+ "run_dir": str(run_dir),
458
+ "paths": summary["paths"],
459
+ "counts": summary["counts"],
460
+ }
461
+ return result["archive"]
462
+
463
+
464
+ def list_runs(
465
+ root: Path | None = None,
466
+ *,
467
+ limit: int = 20,
468
+ tag: str | None = None,
469
+ query_substr: str | None = None,
470
+ ) -> list[dict[str, Any]]:
471
+ archive_root = root or resolve_archive_root()
472
+ index_path = archive_root / "index.jsonl"
473
+ if not index_path.is_file():
474
+ return []
475
+ rows: list[dict[str, Any]] = []
476
+ with index_path.open(encoding="utf-8") as f:
477
+ for line in f:
478
+ line = line.strip()
479
+ if not line:
480
+ continue
481
+ try:
482
+ row = json.loads(line)
483
+ except json.JSONDecodeError:
484
+ continue
485
+ if tag and row.get("tag") != tag:
486
+ continue
487
+ if query_substr and query_substr not in str(row.get("query") or ""):
488
+ continue
489
+ rows.append(row)
490
+ return rows[-limit:]
491
+
492
+
493
+ def load_run(run_dir: str | Path) -> dict[str, Any]:
494
+ p = Path(run_dir)
495
+ summary = json.loads((p / "run-summary.json").read_text(encoding="utf-8"))
496
+ envelope = json.loads((p / "envelope.json").read_text(encoding="utf-8"))
497
+ return {"summary": summary, "envelope": envelope, "run_dir": str(p.resolve())}
498
+
499
+
500
+ def main(argv: list[str] | None = None) -> int:
501
+ parser = argparse.ArgumentParser(description="Argo 搜索 run 工作区归档")
502
+ sub = parser.add_subparsers(dest="cmd", required=True)
503
+
504
+ p_write = sub.add_parser("write", help="从 stdin JSON 写入归档")
505
+ p_write.add_argument("--root", type=Path, default=None)
506
+ p_write.add_argument("--tag", default=None)
507
+ p_write.add_argument("--note", default=None)
508
+ p_write.add_argument("--source", default="argo_search")
509
+
510
+ p_list = sub.add_parser("list", help="列出近期 run")
511
+ p_list.add_argument("--root", type=Path, default=None)
512
+ p_list.add_argument("--limit", type=int, default=20)
513
+ p_list.add_argument("--tag", default=None)
514
+ p_list.add_argument("--query", default=None, help="query 子串过滤")
515
+ p_list.add_argument("--json", action="store_true")
516
+
517
+ p_show = sub.add_parser("show", help="展示某 run 摘要")
518
+ p_show.add_argument("run_dir", type=Path)
519
+ p_show.add_argument("--json", action="store_true")
520
+
521
+ p_root = sub.add_parser("root", help="打印默认归档根目录")
522
+ p_root.add_argument("--root", type=Path, default=None)
523
+
524
+ args = parser.parse_args(argv)
525
+
526
+ if args.cmd == "root":
527
+ print(resolve_archive_root(args.root))
528
+ return 0
529
+
530
+ if args.cmd == "write":
531
+ raw = sys.stdin.read()
532
+ if not raw.strip():
533
+ print("stdin 为空,需要搜索 JSON", file=sys.stderr)
534
+ return 1
535
+ data = json.loads(raw)
536
+ if not isinstance(data, dict):
537
+ print("stdin 须为 JSON object", file=sys.stderr)
538
+ return 1
539
+ meta = write_search_archive(
540
+ data,
541
+ root=args.root,
542
+ tag=args.tag,
543
+ note=args.note,
544
+ source=args.source,
545
+ )
546
+ print(dumps(meta))
547
+ return 0
548
+
549
+ if args.cmd == "list":
550
+ rows = list_runs(
551
+ args.root,
552
+ limit=args.limit,
553
+ tag=args.tag,
554
+ query_substr=args.query,
555
+ )
556
+ if args.json:
557
+ print(dumps(rows))
558
+ else:
559
+ if not rows:
560
+ print("(empty)")
561
+ return 0
562
+ for r in rows:
563
+ c = r.get("counts") or {}
564
+ print(
565
+ f"{r.get('packed_at', '?'):<25} "
566
+ f"cand={c.get('candidates', 0):>3} "
567
+ f"[{r.get('engine') or '?'}] "
568
+ f"{(r.get('query') or '')[:50]} "
569
+ f"→ {r.get('run_dir')}"
570
+ )
571
+ return 0
572
+
573
+ if args.cmd == "show":
574
+ data = load_run(args.run_dir)
575
+ if args.json:
576
+ print(dumps(data["summary"]))
577
+ else:
578
+ s = data["summary"]
579
+ print(f"run_id: {s.get('run_id')}")
580
+ print(f"query: {s.get('query')}")
581
+ print(f"counts: {s.get('counts')}")
582
+ print(f"paths: {s.get('paths', {}).get('run_dir')}")
583
+ for lim in (s.get("limitations") or [])[:5]:
584
+ print(f" ! {lim}")
585
+ return 0
586
+
587
+ return 1
588
+
589
+
590
+ if __name__ == "__main__":
591
+ raise SystemExit(main())