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,606 @@
1
+ #!/usr/bin/env python3
2
+ """search_output.py — 输出成形(信源标准化、漏斗、人读格式)。
3
+
4
+ 「结果 → 给谁看」的全部转换都在这里:Agent 档字段裁剪、sources 标准化、
5
+ 六格漏斗与塌陷归因、阶段耗时表、终端文本格式。与排序层分开的理由:输出契约
6
+ 有独立门禁(tests/test_output_contract.py),改动它不该牵动排序。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+
13
+ from search_rank import filter_results_by_domains
14
+ from typing import Any
15
+
16
+
17
+ # 本模块的 6 处日志都写在 **except 处理器**里,且分散在 shape_response 的
18
+ # 不同阶段。此前其中三处各写了一次裸 `import logging`——CPython 在**编译期**
19
+ # 就把 `logging` 判成 shape_response 的局部变量(与那行是否执行到无关),
20
+ # 于是其余三处 `logging.getLogger(...)` 全部编译成 LOAD_FAST,走到即
21
+ # UnboundLocalError;而它们位于 except 内部,处理器再抛异常会**顶替掉**原本
22
+ # 被 fail-open 吞掉的错误——「增强失败不得让搜索失败」变成整次搜索崩溃。
23
+ #
24
+ # 修法是**一处按需取 logger 的小函数**,全模块统一走它:只在真的要记日志时
25
+ # 才 import logging,模块级不留任何绑定。这样既没有局部遮蔽,也不会把
26
+ # traceback → dataclasses → inspect → _colorize(CPython 3.13+ 实测 21 ms)
27
+ # 拖进 import search 的必经之路——本模块的日志全是 debug,而全仓没有把
28
+ # `unified_search` 的 level 调离默认 WARNING,也就是说它们默认永不产生输出。
29
+ def _log(message: str) -> None:
30
+ """默认静默的调试出口;仅在真正需要记录时才引入 logging。"""
31
+ import logging
32
+ logging.getLogger("unified_search").debug(message)
33
+
34
+
35
+ def _warn(message: str) -> None:
36
+ """默认可见的告警出口:fail-open 类降级必须让用户与运维看见。
37
+
38
+ 与 _log 的分工:_log 是 debug 级(默认 WARNING 下永不输出,用于追迹);
39
+ 本函数用于「门控跳过了但调用方必须知道」的路径——engine_dispatch:377
40
+ 早就为同款「静默回退让慢源修复失明」付过代价。同样模块级不留 logging
41
+ 绑定(CPython 编译期局部名陷阱见 search_entry._log 的 docstring)。
42
+ """
43
+ import logging
44
+ logging.getLogger("unified_search").warning(message)
45
+
46
+
47
+ _AGENT_RESULT_FIELDS = (
48
+ "title", "url", "snippet", "source", "score", "ref",
49
+ "published_at", "fetch_suggested", "full_text_url",
50
+ # 离散权威等级(0-4,≥3 极权威):Agent 的廉价可信闸门——连续 authority
51
+ # 分不知道该在哪切,等级才能直接当门用(DeepSeek Search 同款语义)。
52
+ # 整数字段约 6 字节,剥掉它等于把「先信谁」重新推回给调用方猜。
53
+ "authority_level",
54
+ # Agent 档摘要(600-800 字,核验过的结果才有):snippet 与全文之间的
55
+ # 中间档,借鉴 DeepSeek Search 的 summary 语义。只在确有核验证据时
56
+ # 出现(backfill_results 回填),未核验结果不造假摘要。
57
+ "summary",
58
+ "image_url", "image_license",
59
+ "episode_count", "duration_minutes",
60
+ # 本地已有正文:是可操作提示而非观测数据——告诉 Agent 这条不必重新联网,
61
+ # 以及全文在哪(被截断时还给出 full_text_path)。剥掉它等于把「随时
62
+ # 核对原文」这条路径从 Agent 视野里藏起来,而它正是 Agent 最需要的。
63
+ # 只在确有本地正文时出现,单条约 60 字节。
64
+ "local_body",
65
+ # 取数可用性:取不到(系统类)/ 取到了但没用(内容类)。与 local_body 同源,
66
+ # 都是可操作提示。剥掉等于让调用方抓一次才知道结果,正是它要避免的事。
67
+ "retrieval", "fetch_blocked",
68
+ # 可验证出处:技能目录源给的是市场页,upstream 才是能核对/能安装的地址
69
+ # (上游仓目录或 owner/slug 安装引用)。剥掉等于把「搜到的这条到底是什么」
70
+ # 重新变成一次额外的浏览器往返。
71
+ "upstream",
72
+ )
73
+
74
+
75
+ def _strip_for_agent(payload: dict[str, Any]) -> dict[str, Any]:
76
+ """--fields agent:输出只留答案内容(P2-2,2026-09-13)。
77
+
78
+ 在默认精简档之上再剥观测标量(tfidf_scores/lang_pref/engine_outcomes 等)
79
+ 与 null/空键。fetch_required 必须保留——SKILL.md 的高后果门控纪律依赖它,
80
+ 不能被瘦身掉;funnel 同理保留,它是 agent 判「0 结果卡在哪一层」的唯一依据。
81
+ """
82
+ keep_top = (
83
+ "query", "engine", "engines", "engines_used", "domain", "count",
84
+ "mode", "depth", "status", "fetch_required", "evidence_loop",
85
+ "errors", "login_hint",
86
+ # 阶段漏斗账:约 70 字节,换来「这次为什么只有这么几条」的可归因性。
87
+ # 它是 agent 档里唯一能回答「0 结果卡在哪一层」的东西,不剥。
88
+ "funnel",
89
+ # 质量信号:局限声明与告警必须随答案一起到达。此前 agent 档把
90
+ # limitations/recovery/time_filter_warning 一并剥掉,agent 无从判断
91
+ # 「这批结果能用到什么程度」(2026-09-15 输出契约审查)。
92
+ "limitations", "recovery", "time_filter_warning",
93
+ # 阶段耗时:默认就带(--no-timing 才没有)。剥掉会让使用者看不到
94
+ # 「这次慢在哪」,也拿不到自己动手优化所需的依据。
95
+ "timing",
96
+ )
97
+ out: dict[str, Any] = {k: payload[k] for k in keep_top
98
+ if payload.get(k) is not None}
99
+ slim_results = []
100
+ for r in payload.get("results") or []:
101
+ if not isinstance(r, dict):
102
+ continue
103
+ slim = {k: r[k] for k in _AGENT_RESULT_FIELDS if r.get(k) is not None}
104
+ slim_results.append(slim)
105
+ out["results"] = slim_results
106
+ out["count"] = len(slim_results)
107
+ return out
108
+
109
+
110
+ def build_funnel(routed: int, called: int, returned: int,
111
+ deduped: int, filtered: int, kept: int) -> dict[str, int]:
112
+ """阶段漏斗账:一次调用在管线每一层还剩多少条。
113
+
114
+ why(GLM 密集反馈那篇的核心):端到端指标只能说明「变差了」,说明不了
115
+ **在哪一层**变差。同一句「0 结果」背后至少有四种病——路由没选到能答的引擎、
116
+ 引擎返回了但去重削没了、否定词/时间窗过滤压到 0、精排阶段全被剔掉。
117
+ 这些数字此前散在 minhash_removed / excluded_count / time_filtered / count
118
+ 里,读者得自己拿管线知识把它们按顺序拼起来,拼错就会误判。
119
+
120
+ 六格按管线顺序(routed → called → returned → deduped → filtered → kept),
121
+ 相邻两格的差值就是该层的损耗,「哪一格塌了」一眼可见。
122
+
123
+ routed = 路由选出的引擎数(engines_combo)
124
+ called = 实际发起调用的引擎数(早停时小于 routed)
125
+ returned = 引擎返回的原始条数(跨引擎去重前)
126
+ deduped = 跨引擎合并 + 近重复去重后(SERP/跳转页过滤的损耗已计入本格
127
+ 之差,取值点在 SERP 过滤之前、minhash 之后;此前取值点在
128
+ minhash 之后,把 SERP 丢弃也算成「去重削掉了」,会让 0 结果
129
+ 的首要诊断入口给出错误归因)
130
+ filtered = 否定词过滤 + 时间窗过滤后
131
+ kept = 最终输出条数
132
+ """
133
+ return {"routed": routed, "called": called, "returned": returned,
134
+ "deduped": deduped, "filtered": filtered, "kept": kept}
135
+
136
+
137
+ FUNNEL_STAGES = ("routed", "called", "returned", "deduped", "filtered", "kept")
138
+
139
+
140
+ def describe_funnel(funnel: dict[str, Any] | None) -> str:
141
+ """把漏斗压成一行 `routed 2→called 2→returned 0→…`(给局限声明用)。"""
142
+ if not isinstance(funnel, dict):
143
+ return ""
144
+ return "→".join(f"{k} {funnel.get(k)}" for k in FUNNEL_STAGES
145
+ if k in funnel)
146
+
147
+
148
+ def funnel_collapse(funnel: dict[str, Any] | None) -> str | None:
149
+ """返回漏斗里第一个被打到 0 的层名;没有 0 就返回 None。
150
+
151
+ 这是「0 结果」的归因答案:结果在 `returned` 归零 = 引擎没抓到;
152
+ 在 `deduped` 归零 = 抓到了但被当重复削掉;在 `kept` 归零 = 被过滤/截断压没。
153
+ 三者的处置完全不同,混成一句「没有结果」就没法据此行动。
154
+ """
155
+ if not isinstance(funnel, dict):
156
+ return None
157
+ for name in FUNNEL_STAGES:
158
+ if funnel.get(name) == 0:
159
+ return name
160
+ return None
161
+
162
+
163
+ def _slow_query_ttl(base_ttl: int, elapsed_ms: int) -> int:
164
+ """慢查询的缓存 TTL:按耗时延长,上限一律 2× base。
165
+
166
+ 慢查询值得多缓存一会儿——省的是「同一查询再付一次慢网」的钱。但上限
167
+ 必须存在:此前这条逻辑写成 if/else 两支,`base_ttl > 900` 的 else 支
168
+ **没有上限**,而 multiplier 最大 8。实测后果:evergreen 档
169
+ (image_search / geo_places / book_search,base=86400s)的慢查询拿到
170
+ 691200s=**8 天** TTL,慢查询结果以「新鲜」的样子交付一整周;注释一直
171
+ 写的是「时效域最多 2×」,代码只有一半兑现。
172
+
173
+ 顺带修掉的冗余:原 ≤900 支写 `min(b * min(m, 2), b * 2)`,而它对任意
174
+ b、m 恒等于 `b * min(m, 2)`(外层 min 永远取不到第二个参数),两支本就
175
+ 等价。合并后 TTL 延长规则只剩这一个定义点,也可被测试直接打到。
176
+
177
+ 单独成函数是为了让回归测试调用**真实实现**——把公式抄进测试的写法,
178
+ 实现回退时测试照样绿,等于没锁。
179
+ """
180
+ multiplier = min(2 ** (max(0, elapsed_ms) // 2000), 8)
181
+ return base_ttl * min(multiplier, 2)
182
+
183
+
184
+ def build_sources(results: list[Any] | None) -> list[dict[str, Any]]:
185
+ """将 results 投影为编号信源列表(传统搜索引擎底部「相关链接」形态)。
186
+
187
+ 规则:
188
+ - ref 与列表序号一致,从 1 起
189
+ - 无 URL 的条目跳过(不占号?——保留占位会错位;跳过并重编号)
190
+ - 只保留 ref/url/engine 三个字段,title/snippet/score 不重复——
191
+ 消费方通过 ref 回查 results[ref-1] 获取完整数据(2026-10-05 A2:
192
+ 引用式视图,默认档体积 -38%,消除三份副本漂移的风险)
193
+ """
194
+ sources: list[dict[str, Any]] = []
195
+ ref = 0
196
+ for r in results or []:
197
+ if not isinstance(r, dict):
198
+ continue
199
+ url = (r.get("url") or "").strip()
200
+ if not url:
201
+ continue
202
+ ref += 1
203
+ sources.append({
204
+ "ref": ref,
205
+ "url": url,
206
+ "engine": r.get("source") or r.get("_engine") or r.get("engine"),
207
+ })
208
+ return sources
209
+
210
+
211
+ def format_timing(t: dict[str, Any]) -> str:
212
+ """人读格式的阶段耗时表(`--explain-timing`,非 JSON 分支)。"""
213
+ lines = ["", "=== 阶段耗时 ==="]
214
+ for row in t.get("stages") or []:
215
+ lines.append(f" {row['stage']:<14}{row['ms']:>9.1f} ms {row['pct']:>5.1f}%")
216
+ d = t.get("dispatch") or {}
217
+ if d:
218
+ lines.append(
219
+ f" 引擎并发 墙钟 {d.get('wall_ms')} ms / 引擎合计 "
220
+ f"{d.get('engine_sum_ms')} ms → 并发效率 "
221
+ f"{d.get('parallel_efficiency')}(跑了 {d.get('engines_run')} 个,"
222
+ f"早停={d.get('early_stopped')},浪费 {d.get('wasted_ms')} ms)")
223
+ if "overhead_ms" in t:
224
+ lines.append(
225
+ f" 固定开销 {t['overhead_ms']} ms"
226
+ f"(其中 import {t.get('import_ms')} ms;不含解释器自身启动)")
227
+ lines.append(f" 进程总计 {t.get('process_ms')} ms")
228
+ return "\n".join(lines)
229
+
230
+
231
+ def format_text_output(results: dict[str, Any]) -> str:
232
+ """日常搜索人读格式:条目正文 + 底部「相关信源」链接(类传统 SERP)。"""
233
+ lines = []
234
+ if results.get("status") == "handoff_required":
235
+ ho = results.get("handoff") or {}
236
+ lines.append("=== HANDOFF (known-url, search skipped) ===")
237
+ lines.append(f" url: {ho.get('url')}")
238
+ lines.append(f" suggest: {', '.join(ho.get('suggested_tools') or [])}")
239
+ for lim in (results.get("limitations") or [])[:4]:
240
+ lines.append(f" ! {lim}")
241
+ return "\n".join(lines)
242
+ if results.get("status") in ("ready",) and results.get("steps") and not results.get("results"):
243
+ # plan-only
244
+ lines.append(f"=== PLAN {results.get('status')} kind={results.get('input_kind')} ===")
245
+ route = results.get("route") or {}
246
+ lines.append(f" engine={route.get('backend')} domain={route.get('domain')} combo={route.get('engines_combo')}")
247
+ for lim in (results.get("limitations") or [])[:5]:
248
+ lines.append(f" ! {lim}")
249
+ return "\n".join(lines)
250
+
251
+ count = results.get("count", 0)
252
+ elapsed = results.get("elapsed_ms", 0)
253
+ engine = results.get("engine", "?")
254
+ cached = results.get("cached", False)
255
+ cache_level = results.get("cache_level", "")
256
+ domain = results.get("domain", "")
257
+ mode = results.get("mode", "auto")
258
+
259
+ header = f"=== {count} results ({elapsed}ms via {engine})"
260
+ if cached:
261
+ header += f" [CACHE {cache_level}]"
262
+ elif domain:
263
+ header += f" [domain:{domain}]"
264
+ if mode != "auto":
265
+ header += f" [mode:{mode}]"
266
+ if results.get("input_kind"):
267
+ header += f" [kind:{results.get('input_kind')}]"
268
+ lines.append(header)
269
+
270
+ for err in results.get("errors", [])[:3]:
271
+ lines.append(f" [ERROR] {err}")
272
+
273
+ # 正文区:编号 + 标题 + 摘要(链接沉底,避免噪声)
274
+ sources = results.get("sources")
275
+ if not isinstance(sources, list) or not sources:
276
+ sources = build_sources(results.get("results") or [])
277
+
278
+ # 用 URL 保持一致 ref
279
+ url_to_ref = {s.get("url"): s.get("ref") for s in sources if isinstance(s, dict)}
280
+ body_items = [r for r in (results.get("results") or []) if isinstance(r, dict)]
281
+ for r in body_items:
282
+ url = (r.get("url") or "").strip()
283
+ ref = url_to_ref.get(url)
284
+ if ref is None:
285
+ # 未进 sources 时临时编号。分两种:有 URL 但没被收进 sources
286
+ # (编号未知,用 ?);以及**本就没有 URL** 的条目——天气/行情/
287
+ # 宏观这类结构化快照走的就是这条。此前两种情况共用一个分支,
288
+ # 且分支条件是 `and url`,于是无 URL 的条目 ref 保持 None,
289
+ # 正文里直接打成 `[None]`(2026-09-19 实测「北京天气」复现)。
290
+ ref = "?" if url else "—"
291
+ score = r.get("score", 0)
292
+ title = (r.get("title") or "?")[:80]
293
+ score_s = f"{score:.2f}" if isinstance(score, (int, float)) and score else "—"
294
+ lines.append(f" [{ref}] {title}")
295
+ snippet = (r.get("snippet") or "").strip()
296
+ if snippet:
297
+ lines.append(f" {snippet[:140]}")
298
+ elif score:
299
+ lines.append(f" (score={score_s})")
300
+
301
+ # 底部相关信源(传统搜索引擎形态)
302
+ # sources 只保留 ref/url/engine(A2 引用式),title 从 results 回查
303
+ if sources:
304
+ lines.append("")
305
+ lines.append("── 相关信源 ──")
306
+ _results_list = results.get("results") or []
307
+ for s in sources:
308
+ if not isinstance(s, dict):
309
+ continue
310
+ ref = s.get("ref", "?")
311
+ eng = s.get("engine") or ""
312
+ url = s.get("url") or ""
313
+ eng_s = f" · {eng}" if eng else ""
314
+ # 通过 ref 回查 results 获取 title(A2:sources 不再重复 title)
315
+ _title = ""
316
+ if isinstance(ref, int) and 1 <= ref <= len(_results_list):
317
+ _title = (_results_list[ref - 1].get("title") or "")[:60]
318
+ if _title:
319
+ lines.append(f" [{ref}] {_title}{eng_s}")
320
+ if url:
321
+ lines.append(f" {url}")
322
+ elif url:
323
+ lines.append(f" [{ref}] {url}{eng_s}")
324
+
325
+ if results.get("limitations"):
326
+ lines.append("")
327
+ lines.append("── limitations ──")
328
+ for lim in results["limitations"][:4]:
329
+ lines.append(f" ! {lim}")
330
+
331
+ return "\n".join(lines)
332
+
333
+
334
+ # 不算失败的 outcome 状态:这些情况「引擎跑了、没问题」,不该出现在 errors[]
335
+ # cancelled = 早停收工时被主动弃置(结果已够,引擎没失败也没超窗)
336
+ _NON_ERROR_OUTCOME = frozenset({
337
+ "ok", "ok-cached", "partial", "no-results", "no-results-cached",
338
+ "cancelled",
339
+ # 配额耗尽的跳过是**持续状态**,不是本次查询的失败:标记默认持续到周期
340
+ # 边界(一天),若每个查询都把它塞进 errors,调用方会看到一条持续一天的
341
+ # 「错误」并误判本次检索降级。状态本身在 --list-engines 可见。
342
+ "skipped-quota-exhausted",
343
+ })
344
+
345
+
346
+ def _collect_errors(raw_results: dict[str, list[dict[str, Any]]],
347
+ engine_outcomes: list[dict[str, Any]] | None = None
348
+ ) -> list[str]:
349
+ """收集失败文本,两个来源缺一不可。
350
+
351
+ 1. raw_results 里的 error 条目——引擎把失败**当成结果**返回(异常被
352
+ `_exec_engine` 捕获后塞进列表)。
353
+ 2. engine_outcomes 里带 detail 的失败 outcome——引擎内部**吞掉**异常,
354
+ 只把失败原因写进记录(engines_base.note_failure),列表是空的。
355
+
356
+ 只收第 1 类时,`--engine you` 的 SSL 超时会上报成
357
+ `status=completed, count=0, errors=[]`:调用方(Agent)据此判定「网上
358
+ 没有这个信息」并停止追问,而真相是引擎连不上(2026-09-15 实测)。
359
+ 去重按整行,避免同一失败既来自 error 条目又来自 outcome detail。
360
+ """
361
+ errors: list[str] = []
362
+ seen: set[str] = set()
363
+
364
+ def _add(line: str) -> None:
365
+ if line and line not in seen:
366
+ seen.add(line)
367
+ errors.append(line)
368
+
369
+ for eng, res in raw_results.items():
370
+ for r in res:
371
+ if isinstance(r, dict) and "error" in r:
372
+ _add(f"{eng}: {r['error']}")
373
+ for o in (engine_outcomes or []):
374
+ if not isinstance(o, dict):
375
+ continue
376
+ detail = str(o.get("detail") or "").strip()
377
+ if not detail or str(o.get("status") or "") in _NON_ERROR_OUTCOME:
378
+ continue
379
+ _add(f"{o.get('engine')}: {detail}")
380
+ return errors
381
+
382
+ @dataclass(frozen=True)
383
+ class _ShapeContext:
384
+ """输出成形的全部选项(请求侧只读 + 一个本地检索钩子)。"""
385
+
386
+ query: str
387
+ kind: str
388
+ tier: str
389
+ envelope: bool
390
+ decision: dict[str, Any]
391
+ extra_lim: list[str]
392
+ cache: Any
393
+ include_domains: list[str]
394
+ exclude_domains: list[str]
395
+ include_local: bool
396
+ n: int
397
+ run_local_seek: Any
398
+
399
+
400
+ def shape_response(ctx: _ShapeContext, result: dict[str, Any]) -> dict[str, Any]:
401
+ """把执行层载荷整理成对外响应(就地改 result 并返回它)。
402
+
403
+ 七个阶段,顺序即契约:档位局限 → envelope/局限声明 → 本地正文索引 →
404
+ 证据门控(fetch_required / evidence_loop)→ 域过滤 → 信源标准化 → 本地命中并入。
405
+ 每一步都 fail-open(增强失败不得让一次搜索失败),因此每段自带 try。
406
+
407
+ 住在 search_output 而不是 search.py:这是**响应契约**(字段形态、什么时候
408
+ 生成 sources、envelope 与精简档的差异),与执行/调度无关。
409
+ """
410
+ query = ctx.query
411
+ kind, tier = ctx.kind, ctx.tier
412
+ envelope = ctx.envelope
413
+ decision = ctx.decision
414
+ extra_lim = ctx.extra_lim
415
+ cache = ctx.cache
416
+ include_domains, exclude_domains = ctx.include_domains, ctx.exclude_domains
417
+ include_local, n = ctx.include_local, ctx.n
418
+ _run_local_seek = ctx.run_local_seek
419
+ if tier == "daily":
420
+ extra_lim.append(
421
+ "daily tier: direct search; no pre-confirm gate"
422
+ )
423
+ elif tier == "professional":
424
+ extra_lim.append(
425
+ "professional tier: plan metadata attached; verify top-k before hard claims"
426
+ )
427
+
428
+ # 候选交接包(附加字段,不改 results 排序)
429
+ if envelope:
430
+ try:
431
+ from candidate_envelope import attach_envelope
432
+ attach_envelope(
433
+ result,
434
+ query=query,
435
+ input_kind=kind,
436
+ route_reason=decision.get("reason"),
437
+ extra_limitations=extra_lim,
438
+ )
439
+ except Exception as e:
440
+ _log(f"envelope 跳过: {type(e).__name__}")
441
+ result.setdefault("schema_version", "1.0")
442
+ result.setdefault("limitations", [])
443
+ else:
444
+ # 精简档:attach_envelope 不跑,局限声明仍须上报(同一个 build_limitations
445
+ # 实现,避免两处各写一份导致计算方式漂移)
446
+ try:
447
+ from candidate_envelope import build_limitations
448
+ result["limitations"] = build_limitations(result, extra_lim)
449
+ except Exception:
450
+ result.setdefault("limitations", list(extra_lim))
451
+
452
+ # 本地正文索引:给每条结果标出「这篇的正文我已经取回过」,并给出全文位置。
453
+ # 结果原本只有 300 字摘要与核验分,调用方看不出本地已有正文——想核对原文
454
+ # 只能重新 fetch,或者压根不知道能回看。这里是纯本地查询(冷 L1 下 20 条
455
+ # 约 1.7 ms),不联网、不额外请求,故无条件附加。
456
+ #
457
+ # `_local` 在 try 外初始化:cache.local_status() 抛异常(SQLite 锁/库损坏)
458
+ # 时下面 :469 的 blocked 筛选仍要读它——try 内首次赋值会让那条路径吃
459
+ # NameError,被外层 except 吞掉后「不可取源不再建议核验」这段增强静默
460
+ # 永不执行,日志还把原因误报成「不可取源筛选跳过: NameError」。
461
+ _local: dict[str, Any] = {}
462
+ try:
463
+ _urls = [r.get("url") for r in (result.get("results") or [])
464
+ if isinstance(r, dict) and r.get("url")]
465
+ _local = cache.local_status(_urls) if _urls else {}
466
+ for _r in (result.get("results") or []):
467
+ if not isinstance(_r, dict):
468
+ continue
469
+ _st = _local.get(_r.get("url") or "")
470
+ if not _st:
471
+ continue
472
+ if _st.get("body"):
473
+ _r["local_body"] = _st["body"]
474
+ if _st.get("retrieval"):
475
+ # 取数可用性:把「取不到」(系统类)与「取到了但没用」(内容类)
476
+ # 分开报,两者都不等于「还没试过」。搜索阶段就能判定的事,不该
477
+ # 留给调用方抓一次才知道。
478
+ _r["retrieval"] = _st["retrieval"]
479
+ except Exception as _e:
480
+ _log(f"本地正文索引跳过: {type(_e).__name__}")
481
+
482
+ # 证据完整链路 P0:回填已核验证据分 + 高后果门控(finance/health/legal)
483
+ # 输出 fetch_required / evidence_loop 汇总,每条结果带 fetch_suggested
484
+ # 与 has_fetched_evidence / post_fetch_absorption(若此前 fetch 过)。
485
+ try:
486
+ from evidence_loop import gate_results
487
+ # cache 必须传下去:gate_results → backfill_results → lookup_fetch_evidence
488
+ # 对每条结果 URL 查证据缓存。不传时每条结果各新建一个 SearchCache——
489
+ # 每次 _init_db(connect + 3 PRAGMA + executescript + 2×table_info)
490
+ # 再加一次整页 fetch 条目的 gunzip+json.loads,只为取其中一个子键。
491
+ # 默认 n=5 即 5–20 ms/次搜索纯浪费(缓存命中的整次搜索只要 ~26 ms)。
492
+ # ctx.cache 就在同函数上方 :431 刚用过,递一下参数即可。
493
+ gate = gate_results(result.get("results") or [], result.get("domain"),
494
+ cache=cache)
495
+ result["fetch_required"] = gate["fetch_required"]
496
+ result["evidence_loop"] = {
497
+ "high_consequence_domain": gate["high_consequence_domain"],
498
+ "suggested": gate["suggested"],
499
+ "verified_count": gate["verified_count"],
500
+ "pending_count": gate["pending_count"],
501
+ }
502
+ # 已知取不到的源不再「建议核验」——建议了也只会白跑一趟。
503
+ # 必须放在 gate_results 之后:它才是 fetch_suggested 的产出方。
504
+ # 不改「该不该核验」的判断,只去掉注定徒劳的那部分,并把原因写清楚,
505
+ # 否则调用方会把「未建议」误读成「不必核验」。
506
+ try:
507
+ _blocked = []
508
+ _keep = []
509
+ for _u in (result["evidence_loop"].get("suggested") or []):
510
+ _hit = _local.get(_u) or {}
511
+ if (_hit.get("retrieval") or {}).get("status") == "blocked":
512
+ _blocked.append(_u)
513
+ else:
514
+ _keep.append(_u)
515
+ if _blocked:
516
+ result["evidence_loop"]["suggested"] = _keep
517
+ result["evidence_loop"]["unretrievable"] = _blocked
518
+ result["evidence_loop"]["pending_count"] = max(
519
+ 0, int(result["evidence_loop"].get("pending_count") or 0)
520
+ - len(_blocked))
521
+ for _r in (result.get("results") or []):
522
+ if isinstance(_r, dict) and _r.get("url") in _blocked:
523
+ _r["fetch_suggested"] = False
524
+ _r["fetch_blocked"] = (_local[_r["url"]]
525
+ .get("retrieval") or {}).get("reason")
526
+ except Exception as e:
527
+ _log(f"不可取源筛选跳过: {type(e).__name__}")
528
+ except Exception as e:
529
+ # fail-open 必须可见:被跳过的是高后果域的 fetch_required/evidence_loop
530
+ # (金融/医疗/法律的「先核验再下结论」提示)。_log 是 debug 级——全仓
531
+ # 从未把 unified_search 调离默认 WARNING,即默认永不输出;而
532
+ # engine_dispatch:377 早就为同款「静默回退让修复失明」付过代价。
533
+ # 这里双通道留痕:warning 日志 + limitations(随答案到达调用方)。
534
+ _warn(f"证据门控跳过: {type(e).__name__}"
535
+ "(fetch_required/evidence_loop 本次不可用)")
536
+ try:
537
+ result.setdefault("limitations", []).append(
538
+ f"证据门控本次跳过({type(e).__name__}):fetch_required/"
539
+ "evidence_loop 不可用,高后果域结论请自行核验")
540
+ except Exception:
541
+ pass
542
+
543
+ # 域过滤(后置,引擎无关):融合排序之后裁剪,sources 与 results 保持一致。
544
+ # 裁剪导致不足 n 条是调用方过滤条件的诚实结果,不回填。
545
+ if include_domains or exclude_domains:
546
+ try:
547
+ kept, note = filter_results_by_domains(
548
+ result.get("results"), include_domains, exclude_domains)
549
+ result["results"] = kept
550
+ if note:
551
+ result["domain_filter"] = note
552
+ except Exception as e:
553
+ _log(f"[domain-filter] {type(e).__name__}: {e}")
554
+
555
+ # 相关信源标准化(日常搜索底部引用列表;与 results 顺序一致)。
556
+ # sources 是 results 的降级投影(URL 100% 重叠,实测零信息增量),
557
+ # --no-envelope(Agent 默认输出)下不再生成:每次调用省 ~0.9KB,
558
+ # 要 provenance 时用 envelope 模式或 --archive(2026-09-13 审查 P2-1)。
559
+ if envelope:
560
+ result["sources"] = build_sources(result.get("results") or [])
561
+
562
+ # 本地命中并入:seek 结果尾部拼入,来源 local_files(score 0.9 精确命中 /
563
+ # 0.7 扩展召回,与 MCP argo_local_search 同口径)。触发:fast/budget 自动
564
+ #(宽根守卫 + 3s 超时兜底),--include-local / MCP include_local 显式开,
565
+ # --no-local / include_local=false 关。按 URL 去重后并入,防止与既有
566
+ # 结果(或未来评分变化)产生双份条目。
567
+ if include_local:
568
+ try:
569
+ local_hits = _run_local_seek(query, n)
570
+ except Exception as e:
571
+ local_hits = []
572
+ _log(f"[include-local] {type(e).__name__}: {e}")
573
+ if local_hits:
574
+ _seen_urls = {r.get("url") for r in (result.get("results") or [])
575
+ if isinstance(r, dict)}
576
+ _fresh_local = [h for h in local_hits
577
+ if isinstance(h, dict) and h.get("url") not in _seen_urls]
578
+ if _fresh_local:
579
+ result.setdefault("results", []).extend(_fresh_local)
580
+ result["local_results"] = _fresh_local
581
+ result["include_local"] = True
582
+ # ── 收口:重算一切「由 results 派生」的字段 ────────────────────────────────
583
+ #
584
+ # why:本函数**后置**改 results 的地方不止一处(域过滤裁剪、本地命中并入),
585
+ # 而 count / evidence_loop 的更早版本是按改之前的 results 算的。实测
586
+ # `--include-domains example.com` 得到 `count=4` 而 `results=[]`——JSON 消费
587
+ # 者会以为拿到了 4 条答案,而下游按 count 循环就会取到空气。
588
+ #
589
+ # 这类「改了 results、忘了改派生字段」的缺陷根治不了,只能靠位置根治:把
590
+ # 重算放在**唯一出口**,任何后置改写都自动被覆盖,新增后置阶段也不必记得
591
+ # 来这里补一行。判定以 results 为唯一真值来源,而不是把旧值加减修正——
592
+ # 后者在多次改写叠加时必然漂移。
593
+ result["count"] = len(result.get("results") or [])
594
+ _el = result.get("evidence_loop")
595
+ if isinstance(_el, dict):
596
+ _live = {r.get("url") for r in (result.get("results") or [])
597
+ if isinstance(r, dict)}
598
+ _sug = [u for u in (_el.get("suggested") or []) if u in _live]
599
+ if len(_sug) != len(_el.get("suggested") or []):
600
+ # suggested 是「建议核验的 URL」,被域过滤裁掉的 URL 不该再出现在
601
+ # 这里;pending_count 的语义就是 len(suggested)(见 evidence_loop.
602
+ # gate_results 的返回),两者必须同步,否则会出现「建议 4 条、
603
+ # 待核验 0 条」这类自相矛盾的口径。
604
+ _el["suggested"] = _sug
605
+ _el["pending_count"] = len(_sug)
606
+ return result