argo-search 1.0.1 → 2.9.8

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 (472) hide show
  1. package/LICENSE +201 -21
  2. package/README.en.md +735 -0
  3. package/README.es.md +777 -0
  4. package/README.ja.md +777 -0
  5. package/README.ko.md +777 -0
  6. package/README.md +778 -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/why-better.svg +104 -0
  12. package/assets/readme/workflow.svg +83 -0
  13. package/backends/domain_profiles.json +2356 -96
  14. package/backends/engine_registry.yaml +3428 -503
  15. package/backends/query_synonyms_cn.json +35 -0
  16. package/backends/quota_profiles.json +2488 -160
  17. package/backends/source_types_cn.json +109 -0
  18. package/backends/text_lexicon.json +44 -0
  19. package/bin/argo +772 -0
  20. package/bin/argo.js +102 -39
  21. package/config.yaml +5139 -367
  22. package/cordis.patch.yml +21 -0
  23. package/data/countries.json +40 -0
  24. package/data/lang_matrix/lang_capability.json +842 -0
  25. package/data/lang_matrix/matrix_2026-09-10.json +11921 -0
  26. package/data/regions_cn.json +1 -0
  27. package/docs/ADDING_NEW_ENGINE.md +184 -0
  28. package/docs/ARGO_INTRO.md +124 -0
  29. package/docs/DATA_SOURCE_GAPS.md +81 -0
  30. package/docs/DESIGN_LOCAL_DATA_FUSION.md +90 -0
  31. package/docs/ENGINE_CATALOG.md +575 -0
  32. package/docs/MCP_SETUP.md +108 -0
  33. package/docs/PLATFORM_PATHS.md +111 -0
  34. package/docs/RELEASE_NOTES_v2.8.0.md +71 -0
  35. package/docs/RELEASE_NOTES_v2.8.2.md +63 -0
  36. package/docs/RELEASE_NOTES_v2.8.3.md +55 -0
  37. package/docs/RELEASE_NOTES_v2.8.4.md +79 -0
  38. package/docs/RELEASE_NOTES_v2.8.5.md +102 -0
  39. package/docs/RELEASE_NOTES_v2.8.6.md +66 -0
  40. package/docs/RELEASE_NOTES_v2.8.7.md +65 -0
  41. package/docs/RELEASE_NOTES_v2.8.8.md +67 -0
  42. package/docs/RELEASE_NOTES_v2.8.9.md +67 -0
  43. package/docs/RELEASE_NOTES_v2.9.0.md +64 -0
  44. package/docs/RELEASE_NOTES_v2.9.8.md +73 -0
  45. package/docs/SEARCH_ARCHIVE.md +115 -0
  46. package/docs/assets/architecture.svg +104 -0
  47. package/docs/assets/hero.svg +98 -0
  48. package/docs/engines/bangumi.md +27 -0
  49. package/docs/engines/biorxiv.md +27 -0
  50. package/docs/engines/clawhub.md +27 -0
  51. package/docs/engines/crt_sh.md +27 -0
  52. package/docs/engines/datacite.md +27 -0
  53. package/docs/engines/deps_dev.md +27 -0
  54. package/docs/engines/douban_movie.md +27 -0
  55. package/docs/engines/endoflife.md +27 -0
  56. package/docs/engines/flk_law.md +27 -0
  57. package/docs/engines/fxtwitter.md +27 -0
  58. package/docs/engines/google_news.md +27 -0
  59. package/docs/engines/gov_regulations.md +27 -0
  60. package/docs/engines/guqian.md +27 -0
  61. package/docs/engines/hackernews.md +27 -0
  62. package/docs/engines/iplant.md +27 -0
  63. package/docs/engines/keenable.md +27 -0
  64. package/docs/engines/met_museum.md +27 -0
  65. package/docs/engines/nasa_images.md +27 -0
  66. package/docs/engines/netease_music.md +27 -0
  67. package/docs/engines/npm.md +27 -0
  68. package/docs/engines/nvd.md +27 -0
  69. package/docs/engines/openreview.md +27 -0
  70. package/docs/engines/openstd.md +27 -0
  71. package/docs/engines/parallel_free.md +27 -0
  72. package/docs/engines/people_daily.md +27 -0
  73. package/docs/engines/realtime_index.md +27 -0
  74. package/docs/engines/sec_edgar.md +27 -0
  75. package/docs/engines/seltz.md +27 -0
  76. package/docs/engines/skillsmp.md +27 -0
  77. package/docs/engines/sspai.md +27 -0
  78. package/docs/engines/std_samr.md +27 -0
  79. package/docs/engines/train.md +27 -0
  80. package/docs/engines/un_comtrade.md +27 -0
  81. package/docs/engines/wikisource.md +27 -0
  82. package/docs/engines/zdic.md +27 -0
  83. package/docs/engines/zenodo.md +27 -0
  84. package/docs/engines/zhihu_hot_app.md +27 -0
  85. package/docs//344/270/272/344/273/200/344/271/210/351/200/211/346/213/251argo.md +176 -0
  86. package/engines/README.md +10 -0
  87. package/engines/_template_cli.yaml +39 -0
  88. package/engines/_template_http.yaml +37 -0
  89. package/engines/plugins/_template_plugin.py +50 -0
  90. package/engines/specs/_cisa_kev.yaml +54 -0
  91. package/engines/specs/_osv.yaml +63 -0
  92. package/engines/specs/aljazeera.yaml +43 -0
  93. package/engines/specs/antara.yaml +43 -0
  94. package/engines/specs/artic.yaml +20 -0
  95. package/engines/specs/aviation_weather.yaml +39 -0
  96. package/engines/specs/bangumi.yaml +21 -0
  97. package/engines/specs/biorxiv.yaml +24 -0
  98. package/engines/specs/carbon_intensity.yaml +32 -0
  99. package/engines/specs/cfets.yaml +41 -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/defillama.yaml +38 -0
  113. package/engines/specs/deps_dev.yaml +21 -0
  114. package/engines/specs/digup.yaml +36 -0
  115. package/engines/specs/doi.yaml +39 -0
  116. package/engines/specs/douban_movie.yaml +22 -0
  117. package/engines/specs/dw_news.yaml +47 -0
  118. package/engines/specs/egov_law.yaml +21 -0
  119. package/engines/specs/elpais.yaml +43 -0
  120. package/engines/specs/endoflife.yaml +21 -0
  121. package/engines/specs/energy_charts.yaml +21 -0
  122. package/engines/specs/factcheck_org.yaml +52 -0
  123. package/engines/specs/faz.yaml +43 -0
  124. package/engines/specs/federal_register.yaml +57 -0
  125. package/engines/specs/firecrawl.yaml +53 -0
  126. package/engines/specs/flk_law.yaml +24 -0
  127. package/engines/specs/folha.yaml +43 -0
  128. package/engines/specs/france24.yaml +44 -0
  129. package/engines/specs/full_fact.yaml +47 -0
  130. package/engines/specs/fxtwitter.yaml +36 -0
  131. package/engines/specs/gbfs_nyc.yaml +32 -0
  132. package/engines/specs/gdacs.yaml +21 -0
  133. package/engines/specs/gdelt.yaml +64 -0
  134. package/engines/specs/gleif.yaml +55 -0
  135. package/engines/specs/google_news.yaml +22 -0
  136. package/engines/specs/gov_regulations.yaml +38 -0
  137. package/engines/specs/guardian_rss.yaml +59 -0
  138. package/engines/specs/hkex.yaml +40 -0
  139. package/engines/specs/hurriyet.yaml +43 -0
  140. package/engines/specs/iplant.yaml +25 -0
  141. package/engines/specs/jikan.yaml +24 -0
  142. package/engines/specs/jolpica.yaml +20 -0
  143. package/engines/specs/k10plus.yaml +20 -0
  144. package/engines/specs/keenable.yaml +44 -0
  145. package/engines/specs/keenable_free.yaml +36 -0
  146. package/engines/specs/lefigaro.yaml +43 -0
  147. package/engines/specs/listenbrainz.yaml +19 -0
  148. package/engines/specs/matichon.yaml +43 -0
  149. package/engines/specs/met_museum.yaml +21 -0
  150. package/engines/specs/nasa_images.yaml +45 -0
  151. package/engines/specs/netease_music.yaml +38 -0
  152. package/engines/specs/nhk.yaml +43 -0
  153. package/engines/specs/nhtsa_vpic.yaml +20 -0
  154. package/engines/specs/noaa_swpc.yaml +20 -0
  155. package/engines/specs/nvd.yaml +38 -0
  156. package/engines/specs/obis.yaml +20 -0
  157. package/engines/specs/opencitations.yaml +52 -0
  158. package/engines/specs/openf1.yaml +21 -0
  159. package/engines/specs/openfoodfacts.yaml +45 -0
  160. package/engines/specs/openligadb.yaml +20 -0
  161. package/engines/specs/openreview.yaml +37 -0
  162. package/engines/specs/openstd.yaml +21 -0
  163. package/engines/specs/people_daily.yaml +22 -0
  164. package/engines/specs/realtime_index.yaml +34 -0
  165. package/engines/specs/ror.yaml +20 -0
  166. package/engines/specs/satnogs.yaml +20 -0
  167. package/engines/specs/sec_edgar.yaml +46 -0
  168. package/engines/specs/shfe.yaml +39 -0
  169. package/engines/specs/skillsmp.yaml +50 -0
  170. package/engines/specs/sogou_weixin_browser.yaml +30 -0
  171. package/engines/specs/soilgrids.yaml +25 -0
  172. package/engines/specs/sspai.yaml +36 -0
  173. package/engines/specs/stackexchange.yaml +46 -0
  174. package/engines/specs/std_samr.yaml +21 -0
  175. package/engines/specs/tass.yaml +43 -0
  176. package/engines/specs/tavily_free.yaml +56 -0
  177. package/engines/specs/tle_mirror.yaml +21 -0
  178. package/engines/specs/train.yaml +37 -0
  179. package/engines/specs/tvmaze.yaml +20 -0
  180. package/engines/specs/twitter_syndication.yaml +31 -0
  181. package/engines/specs/un_comtrade.yaml +23 -0
  182. package/engines/specs/unpaywall.yaml +54 -0
  183. package/engines/specs/vnpress.yaml +43 -0
  184. package/engines/specs/weather.yaml +32 -0
  185. package/engines/specs/who_don.yaml +21 -0
  186. package/engines/specs/who_gho.yaml +20 -0
  187. package/engines/specs/wikimedia_commons.yaml +34 -0
  188. package/engines/specs/wikisource.yaml +22 -0
  189. package/engines/specs/worms.yaml +19 -0
  190. package/engines/specs/x_trending.yaml +34 -0
  191. package/engines/specs/yna.yaml +43 -0
  192. package/engines/specs/ynet.yaml +43 -0
  193. package/engines/specs/yuanbao.yaml +93 -0
  194. package/engines/specs/zbmath.yaml +40 -0
  195. package/engines/specs/zdic.yaml +22 -0
  196. package/engines/specs/zenodo.yaml +27 -0
  197. package/engines/specs/zhihu_hot_app.yaml +35 -0
  198. package/mcp/clients.yaml +106 -0
  199. package/package.json +55 -12
  200. package/packages/dsh-plugin/README.md +152 -0
  201. package/packages/dsh-plugin/cordis.patch.yml +48 -0
  202. package/packages/dsh-plugin/dsh/client.js +1533 -0
  203. package/packages/dsh-plugin/dsh/diagnostics.js +54 -0
  204. package/packages/dsh-plugin/dsh/index.js +527 -0
  205. package/packages/dsh-plugin/dsh/native-tools.mjs +749 -0
  206. package/packages/dsh-plugin/dsh/runner.js +666 -0
  207. package/packages/dsh-plugin/dsh/wide-research.js +878 -0
  208. package/packages/dsh-plugin/package.json +38 -0
  209. package/packages/dsh-plugin/scripts/sync-local.sh +70 -0
  210. package/packages/dsh-plugin/test/client.test.js +406 -0
  211. package/packages/dsh-plugin/test/config.test.js +30 -0
  212. package/packages/dsh-plugin/test/diagnostics.test.js +28 -0
  213. package/packages/dsh-plugin/test/index.test.js +1382 -0
  214. package/references/query-templates.md +96 -0
  215. package/references/query-templates.yaml +118 -0
  216. package/scripts/ab_eval.py +219 -0
  217. package/scripts/ab_eval_p0p1.py +266 -0
  218. package/scripts/adaptive.py +294 -70
  219. package/scripts/answer.py +154 -0
  220. package/scripts/archive_run.py +591 -0
  221. package/scripts/argo_engine_registry.py +172 -0
  222. package/scripts/argo_paths.py +728 -0
  223. package/scripts/article.py +136 -0
  224. package/scripts/article_meta.py +131 -0
  225. package/scripts/arxiv_source.py +201 -0
  226. package/scripts/badcase_ledger.py +252 -0
  227. package/scripts/batch_probe.py +241 -0
  228. package/scripts/benchmark.py +19 -0
  229. package/scripts/bounded_run.py +144 -0
  230. package/scripts/browser_auth.py +323 -0
  231. package/scripts/budget.py +247 -0
  232. package/scripts/budget_cli.py +93 -0
  233. package/scripts/build_release_assets.sh +65 -0
  234. package/scripts/burstiness_detector.py +147 -0
  235. package/scripts/cache.py +1066 -67
  236. package/scripts/cache_guard.py +271 -0
  237. package/scripts/cache_key_vdom.py +56 -0
  238. package/scripts/cache_similarity.py +170 -0
  239. package/scripts/caliber_isolation.py +195 -0
  240. package/scripts/calibrate_lowquality.py +210 -0
  241. package/scripts/candidate_envelope.py +366 -0
  242. package/scripts/chrome_cdp.py +871 -0
  243. package/scripts/ci_drift_gate.py +90 -0
  244. package/scripts/circuit_breaker.py +389 -0
  245. package/scripts/citations.py +334 -0
  246. package/scripts/cite_cli.py +59 -0
  247. package/scripts/cjk_tokens.py +95 -0
  248. package/scripts/clarify.py +47 -24
  249. package/scripts/cli_io.py +240 -0
  250. package/scripts/config.py +683 -45
  251. package/scripts/config_cache.py +402 -0
  252. package/scripts/conn_pool.py +220 -0
  253. package/scripts/content_boundary.py +46 -0
  254. package/scripts/content_security.py +685 -0
  255. package/scripts/content_signals.py +751 -0
  256. package/scripts/crawl.py +74 -35
  257. package/scripts/cross_lingual_trace.py +293 -0
  258. package/scripts/daemon.py +866 -0
  259. package/scripts/digup_bridge.py +214 -0
  260. package/scripts/dispatch_result.py +98 -0
  261. package/scripts/engine_admission.py +366 -0
  262. package/scripts/engine_dispatch.py +982 -0
  263. package/scripts/engine_env.py +539 -0
  264. package/scripts/engine_failure.py +411 -0
  265. package/scripts/engine_families.py +738 -0
  266. package/scripts/engine_policy.py +218 -0
  267. package/scripts/engine_requires.py +190 -0
  268. package/scripts/engine_status.py +322 -0
  269. package/scripts/engine_validate.py +638 -0
  270. package/scripts/engines.py +799 -294
  271. package/scripts/engines_base.py +1521 -0
  272. package/scripts/engines_builders.py +310 -0
  273. package/scripts/engines_builders_batch10.py +204 -0
  274. package/scripts/engines_builders_batch11.py +174 -0
  275. package/scripts/engines_builders_batch12.py +103 -0
  276. package/scripts/engines_builders_batch13.py +340 -0
  277. package/scripts/engines_builders_batch14.py +656 -0
  278. package/scripts/engines_builders_batch15.py +249 -0
  279. package/scripts/engines_builders_batch9.py +1468 -0
  280. package/scripts/engines_builders_cn.py +2283 -0
  281. package/scripts/engines_builders_data.py +2923 -0
  282. package/scripts/engines_builders_data_macro.py +775 -0
  283. package/scripts/engines_builders_feeds.py +255 -0
  284. package/scripts/engines_builders_intl.py +1239 -0
  285. package/scripts/engines_builders_keyless.py +111 -0
  286. package/scripts/engines_builders_search.py +440 -0
  287. package/scripts/engines_builders_tech.py +1359 -0
  288. package/scripts/evidence.py +620 -166
  289. package/scripts/evidence_loop.py +624 -0
  290. package/scripts/evidence_tier.py +298 -0
  291. package/scripts/except_sets.py +25 -0
  292. package/scripts/extract.py +55 -16
  293. package/scripts/fact_align.py +207 -0
  294. package/scripts/fact_crosscheck.py +230 -0
  295. package/scripts/fetch.py +25 -12
  296. package/scripts/fetch_browser.py +87 -0
  297. package/scripts/fetch_jsrun.py +122 -0
  298. package/scripts/fetch_quality.py +225 -0
  299. package/scripts/fetch_render_tinyfish.py +117 -0
  300. package/scripts/fetch_v3.py +1804 -0
  301. package/scripts/focus_extract.py +350 -0
  302. package/scripts/fulltext_store.py +169 -0
  303. package/scripts/fxtwitter_api.py +325 -0
  304. package/scripts/gen_engine_catalog.py +364 -0
  305. package/scripts/gen_native_tools.py +91 -0
  306. package/scripts/gen_routing_snapshot.py +195 -0
  307. package/scripts/health_check.py +140 -402
  308. package/scripts/health_probe.py +23 -8
  309. package/scripts/hot_state.py +134 -0
  310. package/scripts/http_client.py +1053 -0
  311. package/scripts/image_ops.py +430 -0
  312. package/scripts/image_vision/vision_probe.swift +126 -0
  313. package/scripts/install.ps1 +166 -0
  314. package/scripts/install.sh +221 -0
  315. package/scripts/job.py +1186 -0
  316. package/scripts/jump_resolver.py +235 -0
  317. package/scripts/lang_capability.py +150 -0
  318. package/scripts/lang_detect.py +289 -0
  319. package/scripts/lang_matrix_probe.py +329 -0
  320. package/scripts/lang_pref.py +311 -0
  321. package/scripts/link_source.py +340 -0
  322. package/scripts/local_image.py +970 -0
  323. package/scripts/local_seek.py +402 -0
  324. package/scripts/macro_countries.py +97 -0
  325. package/scripts/matrix_search_eval.py +1187 -0
  326. package/scripts/mcp_compact.py +234 -0
  327. package/scripts/mcp_diag.py +8 -3
  328. package/scripts/mcp_handlers.py +492 -0
  329. package/scripts/mcp_handlers_surface.py +157 -0
  330. package/scripts/mcp_launch.sh +124 -0
  331. package/scripts/mcp_local.py +241 -0
  332. package/scripts/mcp_server.py +125 -419
  333. package/scripts/mcp_setup.py +431 -0
  334. package/scripts/mcp_tools.py +331 -0
  335. package/scripts/mcp_transport.py +191 -0
  336. package/scripts/mcp_warm.py +127 -0
  337. package/scripts/min_version_scan.py +335 -0
  338. package/scripts/net_proxy.py +258 -0
  339. package/scripts/network_aware.py +152 -0
  340. package/scripts/output_transparency.py +200 -0
  341. package/scripts/paid_policy.py +98 -0
  342. package/scripts/paper.py +274 -0
  343. package/scripts/pdf_extract.py +518 -0
  344. package/scripts/phrase_filter.py +66 -0
  345. package/scripts/plan.py +422 -0
  346. package/scripts/postprocess_signals.py +150 -0
  347. package/scripts/pre-commit +48 -0
  348. package/scripts/query_enhance.py +157 -0
  349. package/scripts/query_operators.py +196 -0
  350. package/scripts/query_rewriter.py +116 -7
  351. package/scripts/query_signals.py +300 -0
  352. package/scripts/query_templates.py +167 -0
  353. package/scripts/query_understanding.py +411 -0
  354. package/scripts/query_variants.py +298 -0
  355. package/scripts/quota.py +633 -76
  356. package/scripts/rank_signals.py +775 -0
  357. package/scripts/ranking_eval.py +319 -0
  358. package/scripts/readability_extract.py +433 -0
  359. package/scripts/recompute.py +415 -0
  360. package/scripts/recovery.py +616 -0
  361. package/scripts/recovery_l0.py +160 -0
  362. package/scripts/recovery_runner.py +150 -0
  363. package/scripts/redskill/redskill_engine.py +332 -0
  364. package/scripts/regression_p0p1.py +318 -0
  365. package/scripts/replay_eval.py +658 -0
  366. package/scripts/rerank_tfidf.py +225 -0
  367. package/scripts/research.py +607 -403
  368. package/scripts/research_cli.py +361 -0
  369. package/scripts/research_dossier.py +492 -0
  370. package/scripts/research_expand.py +169 -0
  371. package/scripts/research_gates.py +272 -0
  372. package/scripts/research_report.py +209 -0
  373. package/scripts/research_strategy.py +37 -0
  374. package/scripts/research_work_packages.py +195 -0
  375. package/scripts/result_lang.py +311 -0
  376. package/scripts/retry_policy.py +76 -0
  377. package/scripts/robots_guard.py +259 -0
  378. package/scripts/route.py +921 -254
  379. package/scripts/route_bangs.py +40 -0
  380. package/scripts/route_cache.py +243 -0
  381. package/scripts/route_combo.py +917 -0
  382. package/scripts/route_domains.py +341 -0
  383. package/scripts/route_lang.py +605 -0
  384. package/scripts/route_log.py +63 -0
  385. package/scripts/route_policy.py +250 -0
  386. package/scripts/rrf_classic.py +66 -0
  387. package/scripts/screenshot.py +73 -0
  388. package/scripts/search.py +886 -406
  389. package/scripts/search_attribution.py +185 -0
  390. package/scripts/search_benchmark.py +407 -0
  391. package/scripts/search_cli.py +640 -0
  392. package/scripts/search_entry.py +395 -0
  393. package/scripts/search_output.py +690 -0
  394. package/scripts/search_pipeline.py +926 -0
  395. package/scripts/search_rank.py +991 -0
  396. package/scripts/search_types.py +129 -16
  397. package/scripts/seek_locator.py +68 -0
  398. package/scripts/semantic_evidence.py +391 -0
  399. package/scripts/serp_guard.py +336 -0
  400. package/scripts/serp_kill_log.py +74 -0
  401. package/scripts/single_flight.py +120 -0
  402. package/scripts/social_engines/__init__.py +10 -0
  403. package/scripts/social_engines/bilibili_engine.py +131 -0
  404. package/scripts/social_engines/hackernews_engine.py +113 -0
  405. package/scripts/social_engines/reddit_engine.py +172 -0
  406. package/scripts/social_engines/twitter_engine.py +397 -0
  407. package/scripts/social_engines/v2ex_engine.py +101 -0
  408. package/scripts/social_engines/weibo_engine.py +128 -0
  409. package/scripts/social_engines/xiaohongshu_engine.py +137 -0
  410. package/scripts/social_engines/zhihu_engine.py +139 -0
  411. package/scripts/social_research.py +201 -0
  412. package/scripts/sogou_weixin_browser.py +318 -0
  413. package/scripts/source_ledger.py +249 -0
  414. package/scripts/stage_timing.py +58 -0
  415. package/scripts/stats_cli.py +107 -0
  416. package/scripts/straggler_policy.py +60 -0
  417. package/scripts/stylometry_detector.py +231 -0
  418. package/scripts/sync_backends.py +353 -0
  419. package/scripts/sync_installs.py +29 -0
  420. package/scripts/template_cli.py +127 -0
  421. package/scripts/text_lexicon.py +72 -0
  422. package/scripts/tfidf_router.py +109 -37
  423. package/scripts/time_utils.py +233 -0
  424. package/scripts/topic_research_profiles.py +643 -0
  425. package/scripts/train.py +737 -0
  426. package/scripts/tweet.py +596 -0
  427. package/scripts/url_canon.py +217 -0
  428. package/scripts/url_optimize.py +56 -0
  429. package/scripts/url_safety.py +236 -0
  430. package/scripts/usage_log.py +116 -0
  431. package/scripts/usage_log_clean.py +113 -0
  432. package/scripts/v2ex_nodes.py +311 -0
  433. package/scripts/variant_recall.py +188 -0
  434. package/scripts/watch.py +205 -0
  435. package/scripts/wechat_article.py +258 -0
  436. package/scripts/wide_research.py +447 -0
  437. package/scripts/wx.py +349 -0
  438. package/scripts/x_article_fetch.py +141 -0
  439. package/scripts/x_search.py +234 -0
  440. package/scripts/x_trending.py +189 -0
  441. package/scripts/yaml_load.py +67 -0
  442. package/scripts/zhihu_quota.py +139 -0
  443. package/sub-skills/ego-search/SKILL.md +253 -0
  444. package/sub-skills/ego-search/scripts/ego_search.py +800 -0
  445. package/sub-skills/ego-search/scripts/merge.py +226 -0
  446. package/sub-skills/ego-search/scripts/quality.py +64 -0
  447. package/sub-skills/ego-search/scripts/runtime.py +172 -0
  448. package/sub-skills/ego-search/scripts/safety.py +109 -0
  449. package/sub-skills/ego-search/scripts/serp_spec.py +109 -0
  450. package/sub-skills/ego-search/scripts/webbridge_adapter.py +374 -0
  451. package/sub-skills/js-run/SKILL.md +67 -0
  452. package/sub-skills/js-run/scripts/jsrun.py +112 -0
  453. package/sub-skills/js-run/scripts/shim.js +188 -0
  454. package/sub-skills/local-search/SKILL.md +50 -53
  455. package/sub-skills/local-search/bm25.py +186 -0
  456. package/sub-skills/local-search/cli_runner.py +331 -0
  457. package/sub-skills/local-search/config.yaml +152 -55
  458. package/sub-skills/local-search/engine_registry.py +91 -26
  459. package/sub-skills/local-search/{health_check.py → local_health_check.py} +140 -10
  460. package/sub-skills/local-search/local_search_adapter.py +13 -2
  461. package/sub-skills/local-search/parse_maps.yaml +40 -42
  462. package/sub-skills/local-search/parsers.py +328 -0
  463. package/sub-skills/local-search/search_v3.py +716 -242
  464. package/sub-skills/local-search/smart_router.py +47 -7
  465. package/sub-skills/local-seek/SKILL.md +110 -0
  466. package/sub-skills/local-seek/config/domains.yaml +20 -0
  467. package/sub-skills/local-seek/scripts/eval_rank.py +216 -0
  468. package/sub-skills/local-seek/scripts/eval_seek.py +224 -0
  469. package/sub-skills/local-seek/scripts/seek.py +1465 -0
  470. package/sub-skills/local-seek/scripts/seek_frecency.py +126 -0
  471. package/sub-skills/local-seek/scripts/seek_fts.py +430 -0
  472. package/sub-skills/local-seek/scripts/seek_rank.py +197 -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,149 +102,294 @@ 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
+ )
127
237
 
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"
238
+ return (best_score, best_reason, tier, domain, bool(is_serp))
239
+
240
+
241
+ def score_authority(url: str, source: str = "") -> dict[str, Any]:
242
+ """评估 URL 的权威性(Selection 门槛)。
129
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
  # ── 交叉验证 ──────────────────────────────────────────────────────────────────
220
372
 
373
+ # 单条结果至少覆盖 20% 的 query 单元才算「与查询相关」。与 serp_guard 的
374
+ # SERP 覆盖率门同量级,但两处判据独立:这里回答「这批结果里几条在答这题」,
375
+ # serp_guard 回答「这一页 SERP 是不是整体答非所问」。
376
+ _RELEVANCE_MIN_COVERAGE = 0.2
377
+
378
+
379
+ def _relevance_units(text: str) -> set[str]:
380
+ """查询/结果 → 相关性比对单元(CJK 二元组 + 小写拉丁数字词)。
381
+
382
+ 复用 rank_signals.relevance_units,避免再维护第三套分词口径。
383
+ import 放在函数内:evidence 不是搜索热路径,但 rank_signals 较重,
384
+ 模块级导入会让所有 evidence 调用方一起付启动成本。
385
+ """
386
+ try:
387
+ from rank_signals import relevance_units
388
+ return set(relevance_units(text or ""))
389
+ except ImportError:
390
+ return {(text or "").lower()}
391
+
392
+
221
393
  def cross_validate(results: list[dict[str, Any]], query: str) -> dict[str, Any]:
222
394
  """多来源交叉验证。"""
223
395
  if len(results) < 2:
@@ -227,106 +399,368 @@ def cross_validate(results: list[dict[str, Any]], query: str) -> dict[str, Any]:
227
399
  "detail": f"仅 {len(results)} 个结果,无法交叉验证",
228
400
  "agreement_count": 0,
229
401
  "total_sources": len(results),
402
+ "unique_domains": 0,
403
+ "url_overlap": 0.0,
404
+ "content_domains": 0,
230
405
  }
231
406
 
232
- # 检查 URL 重叠(不同来源指向同一页面 = 强佐证)
233
407
  urls = [r.get("url", "") for r in results if r.get("url")]
234
408
  unique_urls = set(urls)
235
409
  url_overlap = 1 - (len(unique_urls) / max(len(urls), 1))
236
410
 
237
- # 检查域名多样性
238
411
  domains = set()
412
+ content_domains = set()
239
413
  for r in results:
240
414
  url = r.get("url", "")
241
- if url:
242
- domain = urlparse(url).netloc.lower().strip("www.")
243
- domains.add(domain)
244
-
245
- # 检查标题/内容相似度(简单关键词匹配)
246
- query_words = set(query.lower().split())
415
+ if not url:
416
+ continue
417
+ domain = _normalize_domain(url)
418
+ domains.add(domain)
419
+ if not is_serp_or_jump_url(url):
420
+ content_domains.add(domain)
421
+
422
+ # 旧口径 `any(w in text for w in query.lower().split())` 在中文查询上
423
+ # 两个方向都错:无空格中文被当成一个整词,结果里永远不会出现整句,
424
+ # 真实相关也报 0/N;反过来,结果只要带「2026」这类年份就被算作相关,
425
+ # 年份表垃圾页也能过。改用 CJK 二元组覆盖率(issue #18 的「0/N」信号
426
+ # 只有在这个口径下才可信)。
427
+ q_units = _relevance_units(query)
247
428
  content_matches = 0
248
429
  for r in results:
249
- text = f"{r.get('title', '')} {r.get('snippet', '')}".lower()
250
- if any(w in text for w in query_words if len(w) > 1):
430
+ text = f"{r.get('title', '')} {r.get('snippet', '')}"
431
+ if not q_units:
432
+ continue
433
+ hit = q_units & _relevance_units(text)
434
+ if len(hit) / len(q_units) >= _RELEVANCE_MIN_COVERAGE:
251
435
  content_matches += 1
252
436
 
253
437
  match_ratio = content_matches / max(len(results), 1)
438
+ n_content = len(content_domains)
254
439
 
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
440
+ if match_ratio >= 0.8 and n_content >= 3:
441
+ level, score = "strong", 0.9
442
+ elif match_ratio >= 0.6 and n_content >= 2:
443
+ level, score = "moderate", 0.7
262
444
  elif match_ratio >= 0.4:
263
- level = "weak"
264
- score = 0.5
445
+ level, score = "weak", 0.5
265
446
  else:
266
- level = "minimal"
267
- score = 0.3
447
+ level, score = "minimal", 0.3
268
448
 
269
449
  return {
270
450
  "corroboration_level": level,
271
451
  "score": round(score, 2),
272
- "detail": f"{content_matches}/{len(results)} 个结果与查询相关,{len(domains)} 个独立域名",
452
+ "detail": (
453
+ f"{content_matches}/{len(results)} 与查询相关,"
454
+ f"{n_content} 个可吸收域名(总域名 {len(domains)})"
455
+ ),
273
456
  "agreement_count": content_matches,
274
457
  "total_sources": len(results),
275
458
  "unique_domains": len(domains),
459
+ "content_domains": n_content,
276
460
  "url_overlap": round(url_overlap, 2),
277
461
  }
278
462
 
279
463
 
464
+ # ── 可溯源性评分 ──────────────────────────────────────────────────────────────
465
+
466
+ # 一手信源平台特征(用户直接发布的原始内容)
467
+ _PRIMARY_PLATFORM_PATTERNS = {
468
+ "weibo.com": 0.95, # 微博原帖
469
+ "zhihu.com": 0.90, # 知乎回答/文章
470
+ "github.com": 0.90, # GitHub issue/PR/commit
471
+ "stackoverflow.com": 0.90,
472
+ "xiaohongshu.com": 0.85, # 小红书笔记
473
+ "bilibili.com": 0.85, # B站视频/动态
474
+ "twitter.com": 0.85, # X/Twitter 原帖
475
+ "x.com": 0.85,
476
+ "youtube.com": 0.85, # YouTube 视频
477
+ "reddit.com": 0.80, # Reddit 原帖
478
+ "medium.com": 0.80, # Medium 原文
479
+ "dev.to": 0.80,
480
+ "juejin.cn": 0.75, # 掘金原文
481
+ "cnblogs.com": 0.75, # 博客园原文
482
+ }
483
+
484
+ # 原始媒体报道(新闻机构原创报道)
485
+ _ORIGINAL_MEDIA_PATTERNS = {
486
+ "xinhuanet.com": 0.95, # 新华社
487
+ "people.com.cn": 0.95, # 人民网
488
+ "reuters.com": 0.95, # 路透社
489
+ "bloomberg.com": 0.95, # 彭博社
490
+ "wsj.com": 0.95, # 华尔街日报
491
+ "caixin.com": 0.90, # 财新
492
+ "yicai.com": 0.88, # 第一财经
493
+ "wallstreetcn.com": 0.88,# 华尔街见闻
494
+ "cls.cn": 0.88, # 财联社
495
+ "cs.com.cn": 0.90, # 中证网
496
+ "stcn.com": 0.85, # 证券时报
497
+ "cnr.cn": 0.88, # 央广网
498
+ "thepaper.cn": 0.82, # 澎湃新闻
499
+ "jiemian.com": 0.80, # 界面新闻
500
+ "36kr.com": 0.75, # 36氪
501
+ "eastmoney.com": 0.80, # 东方财富
502
+ "163.com": 0.70, # 网易新闻
503
+ "sohu.com": 0.70, # 搜狐新闻
504
+ "sina.com.cn": 0.70, # 新浪新闻
505
+ }
506
+
507
+ # 二手/聚合/洗稿平台特征
508
+ _SECONDARY_PLATFORM_PATTERNS = {
509
+ "baijiahao.baidu.com": 0.35, # 百家号(自媒体洗稿重灾区)
510
+ "toutiao.com": 0.35, # 头条号(聚合+洗稿)
511
+ "weixin.qq.com": 0.40, # 微信公众号(质量参差不齐)
512
+ "guba.eastmoney.com": 0.30, # 股吧(UGC 讨论)
513
+ "zhuanlan.zhihu.com": 0.55, # 知乎专栏(部分原创部分转载)
514
+ "k.sina.com.cn": 0.40, # 新浪博客/聚合
515
+ }
516
+
517
+ # 转载/引用标记(出现在标题或 snippet 中)
518
+ _REPOST_MARKERS = [
519
+ "转载", "转自", "摘自", "编译自", "译自", "综合自",
520
+ "来源:", "来源:", "原文:", "原文:", "援引", "据援引",
521
+ "reposted", "via@", "via @", "credit:", "courtesy",
522
+ ]
523
+
524
+
525
+ def score_traceability(result: dict[str, Any]) -> dict[str, Any]:
526
+ """评估搜索结果的可溯源性(Traceability)。
527
+
528
+ 借鉴 doubao-sentiment-tracker 的溯源四场景方法论,将「能否追溯到原始
529
+ 来源」量化为 0-1 评分。与 Selection(域名权威)正交:一个结果可以
530
+ 权威但不可溯源(如聚合媒体的转载稿),也可以不权威但可溯源(如个人
531
+ 微博原帖)。
532
+
533
+ 评分规则:
534
+ - 一手信源(用户直接发布的原始帖子):0.80-0.95
535
+ - 原始媒体报道(新闻机构原创报道):0.70-0.95
536
+ - 综合报道/评论(引用了原始来源):0.50-0.65
537
+ - 二手转述/洗稿:0.30-0.45
538
+ - 无法判断:0.50(中性)
539
+
540
+ Returns:
541
+ {"score": float, "reason": str, "is_primary": bool, "platform_type": str}
542
+ """
543
+ url = result.get("url", "") or ""
544
+ title = result.get("title", "") or ""
545
+ snippet = result.get("snippet", "") or ""
546
+ source = result.get("source", "") or ""
547
+ content_type = result.get("content_type", "") or ""
548
+ social_meta = result.get("social_meta")
549
+ text = f"{title} {snippet}"
550
+
551
+ if not url:
552
+ return {
553
+ "score": 0.5,
554
+ "reason": "无 URL,无法判断可溯源性",
555
+ "is_primary": False,
556
+ "platform_type": "unknown",
557
+ }
558
+
559
+ domain = _normalize_domain(url)
560
+
561
+ # 1) 检查是否为 SERP/跳转 URL(不可溯源)
562
+ if is_serp_or_jump_url(url):
563
+ return {
564
+ "score": 0.10,
565
+ "reason": "搜索结果页/跳转链,非原始内容",
566
+ "is_primary": False,
567
+ "platform_type": "serp",
568
+ }
569
+
570
+ # 2) 检查是否为一手信源平台
571
+ best_primary_score = 0.0
572
+ best_primary_domain = ""
573
+ for pattern, score in _PRIMARY_PLATFORM_PATTERNS.items():
574
+ if domain == pattern or domain.endswith("." + pattern):
575
+ if score > best_primary_score:
576
+ best_primary_score = score
577
+ best_primary_domain = pattern
578
+
579
+ if best_primary_score > 0:
580
+ # 检查是否有转载标记(即使是原帖平台,也可能是转载)
581
+ has_repost = any(marker in text for marker in _REPOST_MARKERS)
582
+ if has_repost:
583
+ return {
584
+ "score": 0.45,
585
+ "reason": f"原帖平台({best_primary_domain})但含转载标记",
586
+ "is_primary": False,
587
+ "platform_type": "repost_on_primary",
588
+ }
589
+ return {
590
+ "score": best_primary_score,
591
+ "reason": f"一手信源({best_primary_domain})",
592
+ "is_primary": True,
593
+ "platform_type": "primary",
594
+ }
595
+
596
+ # 3) 检查是否为原始媒体报道
597
+ best_media_score = 0.0
598
+ best_media_domain = ""
599
+ for pattern, score in _ORIGINAL_MEDIA_PATTERNS.items():
600
+ if domain == pattern or domain.endswith("." + pattern):
601
+ if score > best_media_score:
602
+ best_media_score = score
603
+ best_media_domain = pattern
604
+
605
+ if best_media_score > 0:
606
+ has_repost = any(marker in text for marker in _REPOST_MARKERS)
607
+ if has_repost:
608
+ return {
609
+ "score": 0.55,
610
+ "reason": f"媒体平台({best_media_domain})但含转载标记",
611
+ "is_primary": False,
612
+ "platform_type": "media_repost",
613
+ }
614
+ return {
615
+ "score": best_media_score,
616
+ "reason": f"原始媒体报道({best_media_domain})",
617
+ "is_primary": True,
618
+ "platform_type": "official_media",
619
+ }
620
+
621
+ # 4) 检查是否为二手/聚合平台
622
+ for pattern, score in _SECONDARY_PLATFORM_PATTERNS.items():
623
+ if domain == pattern or domain.endswith("." + pattern):
624
+ return {
625
+ "score": score,
626
+ "reason": f"二手/聚合平台({pattern})",
627
+ "is_primary": False,
628
+ "platform_type": "aggregator",
629
+ }
630
+
631
+ # 5) 检查 content_type 和 social_meta
632
+ if content_type == "social_post" or social_meta:
633
+ return {
634
+ "score": 0.75,
635
+ "reason": "社交帖子(content_type=social_post)",
636
+ "is_primary": True,
637
+ "platform_type": "social_post",
638
+ }
639
+
640
+ # 6) 检查转载标记
641
+ has_repost = any(marker in text for marker in _REPOST_MARKERS)
642
+ if has_repost:
643
+ return {
644
+ "score": 0.40,
645
+ "reason": "含转载/引用标记",
646
+ "is_primary": False,
647
+ "platform_type": "repost",
648
+ }
649
+
650
+ # 7) 无法判断(中性)
651
+ return {
652
+ "score": 0.50,
653
+ "reason": "无法判断可溯源性(默认中性)",
654
+ "is_primary": False,
655
+ "platform_type": "unknown",
656
+ }
657
+
658
+
280
659
  # ── 综合可信度 ────────────────────────────────────────────────────────────────
281
660
 
282
- def compute_credibility(results: list[dict[str, Any]], query: str) -> dict[str, Any]:
283
- """计算每个结果的综合可信度评分。"""
661
+ def compute_credibility(
662
+ results: list[dict[str, Any]],
663
+ query: str,
664
+ *,
665
+ high_stakes: bool = False,
666
+ ) -> dict[str, Any]:
667
+ """两阶段综合可信度:Selection × Absorption + Freshness。"""
284
668
  query_time = time.time()
285
- scored_results = []
669
+ scored_results: list[dict[str, Any]] = []
286
670
 
287
671
  for r in results:
288
672
  url = r.get("url", "")
289
673
  source = r.get("source", "")
674
+ title = r.get("title", "") or ""
675
+ snippet = r.get("snippet", "") or ""
290
676
 
291
677
  auth = score_authority(url, source)
292
678
  fresh = score_freshness(r, query_time)
679
+ density = score_evidence_density(snippet, title)
293
680
 
294
- # 综合公式:权威性 0.5 + 时效性 0.3 + 原始分数 0.2
295
- original_score = r.get("score", 0.5) or 0.5
681
+ # Selection:权威为主;SERP 直接压到低分
682
+ selection = auth["score"]
683
+ if auth.get("is_serp"):
684
+ selection = min(selection, 0.15)
685
+
686
+ # Absorption:证据密度
687
+ absorption = density["absorption_score"]
688
+
689
+ original_score = r.get("score")
690
+ if original_score is None:
691
+ original_score = 0.5
692
+ else:
693
+ try:
694
+ original_score = float(original_score)
695
+ except (TypeError, ValueError):
696
+ original_score = 0.5
697
+ # Traceability:可溯源性
698
+ trace = score_traceability(r)
699
+
700
+ # MECE 加权(量化)— 五维度
296
701
  credibility = (
297
- auth["score"] * 0.5 +
298
- fresh["score"] * 0.3 +
299
- original_score * 0.2
702
+ selection * 0.35
703
+ + absorption * 0.30
704
+ + fresh["score"] * 0.15
705
+ + float(original_score) * 0.10
706
+ + trace["score"] * 0.10
300
707
  )
301
708
 
302
709
  scored_results.append({
303
- "title": r.get("title", ""),
710
+ "title": title,
304
711
  "url": url,
305
712
  "source": source,
306
- "snippet": (r.get("snippet", "") or "")[:150],
713
+ "snippet": snippet[:180],
307
714
  "credibility": {
308
715
  "final": round(credibility, 3),
716
+ "selection": round(selection, 3),
717
+ "absorption": round(absorption, 3),
309
718
  "authority": auth,
310
719
  "freshness": fresh,
720
+ "evidence_density": density,
311
721
  "original_score": original_score,
722
+ "traceability": trace,
312
723
  },
313
724
  })
314
725
 
315
- # 交叉验证
316
726
  cross = cross_validate(results, query)
317
727
 
318
- # 排序
728
+ if high_stakes and scored_results:
729
+ # 共识调节:强佐证略抬高 top 可吸收源;弱共识不抬
730
+ boost = 0.0
731
+ if cross["corroboration_level"] == "strong":
732
+ boost = 0.05
733
+ elif cross["corroboration_level"] == "moderate":
734
+ boost = 0.02
735
+ for item in scored_results:
736
+ if not item["credibility"]["authority"].get("is_serp"):
737
+ item["credibility"]["final"] = round(
738
+ min(item["credibility"]["final"] + boost, 1.0), 3
739
+ )
740
+ item["credibility"]["consensus_boost"] = boost
741
+
319
742
  scored_results.sort(key=lambda x: x["credibility"]["final"], reverse=True)
320
743
 
321
744
  return {
322
745
  "query": query,
746
+ "framework": "selection_x_absorption_v2.3_traceability",
323
747
  "results": scored_results,
324
748
  "cross_validation": cross,
325
749
  "summary": {
326
750
  "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),
751
+ "high_credibility": sum(
752
+ 1 for r in scored_results if r["credibility"]["final"] >= 0.7
753
+ ),
754
+ "medium_credibility": sum(
755
+ 1 for r in scored_results if 0.5 <= r["credibility"]["final"] < 0.7
756
+ ),
757
+ "low_credibility": sum(
758
+ 1 for r in scored_results if r["credibility"]["final"] < 0.5
759
+ ),
760
+ "serp_filtered": sum(
761
+ 1 for r in scored_results
762
+ if r["credibility"]["authority"].get("is_serp")
763
+ ),
330
764
  "best_source": scored_results[0] if scored_results else None,
331
765
  },
332
766
  }
@@ -335,36 +769,53 @@ def compute_credibility(results: list[dict[str, Any]], query: str) -> dict[str,
335
769
  # ── CLI ───────────────────────────────────────────────────────────────────────
336
770
 
337
771
  def main():
338
- parser = argparse.ArgumentParser(description="来源可信度评估工具")
772
+ ensure_utf8_stdio() # 入口第一件事:钉住 stdout/stderr 编码(见 cli_io)
773
+ parser = argparse.ArgumentParser(description="来源可信度评估工具(Selection×Absorption)")
339
774
  parser.add_argument("query", nargs="?", default="", help="搜索查询词")
340
775
  parser.add_argument("--stdin", action="store_true", help="从 stdin 读取 JSON 搜索结果")
341
776
  parser.add_argument("--json", action="store_true", help="JSON 输出")
777
+ parser.add_argument(
778
+ "--high-stakes",
779
+ action="store_true",
780
+ help="高后果模式:启用共识微调",
781
+ )
342
782
  args = parser.parse_args()
343
783
 
344
784
  if args.stdin:
345
785
  data = json.load(sys.stdin)
346
786
  results = data.get("results", [])
347
787
  else:
348
- # 从搜索结果文件读取
349
788
  results = []
350
789
 
351
790
  if not results and args.query:
352
- # 无输入结果时,输出提示
353
791
  print("需要提供搜索结果进行评估。用法:")
354
792
  print(' echo \'{"results": [...]}\' | python3 evidence.py --stdin --query "查询词"')
355
793
  sys.exit(1)
356
794
 
357
- report = compute_credibility(results, args.query)
795
+ report = compute_credibility(
796
+ results, args.query, high_stakes=args.high_stakes
797
+ )
358
798
 
359
799
  if args.json:
360
- print(json.dumps(report, ensure_ascii=False, indent=2))
800
+ print(dumps(report))
361
801
  else:
362
- print(f"\n来源可信度评估:{report['query']}")
802
+ print(f"\n来源可信度评估:{report['query']} [{report['framework']}]")
363
803
  print(f"{'='*50}")
364
804
  for r in report["results"]:
365
805
  c = r["credibility"]
366
806
  level = "🟢" if c["final"] >= 0.7 else "🟡" if c["final"] >= 0.5 else "🔴"
367
- print(f"{level} [{c['final']:.2f}] {r['title'][:50]}")
807
+ ed = c.get("evidence_density") or {}
808
+ flags = []
809
+ if ed.get("has_numbers"):
810
+ flags.append("数")
811
+ if ed.get("has_comparison"):
812
+ flags.append("比")
813
+ if ed.get("has_definition"):
814
+ flags.append("义")
815
+ if c["authority"].get("is_serp"):
816
+ flags.append("SERP")
817
+ flag_s = ",".join(flags) if flags else "-"
818
+ print(f"{level} [{c['final']:.2f}] sel={c['selection']:.2f} abs={c['absorption']:.2f} [{flag_s}] {r['title'][:48]}")
368
819
  print(f" 权威:{c['authority']['score']:.2f} ({c['authority']['reason']})")
369
820
  print(f" 时效:{c['freshness']['score']:.2f} ({c['freshness']['reason']})")
370
821
  print()
@@ -374,7 +825,10 @@ def main():
374
825
  print(f" {cv['detail']}")
375
826
 
376
827
  s = report["summary"]
377
- print(f"\n总结:🟢高={s['high_credibility']} 🟡中={s['medium_credibility']} 🔴低={s['low_credibility']}")
828
+ print(
829
+ f"\n总结:🟢高={s['high_credibility']} 🟡中={s['medium_credibility']} "
830
+ f"🔴低={s['low_credibility']} SERP={s.get('serp_filtered', 0)}"
831
+ )
378
832
 
379
833
 
380
834
  if __name__ == "__main__":