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,592 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ evidence_loop.py — 证据完整链路(P0):fetch 后正文吸收分回写 + URL→证据分缓存 + 高后果门控
4
+
5
+ 问题重定义(第一性):
6
+ Argo 搜索输出的 snippet 级证据分(credibility_fast)是「候选」分,
7
+ Agent 高后果下结论前必须 fetch 正文复核。此前 fetch 结果不回填搜索结果、
8
+ 不缓存 URL→证据分、无「该核验哪些」的可编程信号,证据完整链路是开环。
9
+
10
+ MECE 分工(互不重叠):
11
+ A. fetch 证据提取 :从 fetch_v3 结果提取正文级吸收分(extract_fetch_evidence)
12
+ B. URL 证据缓存 :URL → 正文级证据分,独立于 fetch 正文缓存(get/set_evidence)
13
+ C. 回填 :搜索结果若已有证据缓存则回填 post_fetch_absorption(backfill_results)
14
+ D. 高后果门控 :finance/health/legal 等域标记 fetch_required + fetch_suggested(gate_results)
15
+ E. 核验模式 :显式对 top-k 未核验结果 fetch 并产出 evidence_revision 分布(verify_results)
16
+
17
+ 完整链路:search 输出(建议核验) → Agent fetch → fetch_v3 写证据缓存 → 下次 search 自动回填。
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from typing import Any, Callable, Optional
23
+
24
+
25
+ def _log(message: str) -> None:
26
+ """默认静默的调试出口;仅在真正需要记录时才引入 logging。
27
+
28
+ 与 search_output._log 同一范式。本模块曾被 search_output 在**每次搜索的
29
+ 输出阶段**导入(shape_response → gate_results),模块级 `import logging`
30
+ 会把 logging→traceback→dataclasses→inspect→_colorize(实测 21 ms)拖进
31
+ 那条路;而全仓没有把 `unified_search` 的 level 调离默认 WARNING,这些
32
+ debug 默认永不产生输出——纯粹是花钱买一个不说话的 logger。
33
+ """
34
+ import logging
35
+ logging.getLogger("unified_search.evidence_loop").debug(message)
36
+
37
+ # ── 高后果域(finance / health / legal / 事实与安全)──────────────────────────
38
+ # 命中这些域时,Agent 在把搜索结果当答案前必须先核验正文。
39
+ HIGH_CONSEQUENCE_DOMAINS: frozenset[str] = frozenset({
40
+ # 金融
41
+ "stock_query", "us_stock", "fund_query", "financial_news", "macro_data",
42
+ "crypto_search", "jin10_flash", "cls_telegraph_search", "company_search",
43
+ # 健康
44
+ "medical", "chem_search",
45
+ # 法律
46
+ "legal", "us_legal", "wenshu_query",
47
+ # 事实核查与安全关键
48
+ "fact_check", "aviation_weather",
49
+ })
50
+
51
+ # 证据缓存 TTL:正文级证据分是稳定派生数据,但需跟随正文更新,默认与 fetch 一致
52
+ EVIDENCE_DEFAULT_TTL = 3600
53
+ EVIDENCE_DOC_TTL = 86400 # docs/reference 长 TTL(正文稳定)
54
+ EVIDENCE_NEWS_TTL = 600 # news/realtime 短 TTL(正文常更新)
55
+
56
+
57
+ def is_high_consequence_domain(domain: str | None) -> bool:
58
+ """域是否高后果(finance/health/legal/事实安全)。"""
59
+ return bool(domain and domain in HIGH_CONSEQUENCE_DOMAINS)
60
+
61
+
62
+ # ── A. fetch 证据提取 ──────────────────────────────────────────────────────────
63
+
64
+ # ── A0. Agent 档摘要(600-800 字) ──────────────────────────────────────────
65
+ #
66
+ # 借鉴 DeepSeek Search 的 summary 语义(600-800 字「给模型阅读」,与 100-200
67
+ # 字给人看的 snippet 分档)。Argo 的吸收断层在这:snippet 太短不够推理,
68
+ # fetch 全文太贵——中间缺一档。
69
+ #
70
+ # 做法是截断+句界对齐,不是句子压缩:本地无 LLM 的前提下,压缩要么引入
71
+ # 外接服务要么引入幻觉,而「readability 正文的前 800 字、切在句界上」是
72
+ # 可解释、零成本、不撒谎的做法。真正的摘要合成是 Agent 的活——Argo 的
73
+ # 设计契约:只产证据,判断归 Agent。
74
+
75
+ _SENT_END = ("。", "!", "?", ";", ".", "!", "?", ";", "\n")
76
+
77
+
78
+ def summarize_for_agent(content: str, max_chars: int = 800) -> str:
79
+ """正文 → ≤max_chars 的 Agent 档摘要;空正文/过短原样返回。"""
80
+ if not content or not content.strip():
81
+ return ""
82
+ text = content.strip()
83
+ if len(text) <= max_chars:
84
+ return text
85
+ window = text[:max_chars]
86
+ # 取窗内最靠后的任一句界(中英文句号/叹问/分号/换行)——靠后=预算内
87
+ # 最大化。30% 利用率下限:界太靠前(如前 3 字就句号)时效利用率太低,
88
+ # 不如硬切。窗内完全没有句界才硬切——半句话比硬切更伤 Agent 理解。
89
+ best = -1
90
+ for mark in _SENT_END:
91
+ idx = window.rfind(mark)
92
+ if idx > best:
93
+ best = idx
94
+ if best >= max_chars * 0.3:
95
+ return window[: best + 1].strip()
96
+ return window.strip()
97
+
98
+
99
+ def extract_fetch_evidence(fetch_result: dict[str, Any]) -> Optional[dict[str, Any]]:
100
+ """从 fetch_v3 结果提取正文级证据分。
101
+
102
+ 返回 None 表示抓取失败或无正文,不产生证据记录(不污染缓存)。
103
+ """
104
+ if not fetch_result:
105
+ return None
106
+ if not fetch_result.get("success"):
107
+ return None
108
+ content = fetch_result.get("content") or ""
109
+ if not content.strip():
110
+ return None
111
+ title = fetch_result.get("title") or ""
112
+ url = fetch_result.get("url") or ""
113
+ if not url:
114
+ return None
115
+
116
+ evidence: dict[str, Any] = {}
117
+ seo_signals: dict[str, Any] = {}
118
+ try:
119
+ from content_signals import (compute_content_quality, score_clickbait,
120
+ score_title_body_consistency,
121
+ score_template_repetition)
122
+ qual = compute_content_quality(content, title)
123
+ evidence = dict(qual)
124
+ # SEO/低质信号(2026-09-27 新增):三个信号与 absorption 互补——
125
+ # absorption 量「有没有可抽取的证据块」,这三个量「这些内容是不是
126
+ # 为了被检索而制造的」。标题党/文不对题/模板化都会拉低吸收分,
127
+ # 因为它们是「看起来有内容、实际不可用」的典型形态。
128
+ seo_signals = {
129
+ "clickbait": score_clickbait(title),
130
+ "title_body": score_title_body_consistency(title, content),
131
+ "template": score_template_repetition(content),
132
+ }
133
+ except Exception as e: # pragma: no cover - 防御降级
134
+ _log(f"compute_content_quality 失败: {type(e).__name__}")
135
+
136
+ # 低质折扣:三项都只降不升(乘子 ≤1),且各自设下限,避免单个信号
137
+ # 就抹掉全部吸收分——假阳性代价高于漏放。
138
+ #
139
+ # 权重按 2026-09-27 标定数据重排(tests/golden/lowquality_calibration.json):
140
+ # title_body AUC 0.643 最佳门槛 0.697 最佳F1 0.714 → 主力,折扣最深
141
+ # clickbait AUC 0.643 最佳门槛 0.075 最佳F1 0.444 → 辅助,门槛保持 0.5
142
+ # template AUC 0.500(=随机) → 降为纯旁挂,见下
143
+ #
144
+ # template 的折扣从 ×0.85 撤掉:它在本仓口径下与随机猜测无差别(正常技术
145
+ # 长文 0.833 vs 农场 0.875),却要对每一篇句式平行的正常长文都扣 15%——
146
+ # 那是**确定的伤害换不到的收益**。信号仍照常计算并输出(可观测、未来可
147
+ # 替换为真句法分析),但不再参与折扣计算。若将来接入 POS/依存句法并跑出
148
+ # AUC>0.75,这里是恢复点。
149
+ absorption = evidence.get("absorption_score")
150
+ if absorption is not None and seo_signals:
151
+ discount = 1.0
152
+ cb = seo_signals.get("clickbait") or {}
153
+ tb = seo_signals.get("title_body") or {}
154
+ if cb.get("score", 0) >= 0.5:
155
+ discount *= 0.90 # 标题党(弱信号):-10%
156
+ if tb.get("mismatch"):
157
+ # 文不对题按覆盖度线性折扣:coverage=0 → ×0.7,coverage=0.5 → ×0.85
158
+ cov = float(tb.get("coverage") or 0.0)
159
+ discount *= (0.70 + 0.30 * min(cov / 0.5, 1.0))
160
+ absorption = round(max(0.0, float(absorption) * discount), 3)
161
+
162
+ return {
163
+ "url": url,
164
+ "absorption": absorption,
165
+ # Agent 档摘要:与证据一起落缓存,之后任何搜索回填即得——不重新
166
+ # 抓取也有 600-800 字档(DeepSeek summary 语义,见 summarize_for_agent)。
167
+ "summary": summarize_for_agent(content),
168
+ "quality_score": evidence.get("quality_score", fetch_result.get("quality_score")),
169
+ "content_ok": evidence.get("content_ok", fetch_result.get("content_ok")),
170
+ "word_count": evidence.get("word_count", len(content)),
171
+ "evidence_flags": {
172
+ k: bool(evidence.get(k))
173
+ for k in ("has_numbers", "has_definition", "has_comparison",
174
+ "has_howto", "has_disclose", "is_qa_format")
175
+ },
176
+ "seo_signals": seo_signals or None,
177
+ "page_type": fetch_result.get("page_type"),
178
+ "source_type": fetch_result.get("source_type"),
179
+ "fetch_method": fetch_result.get("fetch_method"),
180
+ "cached": bool(fetch_result.get("cached", False)),
181
+ }
182
+
183
+
184
+ def ttl_for_fetch_result(fetch_result: dict[str, Any]) -> int:
185
+ """按页面类型选证据缓存 TTL(与 fetch_v3 写正文缓存的策略保持一致)。"""
186
+ st = fetch_result.get("source_type") or fetch_result.get("page_type") or ""
187
+ if st in ("news", "realtime"):
188
+ return EVIDENCE_NEWS_TTL
189
+ if st in ("docs", "documentation", "reference"):
190
+ return EVIDENCE_DOC_TTL
191
+ return EVIDENCE_DEFAULT_TTL
192
+
193
+
194
+ # ── B. URL 证据缓存 ────────────────────────────────────────────────────────────
195
+
196
+ def store_fetch_evidence(url: str, evidence: dict[str, Any],
197
+ cache: Any | None = None,
198
+ ttl: int | None = None) -> None:
199
+ """写 URL → 正文级证据分缓存(独立 kind,与 fetch 正文缓存隔离)。"""
200
+ if not evidence:
201
+ return
202
+ try:
203
+ from cache import SearchCache
204
+ c = cache if cache is not None else SearchCache()
205
+ c.set_evidence(url, evidence, ttl=ttl if ttl is not None else EVIDENCE_DEFAULT_TTL)
206
+ except Exception as e: # pragma: no cover
207
+ _log(f"store_fetch_evidence 失败: {type(e).__name__}")
208
+
209
+
210
+ def lookup_fetch_evidence(url: str, cache: Any | None = None) -> Optional[dict[str, Any]]:
211
+ """读 URL → 正文级证据分缓存。未命中返回 None。"""
212
+ try:
213
+ from cache import SearchCache
214
+ c = cache if cache is not None else SearchCache()
215
+ hit = c.get_evidence(url)
216
+ if hit:
217
+ out = {k: v for k, v in hit.items() if not str(k).startswith("_")}
218
+ return out
219
+ except Exception as e: # pragma: no cover
220
+ _log(f"lookup_fetch_evidence 失败: {type(e).__name__}")
221
+ return None
222
+
223
+
224
+ # ── C. 回填 ────────────────────────────────────────────────────────────────────
225
+
226
+ def backfill_results(results: list[dict[str, Any]],
227
+ cache: Any | None = None) -> list[dict[str, Any]]:
228
+ """对搜索结果回填已核验证据(若 URL 有证据缓存)。
229
+
230
+ 原地标记(不改排序):
231
+ - has_fetched_evidence: bool
232
+ - post_fetch_absorption: float | None(正文级吸收分)
233
+ - fetched_evidence: dict(完整证据记录,含 word_count/quality/content_ok)
234
+ """
235
+ for r in results:
236
+ if not isinstance(r, dict):
237
+ continue
238
+ url = r.get("url") or ""
239
+ if not url:
240
+ continue
241
+ ev = lookup_fetch_evidence(url, cache)
242
+ if ev and ev.get("absorption") is not None:
243
+ r["has_fetched_evidence"] = True
244
+ r["post_fetch_absorption"] = ev.get("absorption")
245
+ if ev.get("summary"):
246
+ r["summary"] = ev["summary"]
247
+ r["fetched_evidence"] = {
248
+ k: ev.get(k) for k in (
249
+ "quality_score", "word_count", "content_ok",
250
+ "page_type", "fetch_method", "evidence_flags",
251
+ )
252
+ }
253
+ return results
254
+
255
+
256
+ # ── D. 高后果门控 ──────────────────────────────────────────────────────────────
257
+
258
+ def gate_results(results: list[dict[str, Any]],
259
+ domain: str | None,
260
+ cache: Any | None = None) -> dict[str, Any]:
261
+ """证据门控:标记哪些结果建议核验 + 是否高后果域。
262
+
263
+ 返回 gate 元数据(不修改 results 排序):
264
+ - fetch_required: bool(高后果域)
265
+ - high_consequence_domain: str | None
266
+ - suggested: 建议核验的 URL 列表
267
+ - verified_count: 已有正文证据的结果数
268
+ - pending_count: 建议核验但尚未核验的结果数
269
+ 每条结果原地标记 fetch_suggested(SERP/跳转链与已核验不标记)。
270
+ """
271
+ backfill_results(results, cache)
272
+ verified_count = 0
273
+ suggested: list[str] = []
274
+ for r in results:
275
+ if not isinstance(r, dict):
276
+ continue
277
+ url = r.get("url") or ""
278
+ serp = bool(r.get("authority_tier") == "serp") or bool(
279
+ r.get("evidence_flags", {}).get("is_serp"))
280
+ if r.get("has_fetched_evidence"):
281
+ verified_count += 1
282
+ r["fetch_suggested"] = False
283
+ continue
284
+ is_serp_url = False
285
+ if url:
286
+ try:
287
+ from serp_guard import is_serp_or_jump_url
288
+ is_serp_url = is_serp_or_jump_url(url)
289
+ except Exception: # pragma: no cover
290
+ is_serp_url = False
291
+ if serp or is_serp_url or not url:
292
+ r["fetch_suggested"] = False
293
+ continue
294
+ r["fetch_suggested"] = True
295
+ # 源内全文直出优先:gutenberg / e-Gov 这类结果的 `url` 是下载门户或
296
+ # JS 空壳页(实测去标签后取不到正文),而 `full_text_url` 是该源给出
297
+ # 的、可确定性取到正文的端点(公版书纯文本 / 法令全文 XML)。取数建议
298
+ # 据此走直连,省掉「门户页 → 找下载链」或浏览器渲染那几级。
299
+ suggested.append(str(r.get("full_text_url") or url))
300
+
301
+ hc = is_high_consequence_domain(domain)
302
+ return {
303
+ "fetch_required": hc,
304
+ "high_consequence_domain": domain if hc else None,
305
+ "suggested": suggested,
306
+ "verified_count": verified_count,
307
+ "pending_count": len(suggested),
308
+ }
309
+
310
+
311
+ # ── E. 核验模式(显式,不阻塞热路径)────────────────────────────────────────────
312
+
313
+ def reorder_by_evidence(results: list[dict[str, Any]],
314
+ pre_scores: dict[str, float] | None = None,
315
+ weight: float = 0.35) -> dict[str, Any]:
316
+ """把正文级证据分回写进排序(2026-09-27 新增,补上闭环缺口)。
317
+
318
+ **为什么需要它**:`verify_results` 抓回正文、算了正文级 absorption,但旧
319
+ 实现只把分数写进 `post_fetch_absorption` 字段,**不重排**(调用点在
320
+ `search_cli.py` 的排序之后)。于是「抓取链路已经识别出这是低质正文」这份
321
+ 情报到不了排序器——`--verify` 花了 RTT 却只改展示,不改结果顺序。实测
322
+ `argo search "护眼台灯 推荐" --verify 3` 的 rerank_dims 里 authority 一字未变。
323
+
324
+ **做法**:只对**已核验**的结果降权(不提升),中性点为 0.5:
325
+
326
+ factor = 1 - weight × max(0, (0.5 - 正文质量)) / 0.5
327
+ 新分 = 原分 × factor
328
+
329
+ 三个设计取舍,都是为了避免「验证过的反而吃亏」这类采样偏差:
330
+
331
+ ① **只降不升**。verify 只覆盖 top-k,若正文质量好的条目被加分,等于
332
+ 奖励「恰好被抓取」——而抓取与否与内容质量无关。降权没有这个问题:
333
+ 它表达的是「抓到的证据表明这条不实」,是真实信号。
334
+ ② **0.5 为中性点**。absorption 的分布大致以 0.3-0.6 为主(见
335
+ content_signals.compute_content_quality 的权重),若以「质量本身」
336
+ 直接做系数,绝大多数正常内容都会被无端降权(0.4 分的内容 ×0.4)。
337
+ 以 0.5 为界则只有明显低质者受罚。
338
+ ③ **未核验条目不参与比较**。它们的分数保持原样,因此不会出现
339
+ 「没验证过的因为没被降权而反超」。
340
+
341
+ pre_scores:可选的 {url: 原始 score} 快照,用于在返回里给出 delta 分布,
342
+ 便于观测「这次核验让谁动了、动了多少」。
343
+
344
+ 返回:{reordered: bool, adjusted: [...], moved: int}
345
+ """
346
+ if not results:
347
+ return {"reordered": False, "adjusted": [], "moved": 0}
348
+ try:
349
+ weight = float(weight)
350
+ except (TypeError, ValueError):
351
+ weight = 0.35
352
+ weight = max(0.0, min(1.0, weight))
353
+
354
+ adjusted: list[dict[str, Any]] = []
355
+ old_order = [(r.get("url") or "") for r in results]
356
+
357
+ for r in results:
358
+ if not isinstance(r, dict):
359
+ continue
360
+ # 只处理**真的核验过**的条目:has_fetched_evidence 由 _record_verify 写入,
361
+ # 是「本次或历史抓取确实拿到了正文」的标志。
362
+ if not r.get("has_fetched_evidence"):
363
+ continue
364
+ q = r.get("post_fetch_absorption")
365
+ if q is None:
366
+ q = r.get("absorption")
367
+ try:
368
+ quality = float(q)
369
+ except (TypeError, ValueError):
370
+ continue
371
+ quality = max(0.0, min(1.0, quality))
372
+
373
+ try:
374
+ base = float(r.get("score") or 0.0)
375
+ except (TypeError, ValueError):
376
+ continue
377
+ # 只降不升:中性点 0.5,低于它才按偏离幅度打折
378
+ shortfall = max(0.0, (0.5 - quality) / 0.5)
379
+ factor = 1.0 - weight * shortfall
380
+ new_score = base * factor
381
+ if new_score == base:
382
+ continue # 无变化:不记入 adjusted(避免噪声)
383
+ r["score"] = round(new_score, 4)
384
+ dims = r.get("rerank_dims")
385
+ if isinstance(dims, dict):
386
+ dims["post_fetch_quality"] = round(quality, 3)
387
+ adjusted.append({
388
+ "url": r.get("url") or "",
389
+ "pre_score": round(base, 4),
390
+ "post_score": round(new_score, 4),
391
+ "quality": round(quality, 3),
392
+ })
393
+
394
+ if not adjusted:
395
+ return {"reordered": False, "adjusted": [], "moved": 0}
396
+
397
+ # 只在确有分数变化时重排;无变化时保持原序(逐位可对拍)
398
+ changed = any(a["pre_score"] != a["post_score"] for a in adjusted)
399
+ if changed:
400
+ # 不加 abs():分数是「越大越相关」,负数不该被顶到最前。原实现用
401
+ # abs() 纯属冗余(score 恒非负),但它把「负分=最相关」这个反向
402
+ # 语义留在了排序器里——一旦将来有算子产出负分(如「扣分制」改版),
403
+ # 排序会静默倒置。降权只会让分数更小,用普通降序即可。
404
+ results.sort(key=lambda r: r.get("score", 0) or 0, reverse=True)
405
+ new_order = [(r.get("url") or "") for r in results]
406
+ moved = sum(1 for i, u in enumerate(new_order)
407
+ if i < len(old_order) and u != old_order[i])
408
+
409
+ if pre_scores is not None:
410
+ for a in adjusted:
411
+ a["delta"] = round(a["post_score"] - a["pre_score"], 4)
412
+
413
+ return {"reordered": changed, "adjusted": adjusted, "moved": moved}
414
+
415
+
416
+ def verify_results(results: list[dict[str, Any]],
417
+ query: str,
418
+ cache: Any | None = None,
419
+ fetch_fn: Callable[..., dict[str, Any]] | None = None,
420
+ top_k: int = 3,
421
+ max_chars: int = 8000,
422
+ timeout: float = 4.0) -> dict[str, Any]:
423
+ """对 top_k 未核验结果显式 fetch,回填证据分并产出 evidence_revision 分布。
424
+
425
+ 不进热路径:调用方(CLI --verify / research --verify)显式触发。
426
+ 已核验(有证据缓存)的结果跳过,不重复打网。
427
+
428
+ 默认走核验快道:核验只需要「正文存在性 + 吸收分」,不需要全文渲染——
429
+ 关掉浏览器兜底(Wayback/CDP 最慢两级)并用 8s deadline 硬顶单 URL
430
+ (此前吃 60s 全局默认,冷查询实测 8.7s 全在慢 URL 的降级链上)。
431
+ 拿不到正文照旧标 pending,核验语义不变;要完整降级链可显式传 fetch_fn。
432
+
433
+ 返回:
434
+ - verified: [{url, title, pre_absorption, post_absorption, delta, content_ok, fetch_method}]
435
+ - revision_summary: {n, improved, unchanged, degraded, mean_delta, median_delta}
436
+ - pending: 仍然未核验的 URL(fetch 失败/SERP)
437
+ - skipped_cached: 命中有证据缓存而跳过的 URL 数
438
+ """
439
+ if fetch_fn is None:
440
+ try:
441
+ from fetch_v3 import fetch_v3
442
+
443
+ def _verify_fetch(url: str, max_chars: int, timeout: float) -> dict:
444
+ return fetch_v3(url, max_chars=max_chars, timeout=timeout,
445
+ use_browser_fallback=False, deadline_s=8.0)
446
+ fetch_fn = _verify_fetch
447
+ except ImportError as e: # pragma: no cover
448
+ return {"error": f"fetch_v3 不可用: {e}", "verified": [],
449
+ "revision_summary": {}, "pending": [], "skipped_cached": 0}
450
+
451
+ # 守卫:top_k 非正数(0 / 负数)按默认 3 处理,避免反向切片(results[:-1])
452
+ try:
453
+ top_k = int(top_k)
454
+ except (TypeError, ValueError):
455
+ top_k = 3
456
+ if top_k <= 0:
457
+ top_k = 3
458
+
459
+ verified: list[dict[str, Any]] = []
460
+ pending: list[str] = []
461
+ skipped_cached = 0
462
+ revisions: list[float] = []
463
+
464
+
465
+ def _record_verify(r: dict, ev: dict, verified: list, revisions: list) -> None:
466
+ """并行/串行共用的核验回填逻辑。"""
467
+ pre = float(r.get("post_fetch_absorption")
468
+ if r.get("post_fetch_absorption") is not None
469
+ else r.get("absorption") or 0.0)
470
+ post = float(ev["absorption"] or 0.0)
471
+ delta = round(post - pre, 3)
472
+ revisions.append(delta)
473
+ verified.append({
474
+ "url": r.get("url") or "",
475
+ "title": (r.get("title") or "")[:120],
476
+ "pre_absorption": round(pre, 3),
477
+ "post_absorption": round(post, 3),
478
+ "delta": delta,
479
+ "content_ok": ev.get("content_ok"),
480
+ "fetch_method": ev.get("fetch_method"),
481
+ "word_count": ev.get("word_count"),
482
+ })
483
+ r["has_fetched_evidence"] = True
484
+ r["post_fetch_absorption"] = round(post, 3)
485
+ if ev.get("summary"):
486
+ r["summary"] = ev["summary"]
487
+ r["fetch_suggested"] = False
488
+
489
+ # 并行 fetch:URL 独立,串行最坏 top_k×timeout
490
+ from concurrent.futures import ThreadPoolExecutor, as_completed
491
+ targets = []
492
+ for r in results[:top_k]:
493
+ if not isinstance(r, dict):
494
+ continue
495
+ url = r.get("url") or ""
496
+ if not url:
497
+ continue
498
+ # 已核验:跳过,不重复 fetch
499
+ if lookup_fetch_evidence(url, cache) is not None:
500
+ skipped_cached += 1
501
+ continue
502
+ targets.append(r)
503
+
504
+ def _fetch_one(r: dict) -> tuple[dict, dict | None, str]:
505
+ url = r.get("url") or ""
506
+ try:
507
+ fr = fetch_fn(url, max_chars=max_chars, timeout=timeout)
508
+ except Exception as e: # pragma: no cover
509
+ _log(f"verify fetch 异常 {url}: {type(e).__name__}")
510
+ return r, None, ""
511
+ # fetch_fn 可能返回非 dict(上游异常形态),取正文前必须判型,
512
+ # 与 extract_fetch_evidence 的容错口径保持一致
513
+ text = (fr.get("content") or "") if isinstance(fr, dict) else ""
514
+ return r, extract_fetch_evidence(fr), text
515
+
516
+ # 语义层(默认关)的输入:正文已在内存,批量一次调用;关闭时不产生任何行为
517
+ scored_inputs: list[dict[str, Any]] = []
518
+
519
+ if len(targets) > 1:
520
+ with ThreadPoolExecutor(max_workers=min(len(targets), 3)) as ex:
521
+ futures = [ex.submit(_fetch_one, r) for r in targets]
522
+ for fut in as_completed(futures):
523
+ r, ev, text = fut.result()
524
+ if ev is None:
525
+ pending.append(r.get("url") or "")
526
+ continue
527
+ # 不再在这里重复写证据:fetch_fn(fetch_v3)成功时已把证据分
528
+ # 并入该 URL 的正文条目。此处再写一遍是同源同刻的双写,
529
+ # 合并存储后还会因为「没有正文条目」而变成静默空操作。
530
+ _record_verify(r, ev, verified, revisions)
531
+ if text.strip():
532
+ scored_inputs.append({"url": r.get("url") or "",
533
+ "title": r.get("title") or "",
534
+ "text": text})
535
+ else:
536
+ for r in targets:
537
+ r2, ev, text = _fetch_one(r)
538
+ if ev is None:
539
+ pending.append(r2.get("url") or "")
540
+ continue
541
+ _record_verify(r2, ev, verified, revisions)
542
+ if text.strip():
543
+ scored_inputs.append({"url": r2.get("url") or "",
544
+ "title": r2.get("title") or "",
545
+ "text": text})
546
+
547
+ semantic_meta: dict[str, Any] | None = None
548
+ if scored_inputs:
549
+ try:
550
+ from semantic_evidence import assess_support, enabled as _semantic_enabled
551
+ if _semantic_enabled():
552
+ support = assess_support(query, scored_inputs)
553
+ if support:
554
+ for item in verified:
555
+ info = support.get(item.get("url") or "")
556
+ if info:
557
+ item["semantic_support"] = info
558
+ for r in results:
559
+ info = support.get(r.get("url") or "")
560
+ if info:
561
+ r["semantic_support"] = info
562
+ semantic_meta = {
563
+ "scored": len(support),
564
+ "supports": sum(1 for x in support.values() if x["supports"]),
565
+ "contradicts": sum(1 for x in support.values() if x["contradicts"]),
566
+ }
567
+ except Exception as e: # fail-open:语义层不阻断核验
568
+ _log(f"语义证据层失败: {type(e).__name__}")
569
+
570
+ summary: dict[str, Any] = {"n": len(revisions)}
571
+ if revisions:
572
+ summary.update({
573
+ "improved": sum(1 for d in revisions if d > 0.02),
574
+ "unchanged": sum(1 for d in revisions if abs(d) <= 0.02),
575
+ "degraded": sum(1 for d in revisions if d < -0.02),
576
+ "mean_delta": round(sum(revisions) / len(revisions), 3),
577
+ "median_delta": round(sorted(revisions)[len(revisions) // 2], 3),
578
+ "max_delta": round(max(revisions), 3),
579
+ "min_delta": round(min(revisions), 3),
580
+ })
581
+
582
+ out: dict[str, Any] = {
583
+ "query": query,
584
+ "verified": verified,
585
+ "revision_summary": summary,
586
+ "pending": pending,
587
+ "skipped_cached": skipped_cached,
588
+ }
589
+ # 语义层默认关:关闭时连键都不出现,输出与未接入时逐位一致
590
+ if semantic_meta is not None:
591
+ out["semantic"] = semantic_meta
592
+ return out