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
@@ -1,73 +1,100 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- evidence.py — 来源可信度评估工具(wigolo evidence_score 理念移植)
3
+ evidence.py — 来源可信度评估(v2.2 两阶段:Selection × Absorption)
4
4
 
5
- 核心能力:
6
- 1. 权威性评分:基于域名白名单/黑名单 + 来源类型分级
7
- 2. 时效性评分:基于内容时间戳 + 搜索时间
8
- 3. 交叉验证:多个来源是否佐证同一结论
9
- 4. 综合可信度:加权计算 + 透明分解
5
+ 问题重定义(第一性):
6
+ Agent 要的不是「搜到了什么」,而是「哪些材料可被吸收进可核验答案」。
7
+ 被检索到(selection)≠ 被采用为证据(absorption)。
8
+
9
+ MECE 分解:
10
+ A. Selection 门槛:URL 是否可引用页面 / 域名权威 / 是否 SERP·榜单污染
11
+ B. Absorption 深度:数字·定义·对比·披露 等证据块密度(snippet 或正文)
12
+ C. Freshness:发布年/URL 年;忽略「2015年以来」类历史对比年
13
+ D. Consensus:多域名佐证(交叉验证)
14
+
15
+ 综合:
16
+ final = 0.40 * selection + 0.35 * absorption + 0.15 * freshness + 0.10 * original
17
+ high-stakes 可再 × consensus 调节(见 compute_credibility)
10
18
 
11
19
  用法:
12
- python3 evidence.py --urls "https://example.com" "https://example2.com"
13
- python3 evidence.py --search-result '{"results": [...]}' --query "茅台股价"
14
- echo '{"results": [...]}' | python3 evidence.py --stdin --query "查询词"
20
+ echo '{"results": [...]}' | python3 evidence.py "查询词" --stdin --json
15
21
  """
16
22
 
17
23
  from __future__ import annotations
18
24
 
19
25
  import argparse
26
+ import functools
20
27
  import json
21
28
  import os
22
29
  import re
23
30
  import sys
24
31
  import time
32
+ from datetime import datetime
25
33
  from typing import Any
26
34
  from urllib.parse import urlparse
35
+ from cli_io import dumps, ensure_utf8_stdio
27
36
 
28
37
  SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
29
38
  sys.path.insert(0, SCRIPT_DIR)
30
39
 
40
+ try:
41
+ from content_signals import score_evidence_density
42
+ except ImportError: # pragma: no cover
43
+ def score_evidence_density(text: str, title: str = "") -> dict[str, Any]:
44
+ return {
45
+ "has_numbers": False,
46
+ "has_definition": False,
47
+ "has_comparison": False,
48
+ "has_howto": False,
49
+ "has_disclose": False,
50
+ "is_qa_format": False,
51
+ "absorption_score": 0.2,
52
+ }
53
+
31
54
 
32
55
  # ── 权威性评分 ────────────────────────────────────────────────────────────────
33
56
 
34
- # 域名权威性分级(越高越好)
35
57
  AUTHORITY_TIERS = {
36
- # Tier 1: 官方/权威(0.95-1.0)
58
+ # Tier 1: 官方/权威
37
59
  "gov.cn": 1.0, "gov": 0.95, "edu.cn": 0.95, "edu": 0.9,
38
- "ac.cn": 0.95, # 中国科学院
60
+ "ac.cn": 0.95,
39
61
  "nature.com": 0.95, "science.org": 0.95, "ieee.org": 0.9,
40
62
  "acm.org": 0.9, "springer.com": 0.9, "elsevier.com": 0.9,
41
63
  "arxiv.org": 0.9, "pubmed.ncbi.nlm.nih.gov": 0.95,
42
64
  "scholar.google.com": 0.85, "ncbi.nlm.nih.gov": 0.95,
43
- "nvd.nist.gov": 0.95, # 国家漏洞数据库
44
- "cve.mitre.org": 0.95,
65
+ "nvd.nist.gov": 0.95, "cve.mitre.org": 0.95,
66
+ "xinhuanet.com": 0.95, "people.com.cn": 0.95,
45
67
 
46
- # Tier 2: 专业媒体/平台(0.75-0.9)
68
+ # Tier 2: 专业媒体/平台
47
69
  "zhihu.com": 0.85, "github.com": 0.85, "stackoverflow.com": 0.85,
48
70
  "medium.com": 0.75, "dev.to": 0.75,
49
71
  "reuters.com": 0.9, "bloomberg.com": 0.9, "wsj.com": 0.9,
50
- "财新": 0.9, "caixin.com": 0.9,
72
+ "caixin.com": 0.9, "yicai.com": 0.86, "wallstreetcn.com": 0.86,
73
+ "cls.cn": 0.88, "api3.cls.cn": 0.85, "cs.com.cn": 0.92, "stcn.com": 0.88,
74
+ "cnr.cn": 0.9, "thepaper.cn": 0.82, "jiemian.com": 0.8,
51
75
  "36kr.com": 0.8, "infoq.cn": 0.8, "juejin.cn": 0.75,
52
- "eastmoney.com": 0.85, "xueqiu.com": 0.8,
76
+ "eastmoney.com": 0.85, "xueqiu.com": 0.8, "10jqka.com.cn": 0.78,
53
77
  "docs.python.org": 0.9, "react.dev": 0.9, "nextjs.org": 0.9,
54
78
 
55
- # Tier 3: 通用可信(0.6-0.75)
79
+ # Tier 3: 通用可信
56
80
  "wikipedia.org": 0.75, "baike.baidu.com": 0.7,
57
- "维基百科": 0.75, "百度百科": 0.7,
58
- "linkedin.com": 0.65, "twitter.com": 0.55, "x.com": 0.55,
59
- "reddit.com": 0.65, "hackernews": 0.7,
60
-
61
- # Tier 4: 内容农场/低质(0.2-0.4)
62
- "sohu.com": 0.4, "163.com": 0.45, "sina.com.cn": 0.5,
63
- "baijiahao.baidu.com": 0.35, "zhuanlan.zhihu.com": 0.7,
64
- "toutiao.com": 0.4, "weixin.qq.com": 0.5,
65
-
66
- # Tier 5: 已知低质(0.1-0.2)
67
- "content-farm": 0.1, "seo-spam": 0.1,
81
+ "linkedin.com": 0.65, "twitter.com": 0.45, "x.com": 0.45,
82
+ "reddit.com": 0.55,
83
+ # 主流门户:与 evidence_tier.py B 级「知名第三方」同口径(163/sohu/sina
84
+ # 都在 B 级名单里)。此前压在下方「内容农场」档 0.4-0.55,两份名单
85
+ # 自相矛盾,中文主流媒体被系统性降权(09-26 审查第 7 项,十项 P0 的
86
+ # 最后一项,2026-10-02 归位)。k.sina.com.cn 是聚合/博客通道,留 Tier 4。
87
+ "163.com": 0.65, "sohu.com": 0.65, "sina.com.cn": 0.65,
88
+
89
+ # Tier 4: 内容农场/低质
90
+ "k.sina.com.cn": 0.55, "baijiahao.baidu.com": 0.35,
91
+ "zhuanlan.zhihu.com": 0.7, "toutiao.com": 0.4, "weixin.qq.com": 0.5,
92
+ "guba.eastmoney.com": 0.35,
93
+
94
+ # 商业榜单(高引用≠高可信)
95
+ "maigoo.com": 0.28, "chinapp.com": 0.28, "cnpp.cn": 0.28,
68
96
  }
69
97
 
70
- # 来源类型映射
71
98
  SOURCE_TYPE_MAP = {
72
99
  "eastmoney": ("金融数据", 0.9),
73
100
  "zhihu": ("社区观点", 0.8),
@@ -75,145 +102,270 @@ SOURCE_TYPE_MAP = {
75
102
  "semantic_scholar": ("学术索引", 0.9),
76
103
  "openalex": ("学术索引", 0.85),
77
104
  "crossref": ("学术元数据", 0.9),
105
+ "europepmc": ("生物医学文献", 0.88),
106
+ "dblp": ("CS 文献索引", 0.85),
78
107
  "github": ("代码仓库", 0.85),
79
108
  "byted": ("中文搜索", 0.7),
80
109
  "bocha": ("中文搜索", 0.7),
81
110
  "tavily": ("AI搜索", 0.75),
82
111
  "felo": ("AI搜索", 0.7),
83
- "duckduckgo": ("通用搜索", 0.65),
84
112
  "wikipedia": ("百科", 0.75),
113
+ "baidu_baike": ("中文百科", 0.72),
114
+ "mdn": ("官方文档", 0.9),
115
+ "pypi": ("包注册表", 0.88),
116
+ "npm": ("包注册表", 0.88),
117
+ "huggingface": ("模型仓库", 0.85),
118
+ "docker_hub": ("容器镜像", 0.8),
119
+ "juejin": ("中文技术社区", 0.65),
120
+ "devto": ("技术社区", 0.65),
121
+ "clinicaltrials": ("临床试验注册", 0.9),
122
+ "openfda": ("药品标签", 0.88),
123
+ "archive_org": ("数字档案馆", 0.75),
124
+ "steam": ("游戏商店", 0.7),
125
+ "polymarket": ("预测市场叙事", 0.35),
85
126
  "anysearch": ("垂直搜索", 0.75),
86
- "wigolo": ("本地搜索", 0.7),
127
+ "local_search": ("本地搜索", 0.7),
87
128
  "metaso": ("AI搜索", 0.75),
129
+ "local_baidu": ("搜索结果页聚合", 0.35),
130
+ "local_sogou": ("搜索结果页聚合", 0.35),
131
+ "local_bing": ("搜索结果页聚合", 0.4),
132
+ "finviz": ("美股快照", 0.75),
133
+ "seeking_alpha": ("美股分析", 0.7),
134
+ "jin10": ("财经快讯", 0.65),
135
+ "models_dev": ("模型目录", 0.85),
136
+ "octen": ("AI搜索", 0.7),
137
+ "qweather": ("天气数据", 0.9),
138
+ "wenshu": ("裁判文书", 0.75),
139
+ "zhihu_global": ("社区观点", 0.8),
140
+ "zhihu_hot": ("社区热榜", 0.55),
88
141
  }
89
142
 
143
+ # SERP/跳转 URL 判定已迁入 serp_guard(纯 URL 规则,轻依赖;热路径不再
144
+ # 为一次判定付出本模块的证据质量栈导入)。此处转出仅为既有调用方兼容,
145
+ # 新代码直接从 serp_guard import。判定的实现注释(为什么后缀匹配、为什么
146
+ # 单一数据源)见 serp_guard.py。
147
+ from serp_guard import ( # noqa: E402
148
+ _load_cn_source_types,
149
+ _normalize_domain,
150
+ is_serp_or_jump_url,
151
+ )
90
152
 
91
- def score_authority(url: str, source: str = "") -> dict[str, Any]:
92
- """评估 URL 的权威性。"""
93
- if not url:
94
- return {"score": 0.3, "reason": "无 URL", "tier": "unknown"}
95
153
 
96
- parsed = urlparse(url)
97
- domain = parsed.netloc.lower().strip("www.")
98
- path = parsed.path.lower()
154
+ @functools.lru_cache(maxsize=512)
155
+ def _score_authority_cached(url: str, source: str = "") -> tuple:
156
+ """score_authority 的缓存版本,返回 tuple(lru_cache 要求可哈希返回值)。"""
157
+ if not url:
158
+ return (0.3, "无 URL", "unknown", "", True)
159
+
160
+ domain = _normalize_domain(url)
161
+ is_serp = is_serp_or_jump_url(url)
162
+
163
+ if is_serp:
164
+ return (
165
+ 0.12,
166
+ "搜索结果页/跳转链(不可作吸收源)",
167
+ "very_low",
168
+ domain,
169
+ True,
170
+ )
99
171
 
100
- # 精确匹配域名
101
172
  best_score = 0.5
102
173
  best_reason = "通用域名"
103
-
174
+ best_len = -1
104
175
  for pattern, score in AUTHORITY_TIERS.items():
105
176
  if domain == pattern or domain.endswith("." + pattern):
106
- if score > best_score:
177
+ if len(pattern) > best_len:
178
+ best_len = len(pattern)
107
179
  best_score = score
108
180
  best_reason = f"域名匹配:{pattern}"
109
181
 
110
- # 路径特征加分
182
+ # JSON 覆盖
183
+ for d, score in (_load_cn_source_types().get("authority_overrides") or {}).items():
184
+ if domain == d or domain.endswith("." + d):
185
+ if len(d) >= best_len:
186
+ best_len = len(d)
187
+ best_score = float(score)
188
+ best_reason = f"中文信源表:{d}"
189
+
190
+ demote = _load_cn_source_types().get("demote_domains") or {}
191
+ for d, meta in demote.items():
192
+ if domain == d or domain.endswith("." + d):
193
+ best_score = float(meta.get("score", 0.3))
194
+ best_reason = f"降权:{meta.get('reason', d)}"
195
+ break
196
+
197
+ social = _load_cn_source_types().get("social_narrative_only") or {}
198
+ for d, score in social.items():
199
+ if domain == d or domain.endswith("." + d):
200
+ best_score = min(best_score, float(score))
201
+ best_reason = f"社交/叙事源(非事实真值):{d}"
202
+ break
203
+
204
+ # 路径特征
205
+ try:
206
+ path = urlparse(url).path.lower()
207
+ except Exception:
208
+ path = ""
111
209
  if "/docs/" in path or "/documentation/" in path:
112
210
  best_score = min(best_score + 0.05, 1.0)
113
211
  best_reason += "(文档路径)"
114
212
  if "/paper/" in path or "/arxiv/" in path or "/abs/" in path:
115
213
  best_score = min(best_score + 0.05, 1.0)
116
214
  best_reason += "(论文路径)"
117
- if "/issues/" in path or "/pull/" in path:
118
- best_score = min(best_score + 0.03, 1.0)
119
- best_reason += "(Issue/PR路径)"
120
215
 
121
- # 来源类型加分
216
+ # 来源类型:作为保底分(max),本地 SERP 聚合引擎不抬升
122
217
  if source and source in SOURCE_TYPE_MAP:
123
218
  type_name, type_score = SOURCE_TYPE_MAP[source]
124
- if type_score > best_score:
125
- best_score = type_score
126
- best_reason = f"来源类型:{type_name}"
219
+ if source.startswith("local_") and type_score < 0.5:
220
+ # 搜索结果页聚合:压低
221
+ best_score = min(best_score, type_score)
222
+ best_reason += f"(引擎={source}/{type_name})"
223
+ else:
224
+ if type_score > best_score:
225
+ best_score = type_score
226
+ best_reason = f"来源类型:{type_name}" if best_reason == "通用域名" \
227
+ else f"{best_reason}(引擎={source}↑)"
228
+ else:
229
+ best_reason += f"(引擎={source})"
230
+
231
+ tier = (
232
+ "high" if best_score >= 0.8
233
+ else "medium" if best_score >= 0.6
234
+ else "low" if best_score >= 0.35
235
+ else "very_low"
236
+ )
237
+
238
+ return (best_score, best_reason, tier, domain, bool(is_serp))
127
239
 
128
- tier = "high" if best_score >= 0.8 else "medium" if best_score >= 0.6 else "low" if best_score >= 0.4 else "very_low"
129
240
 
241
+ def score_authority(url: str, source: str = "") -> dict[str, Any]:
242
+ """评估 URL 的权威性(Selection 门槛)。
243
+
244
+ 结果按 (url, source) 做进程内 lru_cache:同一 URL + source 组合在多次
245
+ rerank/selection 中只算一次,避免 search.py 的 local_five_dim_rerank 与
246
+ _attach_selection_signals 对同一 URL 重复评分。
247
+ """
248
+ score, reason, tier, domain, is_serp = _score_authority_cached(url, source)
130
249
  return {
131
- "score": round(best_score, 2),
132
- "reason": best_reason,
250
+ "score": round(score, 2),
251
+ "reason": reason,
133
252
  "tier": tier,
134
253
  "domain": domain,
254
+ "is_serp": is_serp,
135
255
  }
136
256
 
137
257
 
138
258
  # ── 时效性评分 ────────────────────────────────────────────────────────────────
139
259
 
140
- def score_freshness(result: dict[str, Any], query_time: float = None) -> dict[str, Any]:
141
- """评估结果的时效性。"""
142
-
143
- def _extract_year_from_url(url: str):
144
- """从 URL 路径提取年份。"""
145
- patterns = [
146
- r"/(20\d{2})[/-](\d{1,2})[/-](\d{1,2})",
147
- r"/(20\d{2})[/-](\d{1,2})",
148
- r"/(20\d{2})/",
149
- ]
150
- for p in patterns:
151
- m = re.search(p, url)
152
- if m:
153
- return int(m.group(1))
154
- return None
155
-
156
- if query_time is None:
157
- query_time = time.time()
158
-
159
- snippet = result.get("snippet", "") or ""
160
- title = result.get("title", "") or ""
161
- url = result.get("url", "") or ""
260
+ # 「2015年以来」等历史对比年 — 不当作发布年
261
+ _HIST_SINCE_RE = re.compile(
262
+ r"(?:自|从)?(19|20)\d{2}\s*年?\s*(?:以来|起|至今|至|到现在|之前|以前|以前水平)"
263
+ )
264
+ _FULL_DATE_RE = re.compile(
265
+ r"(20\d{2})[年/-](\d{1,2})[月/-](\d{1,2})"
266
+ )
267
+ _PUBLISH_HINT_RE = re.compile(
268
+ r"(?:发布|披露|刊登|更新|Published|发表)[^\n]{0,12}(20\d{2})"
269
+ r"|(20\d{2})\s*年\s*(?:[0-1]?\d\s*月)?\s*(?:[0-3]?\d\s*日)?\s*(?:电|讯|报道)"
270
+ )
271
+
272
+
273
+ def _years_from_text(text: str) -> list[int]:
274
+ """提取候选年份,排除历史对比语境中的旧年。"""
275
+ if not text:
276
+ return []
277
+ # 挖掉「2015年以来」整段,避免污染
278
+ cleaned = _HIST_SINCE_RE.sub(" ", text)
279
+ # 中文语境不用 \b(「2026年」左侧无词界)
280
+ years = [int(y) for y in re.findall(r"(?<![0-9])(20\d{2})(?![0-9])", cleaned)]
281
+ # 完整日期优先(用原文,保留结构)
282
+ for m in _FULL_DATE_RE.finditer(text):
283
+ years.append(int(m.group(1)))
284
+ for m in _PUBLISH_HINT_RE.finditer(text):
285
+ for g in m.groups():
286
+ if g and re.fullmatch(r"20\d{2}", g):
287
+ years.append(int(g))
288
+ # 合理范围
289
+ now_y = datetime.now().year
290
+ return [y for y in years if 1990 <= y <= now_y + 1]
291
+
292
+
293
+ @functools.lru_cache(maxsize=512)
294
+ def _score_freshness_cached(url: str, title: str, snippet: str) -> tuple[float, str]:
295
+ """score_freshness 的缓存版本:同一 (url, title, snippet) 组合在 rerank、
296
+ selection 及 evidence 打分间只算一次,避免重复的时效性评分
297
+ (实测热路径节省一次 full-text 年份提取)。返回 (score, reason)。
298
+
299
+ 注:原 `query_time` 参数在 score_freshness 内从未参与计算,
300
+ 此处不入缓存键以保持检索一致性。
301
+ """
162
302
  combined = f"{title} {snippet}"
163
-
164
- # 提取时间信息
165
- # 年份
166
- year_match = re.search(r"20(\d{2})", combined)
167
- if year_match:
168
- year = 2000 + int(year_match.group(1))
169
- from datetime import datetime
170
- age_years = datetime.now().year - year
171
- if age_years < 0:
172
- score = 0.4
173
- reason = f"{year}年(未来年份)"
174
- elif age_years == 0:
175
- score = 0.8
176
- reason = f"{year}年(今年)"
177
- elif age_years == 1:
178
- score = 0.9
179
- reason = f"{year}年(去年)"
180
- elif age_years <= 2:
181
- score = 0.7
182
- reason = f"{year}年({age_years}年前)"
183
- elif age_years <= 5:
184
- score = 0.5
185
- reason = f"{year}年({age_years}年前)"
186
- else:
187
- score = 0.3
188
- reason = f"{year}年({age_years}年前,较旧)"
303
+ now_y = datetime.now().year
304
+
305
+ # 1) 完整日期
306
+ full = list(_FULL_DATE_RE.finditer(combined))
307
+ if full:
308
+ # 取最「新」的完整日期
309
+ best = max(full, key=lambda m: (int(m.group(1)), int(m.group(2)), int(m.group(3))))
310
+ year = int(best.group(1))
311
+ age_years = now_y - year
312
+ score, reason = _age_to_score(age_years, year, precise=True)
189
313
  else:
190
- # 无时间信息,尝试从 URL 提取
191
- url_year = _extract_year_from_url(url)
192
- if url_year:
193
- from datetime import datetime
194
- age_years = datetime.now().year - url_year
195
- if age_years <= 1:
196
- score = 0.8
197
- reason = f"URL含{url_year}年"
198
- elif age_years <= 3:
199
- score = 0.6
200
- reason = f"URL含{url_year}年({age_years}年前)"
201
- else:
202
- score = 0.4
203
- reason = f"URL含{url_year}年(较旧)"
314
+ years = _years_from_text(combined)
315
+ url_year = None
316
+ m = re.search(r"/(20\d{2})(?:/|-)", url)
317
+ if m:
318
+ url_year = int(m.group(1))
319
+ years.append(url_year)
320
+
321
+ if years:
322
+ # 优先靠近今年的年份(发布语境),而非文中最早的历史年
323
+ # 若存在今年或去年,取 max;否则取 max 全体
324
+ recent = [y for y in years if y >= now_y - 1]
325
+ year = max(recent) if recent else max(years)
326
+ age_years = now_y - year
327
+ score, reason = _age_to_score(age_years, year, precise=False)
328
+ if url_year and year == url_year:
329
+ reason += "(URL年)"
204
330
  else:
205
331
  score = 0.5
206
332
  reason = "无明确时间标记"
207
333
 
208
- # 时效性关键词加分
209
- if re.search(r"(最新|latest|recent|breaking|just|刚刚|今日|today)", combined, re.I):
334
+ if re.search(r"(最新|latest|recent|breaking|just|刚刚|今日|today|本周|本月)", combined, re.I):
210
335
  score = min(score + 0.1, 1.0)
211
336
  reason += "(含时效关键词)"
212
337
 
213
- return {
214
- "score": round(score, 2),
215
- "reason": reason,
216
- }
338
+ return score, reason
339
+
340
+
341
+ def score_freshness(result: dict[str, Any], query_time: float = None) -> dict[str, Any]:
342
+ """评估结果的时效性(优先完整日期 / URL 年 / 最新合理年)。
343
+
344
+ 结果按 (url, title, snippet) 做进程内 lru_cache:同一 URL 在 rerank、
345
+ selection 与 evidence 打分间只算一次,避免 search.py 的
346
+ local_five_dim_rerank 与 _attach_selection_signals 对同一结果重复评分。
347
+ `query_time` 保留以保持调用契约(当前评分逻辑不依赖它)。
348
+ """
349
+ score, reason = _score_freshness_cached(
350
+ result.get("url", "") or "",
351
+ result.get("title", "") or "",
352
+ result.get("snippet", "") or "",
353
+ )
354
+ return {"score": round(score, 2), "reason": reason}
355
+
356
+
357
+ def _age_to_score(age_years: int, year: int, precise: bool) -> tuple[float, str]:
358
+ if age_years < 0:
359
+ return 0.4, f"{year}年(未来年份)"
360
+ if age_years == 0:
361
+ return (0.9 if precise else 0.85), f"{year}年(今年)"
362
+ if age_years == 1:
363
+ return 0.8, f"{year}年(去年)"
364
+ if age_years <= 2:
365
+ return 0.65, f"{year}年({age_years}年前)"
366
+ if age_years <= 5:
367
+ return 0.45, f"{year}年({age_years}年前)"
368
+ return 0.25, f"{year}年({age_years}年前,较旧)"
217
369
 
218
370
 
219
371
  # ── 交叉验证 ──────────────────────────────────────────────────────────────────
@@ -227,22 +379,26 @@ def cross_validate(results: list[dict[str, Any]], query: str) -> dict[str, Any]:
227
379
  "detail": f"仅 {len(results)} 个结果,无法交叉验证",
228
380
  "agreement_count": 0,
229
381
  "total_sources": len(results),
382
+ "unique_domains": 0,
383
+ "url_overlap": 0.0,
384
+ "content_domains": 0,
230
385
  }
231
386
 
232
- # 检查 URL 重叠(不同来源指向同一页面 = 强佐证)
233
387
  urls = [r.get("url", "") for r in results if r.get("url")]
234
388
  unique_urls = set(urls)
235
389
  url_overlap = 1 - (len(unique_urls) / max(len(urls), 1))
236
390
 
237
- # 检查域名多样性
238
391
  domains = set()
392
+ content_domains = set()
239
393
  for r in results:
240
394
  url = r.get("url", "")
241
- if url:
242
- domain = urlparse(url).netloc.lower().strip("www.")
243
- domains.add(domain)
395
+ if not url:
396
+ continue
397
+ domain = _normalize_domain(url)
398
+ domains.add(domain)
399
+ if not is_serp_or_jump_url(url):
400
+ content_domains.add(domain)
244
401
 
245
- # 检查标题/内容相似度(简单关键词匹配)
246
402
  query_words = set(query.lower().split())
247
403
  content_matches = 0
248
404
  for r in results:
@@ -251,82 +407,332 @@ def cross_validate(results: list[dict[str, Any]], query: str) -> dict[str, Any]:
251
407
  content_matches += 1
252
408
 
253
409
  match_ratio = content_matches / max(len(results), 1)
410
+ n_content = len(content_domains)
254
411
 
255
- # 计算佐证等级
256
- if match_ratio >= 0.8 and len(domains) >= 3:
257
- level = "strong"
258
- score = 0.9
259
- elif match_ratio >= 0.6 and len(domains) >= 2:
260
- level = "moderate"
261
- score = 0.7
412
+ if match_ratio >= 0.8 and n_content >= 3:
413
+ level, score = "strong", 0.9
414
+ elif match_ratio >= 0.6 and n_content >= 2:
415
+ level, score = "moderate", 0.7
262
416
  elif match_ratio >= 0.4:
263
- level = "weak"
264
- score = 0.5
417
+ level, score = "weak", 0.5
265
418
  else:
266
- level = "minimal"
267
- score = 0.3
419
+ level, score = "minimal", 0.3
268
420
 
269
421
  return {
270
422
  "corroboration_level": level,
271
423
  "score": round(score, 2),
272
- "detail": f"{content_matches}/{len(results)} 个结果与查询相关,{len(domains)} 个独立域名",
424
+ "detail": (
425
+ f"{content_matches}/{len(results)} 与查询相关,"
426
+ f"{n_content} 个可吸收域名(总域名 {len(domains)})"
427
+ ),
273
428
  "agreement_count": content_matches,
274
429
  "total_sources": len(results),
275
430
  "unique_domains": len(domains),
431
+ "content_domains": n_content,
276
432
  "url_overlap": round(url_overlap, 2),
277
433
  }
278
434
 
279
435
 
436
+ # ── 可溯源性评分 ──────────────────────────────────────────────────────────────
437
+
438
+ # 一手信源平台特征(用户直接发布的原始内容)
439
+ _PRIMARY_PLATFORM_PATTERNS = {
440
+ "weibo.com": 0.95, # 微博原帖
441
+ "zhihu.com": 0.90, # 知乎回答/文章
442
+ "github.com": 0.90, # GitHub issue/PR/commit
443
+ "stackoverflow.com": 0.90,
444
+ "xiaohongshu.com": 0.85, # 小红书笔记
445
+ "bilibili.com": 0.85, # B站视频/动态
446
+ "twitter.com": 0.85, # X/Twitter 原帖
447
+ "x.com": 0.85,
448
+ "youtube.com": 0.85, # YouTube 视频
449
+ "reddit.com": 0.80, # Reddit 原帖
450
+ "medium.com": 0.80, # Medium 原文
451
+ "dev.to": 0.80,
452
+ "juejin.cn": 0.75, # 掘金原文
453
+ "cnblogs.com": 0.75, # 博客园原文
454
+ }
455
+
456
+ # 原始媒体报道(新闻机构原创报道)
457
+ _ORIGINAL_MEDIA_PATTERNS = {
458
+ "xinhuanet.com": 0.95, # 新华社
459
+ "people.com.cn": 0.95, # 人民网
460
+ "reuters.com": 0.95, # 路透社
461
+ "bloomberg.com": 0.95, # 彭博社
462
+ "wsj.com": 0.95, # 华尔街日报
463
+ "caixin.com": 0.90, # 财新
464
+ "yicai.com": 0.88, # 第一财经
465
+ "wallstreetcn.com": 0.88,# 华尔街见闻
466
+ "cls.cn": 0.88, # 财联社
467
+ "cs.com.cn": 0.90, # 中证网
468
+ "stcn.com": 0.85, # 证券时报
469
+ "cnr.cn": 0.88, # 央广网
470
+ "thepaper.cn": 0.82, # 澎湃新闻
471
+ "jiemian.com": 0.80, # 界面新闻
472
+ "36kr.com": 0.75, # 36氪
473
+ "eastmoney.com": 0.80, # 东方财富
474
+ "163.com": 0.70, # 网易新闻
475
+ "sohu.com": 0.70, # 搜狐新闻
476
+ "sina.com.cn": 0.70, # 新浪新闻
477
+ }
478
+
479
+ # 二手/聚合/洗稿平台特征
480
+ _SECONDARY_PLATFORM_PATTERNS = {
481
+ "baijiahao.baidu.com": 0.35, # 百家号(自媒体洗稿重灾区)
482
+ "toutiao.com": 0.35, # 头条号(聚合+洗稿)
483
+ "weixin.qq.com": 0.40, # 微信公众号(质量参差不齐)
484
+ "guba.eastmoney.com": 0.30, # 股吧(UGC 讨论)
485
+ "zhuanlan.zhihu.com": 0.55, # 知乎专栏(部分原创部分转载)
486
+ "k.sina.com.cn": 0.40, # 新浪博客/聚合
487
+ }
488
+
489
+ # 转载/引用标记(出现在标题或 snippet 中)
490
+ _REPOST_MARKERS = [
491
+ "转载", "转自", "摘自", "编译自", "译自", "综合自",
492
+ "来源:", "来源:", "原文:", "原文:", "援引", "据援引",
493
+ "reposted", "via@", "via @", "credit:", "courtesy",
494
+ ]
495
+
496
+
497
+ def score_traceability(result: dict[str, Any]) -> dict[str, Any]:
498
+ """评估搜索结果的可溯源性(Traceability)。
499
+
500
+ 借鉴 doubao-sentiment-tracker 的溯源四场景方法论,将「能否追溯到原始
501
+ 来源」量化为 0-1 评分。与 Selection(域名权威)正交:一个结果可以
502
+ 权威但不可溯源(如聚合媒体的转载稿),也可以不权威但可溯源(如个人
503
+ 微博原帖)。
504
+
505
+ 评分规则:
506
+ - 一手信源(用户直接发布的原始帖子):0.80-0.95
507
+ - 原始媒体报道(新闻机构原创报道):0.70-0.95
508
+ - 综合报道/评论(引用了原始来源):0.50-0.65
509
+ - 二手转述/洗稿:0.30-0.45
510
+ - 无法判断:0.50(中性)
511
+
512
+ Returns:
513
+ {"score": float, "reason": str, "is_primary": bool, "platform_type": str}
514
+ """
515
+ url = result.get("url", "") or ""
516
+ title = result.get("title", "") or ""
517
+ snippet = result.get("snippet", "") or ""
518
+ source = result.get("source", "") or ""
519
+ content_type = result.get("content_type", "") or ""
520
+ social_meta = result.get("social_meta")
521
+ text = f"{title} {snippet}"
522
+
523
+ if not url:
524
+ return {
525
+ "score": 0.5,
526
+ "reason": "无 URL,无法判断可溯源性",
527
+ "is_primary": False,
528
+ "platform_type": "unknown",
529
+ }
530
+
531
+ domain = _normalize_domain(url)
532
+
533
+ # 1) 检查是否为 SERP/跳转 URL(不可溯源)
534
+ if is_serp_or_jump_url(url):
535
+ return {
536
+ "score": 0.10,
537
+ "reason": "搜索结果页/跳转链,非原始内容",
538
+ "is_primary": False,
539
+ "platform_type": "serp",
540
+ }
541
+
542
+ # 2) 检查是否为一手信源平台
543
+ best_primary_score = 0.0
544
+ best_primary_domain = ""
545
+ for pattern, score in _PRIMARY_PLATFORM_PATTERNS.items():
546
+ if domain == pattern or domain.endswith("." + pattern):
547
+ if score > best_primary_score:
548
+ best_primary_score = score
549
+ best_primary_domain = pattern
550
+
551
+ if best_primary_score > 0:
552
+ # 检查是否有转载标记(即使是原帖平台,也可能是转载)
553
+ has_repost = any(marker in text for marker in _REPOST_MARKERS)
554
+ if has_repost:
555
+ return {
556
+ "score": 0.45,
557
+ "reason": f"原帖平台({best_primary_domain})但含转载标记",
558
+ "is_primary": False,
559
+ "platform_type": "repost_on_primary",
560
+ }
561
+ return {
562
+ "score": best_primary_score,
563
+ "reason": f"一手信源({best_primary_domain})",
564
+ "is_primary": True,
565
+ "platform_type": "primary",
566
+ }
567
+
568
+ # 3) 检查是否为原始媒体报道
569
+ best_media_score = 0.0
570
+ best_media_domain = ""
571
+ for pattern, score in _ORIGINAL_MEDIA_PATTERNS.items():
572
+ if domain == pattern or domain.endswith("." + pattern):
573
+ if score > best_media_score:
574
+ best_media_score = score
575
+ best_media_domain = pattern
576
+
577
+ if best_media_score > 0:
578
+ has_repost = any(marker in text for marker in _REPOST_MARKERS)
579
+ if has_repost:
580
+ return {
581
+ "score": 0.55,
582
+ "reason": f"媒体平台({best_media_domain})但含转载标记",
583
+ "is_primary": False,
584
+ "platform_type": "media_repost",
585
+ }
586
+ return {
587
+ "score": best_media_score,
588
+ "reason": f"原始媒体报道({best_media_domain})",
589
+ "is_primary": True,
590
+ "platform_type": "official_media",
591
+ }
592
+
593
+ # 4) 检查是否为二手/聚合平台
594
+ for pattern, score in _SECONDARY_PLATFORM_PATTERNS.items():
595
+ if domain == pattern or domain.endswith("." + pattern):
596
+ return {
597
+ "score": score,
598
+ "reason": f"二手/聚合平台({pattern})",
599
+ "is_primary": False,
600
+ "platform_type": "aggregator",
601
+ }
602
+
603
+ # 5) 检查 content_type 和 social_meta
604
+ if content_type == "social_post" or social_meta:
605
+ return {
606
+ "score": 0.75,
607
+ "reason": "社交帖子(content_type=social_post)",
608
+ "is_primary": True,
609
+ "platform_type": "social_post",
610
+ }
611
+
612
+ # 6) 检查转载标记
613
+ has_repost = any(marker in text for marker in _REPOST_MARKERS)
614
+ if has_repost:
615
+ return {
616
+ "score": 0.40,
617
+ "reason": "含转载/引用标记",
618
+ "is_primary": False,
619
+ "platform_type": "repost",
620
+ }
621
+
622
+ # 7) 无法判断(中性)
623
+ return {
624
+ "score": 0.50,
625
+ "reason": "无法判断可溯源性(默认中性)",
626
+ "is_primary": False,
627
+ "platform_type": "unknown",
628
+ }
629
+
630
+
280
631
  # ── 综合可信度 ────────────────────────────────────────────────────────────────
281
632
 
282
- def compute_credibility(results: list[dict[str, Any]], query: str) -> dict[str, Any]:
283
- """计算每个结果的综合可信度评分。"""
633
+ def compute_credibility(
634
+ results: list[dict[str, Any]],
635
+ query: str,
636
+ *,
637
+ high_stakes: bool = False,
638
+ ) -> dict[str, Any]:
639
+ """两阶段综合可信度:Selection × Absorption + Freshness。"""
284
640
  query_time = time.time()
285
- scored_results = []
641
+ scored_results: list[dict[str, Any]] = []
286
642
 
287
643
  for r in results:
288
644
  url = r.get("url", "")
289
645
  source = r.get("source", "")
646
+ title = r.get("title", "") or ""
647
+ snippet = r.get("snippet", "") or ""
290
648
 
291
649
  auth = score_authority(url, source)
292
650
  fresh = score_freshness(r, query_time)
651
+ density = score_evidence_density(snippet, title)
293
652
 
294
- # 综合公式:权威性 0.5 + 时效性 0.3 + 原始分数 0.2
295
- original_score = r.get("score", 0.5) or 0.5
653
+ # Selection:权威为主;SERP 直接压到低分
654
+ selection = auth["score"]
655
+ if auth.get("is_serp"):
656
+ selection = min(selection, 0.15)
657
+
658
+ # Absorption:证据密度
659
+ absorption = density["absorption_score"]
660
+
661
+ original_score = r.get("score")
662
+ if original_score is None:
663
+ original_score = 0.5
664
+ else:
665
+ try:
666
+ original_score = float(original_score)
667
+ except (TypeError, ValueError):
668
+ original_score = 0.5
669
+ # Traceability:可溯源性
670
+ trace = score_traceability(r)
671
+
672
+ # MECE 加权(量化)— 五维度
296
673
  credibility = (
297
- auth["score"] * 0.5 +
298
- fresh["score"] * 0.3 +
299
- original_score * 0.2
674
+ selection * 0.35
675
+ + absorption * 0.30
676
+ + fresh["score"] * 0.15
677
+ + float(original_score) * 0.10
678
+ + trace["score"] * 0.10
300
679
  )
301
680
 
302
681
  scored_results.append({
303
- "title": r.get("title", ""),
682
+ "title": title,
304
683
  "url": url,
305
684
  "source": source,
306
- "snippet": (r.get("snippet", "") or "")[:150],
685
+ "snippet": snippet[:180],
307
686
  "credibility": {
308
687
  "final": round(credibility, 3),
688
+ "selection": round(selection, 3),
689
+ "absorption": round(absorption, 3),
309
690
  "authority": auth,
310
691
  "freshness": fresh,
692
+ "evidence_density": density,
311
693
  "original_score": original_score,
694
+ "traceability": trace,
312
695
  },
313
696
  })
314
697
 
315
- # 交叉验证
316
698
  cross = cross_validate(results, query)
317
699
 
318
- # 排序
700
+ if high_stakes and scored_results:
701
+ # 共识调节:强佐证略抬高 top 可吸收源;弱共识不抬
702
+ boost = 0.0
703
+ if cross["corroboration_level"] == "strong":
704
+ boost = 0.05
705
+ elif cross["corroboration_level"] == "moderate":
706
+ boost = 0.02
707
+ for item in scored_results:
708
+ if not item["credibility"]["authority"].get("is_serp"):
709
+ item["credibility"]["final"] = round(
710
+ min(item["credibility"]["final"] + boost, 1.0), 3
711
+ )
712
+ item["credibility"]["consensus_boost"] = boost
713
+
319
714
  scored_results.sort(key=lambda x: x["credibility"]["final"], reverse=True)
320
715
 
321
716
  return {
322
717
  "query": query,
718
+ "framework": "selection_x_absorption_v2.3_traceability",
323
719
  "results": scored_results,
324
720
  "cross_validation": cross,
325
721
  "summary": {
326
722
  "total": len(scored_results),
327
- "high_credibility": sum(1 for r in scored_results if r["credibility"]["final"] >= 0.7),
328
- "medium_credibility": sum(1 for r in scored_results if 0.5 <= r["credibility"]["final"] < 0.7),
329
- "low_credibility": sum(1 for r in scored_results if r["credibility"]["final"] < 0.5),
723
+ "high_credibility": sum(
724
+ 1 for r in scored_results if r["credibility"]["final"] >= 0.7
725
+ ),
726
+ "medium_credibility": sum(
727
+ 1 for r in scored_results if 0.5 <= r["credibility"]["final"] < 0.7
728
+ ),
729
+ "low_credibility": sum(
730
+ 1 for r in scored_results if r["credibility"]["final"] < 0.5
731
+ ),
732
+ "serp_filtered": sum(
733
+ 1 for r in scored_results
734
+ if r["credibility"]["authority"].get("is_serp")
735
+ ),
330
736
  "best_source": scored_results[0] if scored_results else None,
331
737
  },
332
738
  }
@@ -335,36 +741,53 @@ def compute_credibility(results: list[dict[str, Any]], query: str) -> dict[str,
335
741
  # ── CLI ───────────────────────────────────────────────────────────────────────
336
742
 
337
743
  def main():
338
- parser = argparse.ArgumentParser(description="来源可信度评估工具")
744
+ ensure_utf8_stdio() # 入口第一件事:钉住 stdout/stderr 编码(见 cli_io)
745
+ parser = argparse.ArgumentParser(description="来源可信度评估工具(Selection×Absorption)")
339
746
  parser.add_argument("query", nargs="?", default="", help="搜索查询词")
340
747
  parser.add_argument("--stdin", action="store_true", help="从 stdin 读取 JSON 搜索结果")
341
748
  parser.add_argument("--json", action="store_true", help="JSON 输出")
749
+ parser.add_argument(
750
+ "--high-stakes",
751
+ action="store_true",
752
+ help="高后果模式:启用共识微调",
753
+ )
342
754
  args = parser.parse_args()
343
755
 
344
756
  if args.stdin:
345
757
  data = json.load(sys.stdin)
346
758
  results = data.get("results", [])
347
759
  else:
348
- # 从搜索结果文件读取
349
760
  results = []
350
761
 
351
762
  if not results and args.query:
352
- # 无输入结果时,输出提示
353
763
  print("需要提供搜索结果进行评估。用法:")
354
764
  print(' echo \'{"results": [...]}\' | python3 evidence.py --stdin --query "查询词"')
355
765
  sys.exit(1)
356
766
 
357
- report = compute_credibility(results, args.query)
767
+ report = compute_credibility(
768
+ results, args.query, high_stakes=args.high_stakes
769
+ )
358
770
 
359
771
  if args.json:
360
- print(json.dumps(report, ensure_ascii=False, indent=2))
772
+ print(dumps(report))
361
773
  else:
362
- print(f"\n来源可信度评估:{report['query']}")
774
+ print(f"\n来源可信度评估:{report['query']} [{report['framework']}]")
363
775
  print(f"{'='*50}")
364
776
  for r in report["results"]:
365
777
  c = r["credibility"]
366
778
  level = "🟢" if c["final"] >= 0.7 else "🟡" if c["final"] >= 0.5 else "🔴"
367
- print(f"{level} [{c['final']:.2f}] {r['title'][:50]}")
779
+ ed = c.get("evidence_density") or {}
780
+ flags = []
781
+ if ed.get("has_numbers"):
782
+ flags.append("数")
783
+ if ed.get("has_comparison"):
784
+ flags.append("比")
785
+ if ed.get("has_definition"):
786
+ flags.append("义")
787
+ if c["authority"].get("is_serp"):
788
+ flags.append("SERP")
789
+ flag_s = ",".join(flags) if flags else "-"
790
+ print(f"{level} [{c['final']:.2f}] sel={c['selection']:.2f} abs={c['absorption']:.2f} [{flag_s}] {r['title'][:48]}")
368
791
  print(f" 权威:{c['authority']['score']:.2f} ({c['authority']['reason']})")
369
792
  print(f" 时效:{c['freshness']['score']:.2f} ({c['freshness']['reason']})")
370
793
  print()
@@ -374,7 +797,10 @@ def main():
374
797
  print(f" {cv['detail']}")
375
798
 
376
799
  s = report["summary"]
377
- print(f"\n总结:🟢高={s['high_credibility']} 🟡中={s['medium_credibility']} 🔴低={s['low_credibility']}")
800
+ print(
801
+ f"\n总结:🟢高={s['high_credibility']} 🟡中={s['medium_credibility']} "
802
+ f"🔴低={s['low_credibility']} SERP={s.get('serp_filtered', 0)}"
803
+ )
378
804
 
379
805
 
380
806
  if __name__ == "__main__":