eduevidence 5.2.0 → 6.2.0

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 (386) hide show
  1. package/CONTRIBUTING.md +105 -0
  2. package/README.md +142 -75
  3. package/README.zh-CN.md +73 -30
  4. package/SKILL.md +397 -131
  5. package/agents/openai.yaml +4 -0
  6. package/assets/readme/controlled-execution.svg +34 -0
  7. package/assets/readme/landing-tour.gif +0 -0
  8. package/assets/readme/logo.png +0 -0
  9. package/assets/readme/research-workflow.svg +56 -0
  10. package/assets/readme/studio-graph.png +0 -0
  11. package/assets/readme/studio-overview.png +0 -0
  12. package/assets/readme/studio-reports.png +0 -0
  13. package/assets/readme/studio-tour.gif +0 -0
  14. package/autoevolve/config.yaml +17 -0
  15. package/autoevolve/program.md +25 -0
  16. package/autoevolve/protected.manifest.yaml +34 -0
  17. package/benchmarks/adversarial/cases.jsonl +7 -0
  18. package/benchmarks/evidence-library.json +5268 -0
  19. package/benchmarks/partitions.json +8 -0
  20. package/bin/eduevidence.js +2 -1
  21. package/docs/architecture.md +496 -0
  22. package/docs/autoresearch-evolution-plan.md +2903 -0
  23. package/docs/autoresearch-implementation-status.md +101 -0
  24. package/docs/demo-storyboard.md +20 -0
  25. package/docs/demo-workplace-ai.md +92 -0
  26. package/docs/demo.md +32 -0
  27. package/docs/install-guide.md +150 -0
  28. package/docs/orchestration-role-model.md +1254 -0
  29. package/docs/release-closeout/README.md +17 -0
  30. package/docs/release-closeout/frontend-acceptance.md +23 -0
  31. package/docs/release-closeout/issues.md +19 -0
  32. package/docs/release-closeout/verification.md +28 -0
  33. package/docs/release-contract.md +108 -0
  34. package/docs/research-studio-guide.zh-CN.md +166 -0
  35. package/docs/sciverse-api.md +125 -0
  36. package/eduevidence_cli.py +29 -13
  37. package/engine/_resources.py +13 -0
  38. package/engine/autoevolve/__init__.py +3 -0
  39. package/engine/autoevolve/agent_view.py +167 -0
  40. package/engine/autoevolve/core.py +357 -0
  41. package/engine/autoevolve/events.py +11 -0
  42. package/engine/autoevolve/git_workspace.py +77 -0
  43. package/engine/autoevolve/projection.py +23 -0
  44. package/engine/autoevolve/runner.py +413 -0
  45. package/engine/autoevolve/trust.py +146 -0
  46. package/engine/autoresearch/__init__.py +6 -0
  47. package/engine/autoresearch/commit.py +132 -0
  48. package/engine/autoresearch/contracts.py +126 -0
  49. package/engine/autoresearch/controller.py +207 -0
  50. package/engine/autoresearch/events.py +12 -0
  51. package/engine/autoresearch/gap_priority.py +168 -0
  52. package/engine/autoresearch/projection.py +30 -0
  53. package/engine/autoresearch/research_memory.py +59 -0
  54. package/engine/autoresearch/saturation.py +91 -0
  55. package/engine/briefs.py +2 -1
  56. package/engine/capabilities.py +1 -0
  57. package/engine/contracts.py +3 -1
  58. package/engine/decision_policy.py +96 -0
  59. package/engine/evidence_graph.py +14 -10
  60. package/engine/evidencecore.py +7 -5
  61. package/engine/gaps.py +132 -73
  62. package/engine/ids.py +2 -0
  63. package/engine/judge_pack.py +65 -0
  64. package/engine/library.py +6 -2
  65. package/engine/library_builtin.py +3 -1
  66. package/engine/living.py +36 -5
  67. package/engine/meta_synthesis.py +3 -1
  68. package/engine/migration.py +88 -3
  69. package/engine/orchestration.py +460 -0
  70. package/engine/paths.py +2 -0
  71. package/engine/pilot.py +36 -33
  72. package/engine/project.py +2 -2
  73. package/engine/research_service.py +113 -0
  74. package/engine/studio_read_model.py +400 -0
  75. package/engine/taxonomy.py +211 -0
  76. package/engine/tribunal.py +44 -33
  77. package/engine/update.py +1 -0
  78. package/engine/versions.py +1 -1
  79. package/engine/worker_result.py +109 -0
  80. package/engine/workflows.py +70 -0
  81. package/examples/ai-coding-assistant-evidence/EduEvidence_Report.html +2934 -0
  82. package/examples/ai-coding-assistant-evidence/artifact_manifest.json +15 -0
  83. package/examples/ai-coding-assistant-evidence/citation_check.json +79 -0
  84. package/examples/ai-coding-assistant-evidence/claims.jsonl +12 -0
  85. package/examples/ai-coding-assistant-evidence/evaluation.json +35 -0
  86. package/examples/ai-coding-assistant-evidence/evidence.jsonl +12 -0
  87. package/examples/ai-coding-assistant-evidence/final_verdict.json +107 -0
  88. package/examples/ai-coding-assistant-evidence/frame.json +48 -0
  89. package/examples/ai-coding-assistant-evidence/gate_report.json +101 -0
  90. package/examples/ai-coding-assistant-evidence/intervention.json +51 -0
  91. package/examples/ai-coding-assistant-evidence/methodology.json +36 -0
  92. package/examples/ai-coding-assistant-evidence/raw_verdict.json +86 -0
  93. package/examples/ai-coding-assistant-evidence/report_spec.json +230 -0
  94. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_academic.html +2934 -0
  95. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_claude.html +2934 -0
  96. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab-dark.html +2934 -0
  97. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab.html +2934 -0
  98. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_presentation.html +2934 -0
  99. package/examples/ai-coding-assistant-evidence/reports-5themes/report_academic.html +2934 -0
  100. package/examples/ai-coding-assistant-evidence/reports-5themes/report_claude.html +2934 -0
  101. package/examples/ai-coding-assistant-evidence/reports-5themes/report_datalab-dark.html +2934 -0
  102. package/examples/ai-coding-assistant-evidence/reports-5themes/report_datalab.html +2934 -0
  103. package/examples/ai-coding-assistant-evidence/reports-5themes/report_presentation.html +2934 -0
  104. package/examples/ai-coding-assistant-evidence/result.json +1457 -0
  105. package/examples/ai-coding-assistant-evidence/result.zh.json +1457 -0
  106. package/examples/ai-coding-assistant-evidence/skeptic.json +72 -0
  107. package/examples/ai-coding-assistant-evidence/sources.jsonl +8 -0
  108. package/examples/ai-coding-assistant-evidence/verdict.json +107 -0
  109. package/examples/spaced-retrieval-practice/applicability.json +14 -0
  110. package/examples/spaced-retrieval-practice/artifact_manifest.json +15 -0
  111. package/examples/spaced-retrieval-practice/claims.jsonl +3 -0
  112. package/examples/spaced-retrieval-practice/evidence.jsonl +6 -0
  113. package/examples/spaced-retrieval-practice/final_verdict.json +93 -0
  114. package/examples/spaced-retrieval-practice/frame.json +58 -0
  115. package/examples/spaced-retrieval-practice/gate_report.json +101 -0
  116. package/examples/spaced-retrieval-practice/methodology.json +78 -0
  117. package/examples/spaced-retrieval-practice/report_spec.json +212 -0
  118. package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_academic.html +2728 -0
  119. package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_claude.html +2728 -0
  120. package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_datalab-dark.html +2728 -0
  121. package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_datalab.html +2728 -0
  122. package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_presentation.html +2728 -0
  123. package/examples/spaced-retrieval-practice/reports-5themes/report_academic.html +2728 -0
  124. package/examples/spaced-retrieval-practice/reports-5themes/report_claude.html +2728 -0
  125. package/examples/spaced-retrieval-practice/reports-5themes/report_datalab-dark.html +2728 -0
  126. package/examples/spaced-retrieval-practice/reports-5themes/report_datalab.html +2728 -0
  127. package/examples/spaced-retrieval-practice/reports-5themes/report_presentation.html +2728 -0
  128. package/examples/spaced-retrieval-practice/result.json +942 -0
  129. package/examples/spaced-retrieval-practice/result.zh.json +942 -0
  130. package/examples/spaced-retrieval-practice/skeptic.json +70 -0
  131. package/examples/spaced-retrieval-practice/sources.jsonl +7 -0
  132. package/examples/spaced-retrieval-practice/verdict.json +93 -0
  133. package/examples/workplace-ai-assistant/artifact_manifest.json +15 -0
  134. package/examples/workplace-ai-assistant/claims.jsonl +4 -0
  135. package/examples/workplace-ai-assistant/evaluation.json +19 -0
  136. package/examples/workplace-ai-assistant/evidence.jsonl +4 -0
  137. package/examples/workplace-ai-assistant/evidence_graph.json +444 -0
  138. package/examples/workplace-ai-assistant/final_verdict.json +78 -0
  139. package/examples/workplace-ai-assistant/frame.json +41 -0
  140. package/examples/workplace-ai-assistant/gate_report.json +101 -0
  141. package/examples/workplace-ai-assistant/intervention.json +27 -0
  142. package/examples/workplace-ai-assistant/legacy-link-check.json +16 -0
  143. package/examples/workplace-ai-assistant/methodology.json +60 -0
  144. package/examples/workplace-ai-assistant/report_spec.json +224 -0
  145. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_academic.html +2814 -0
  146. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_claude.html +2814 -0
  147. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab-dark.html +2814 -0
  148. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab.html +2814 -0
  149. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_presentation.html +2814 -0
  150. package/examples/workplace-ai-assistant/reports-5themes/report_academic.html +2814 -0
  151. package/examples/workplace-ai-assistant/reports-5themes/report_claude.html +2814 -0
  152. package/examples/workplace-ai-assistant/reports-5themes/report_datalab-dark.html +2814 -0
  153. package/examples/workplace-ai-assistant/reports-5themes/report_datalab.html +2814 -0
  154. package/examples/workplace-ai-assistant/reports-5themes/report_presentation.html +2814 -0
  155. package/examples/workplace-ai-assistant/result.json +615 -0
  156. package/examples/workplace-ai-assistant/result.zh.json +615 -0
  157. package/examples/workplace-ai-assistant/search_log.json +19 -0
  158. package/examples/workplace-ai-assistant/skeptic.json +72 -0
  159. package/examples/workplace-ai-assistant/sources.jsonl +3 -0
  160. package/examples/workplace-ai-assistant/validation_result.json +9 -0
  161. package/examples/workplace-ai-assistant/verdict.json +78 -0
  162. package/install.sh +7 -7
  163. package/integrations/agent_mcp.py +2 -2
  164. package/integrations/orchestration_dispatch.py +146 -0
  165. package/package.json +46 -3
  166. package/pyproject.toml +14 -22
  167. package/references/autoresearch.md +30 -0
  168. package/references/evaluation-policy.md +24 -0
  169. package/references/orchestration.md +22 -0
  170. package/references/report-copy-style.md +67 -0
  171. package/references/retrieval-compliance.md +75 -0
  172. package/references/retrieval-protocol.md +20 -0
  173. package/references/scientific-invariants.md +19 -0
  174. package/retrieval/audit.py +178 -0
  175. package/retrieval/fetch.py +96 -0
  176. package/retrieval/sciverse.py +398 -0
  177. package/retrieval/search.py +47 -7
  178. package/schemas/applicability.schema.json +94 -0
  179. package/schemas/chart-spec.schema.json +10 -3
  180. package/schemas/evidence.schema.json +316 -43
  181. package/schemas/fetch-result.schema.json +2 -1
  182. package/schemas/intervention.schema.json +106 -21
  183. package/schemas/report-result.schema.json +12 -4
  184. package/schemas/report-spec.schema.json +98 -100
  185. package/schemas/skeptic.schema.json +86 -0
  186. package/schemas/source.schema.json +21 -2
  187. package/schemas/v2/finding.schema.json +5 -1
  188. package/schemas/v2/methodology-audit.schema.json +5 -1
  189. package/schemas/v2/outcome.schema.json +28 -5
  190. package/schemas/v2/project.schema.json +2 -2
  191. package/schemas/v2/run.schema.json +1 -1
  192. package/schemas/v2/study.schema.json +5 -1
  193. package/schemas/vNext/autoevolve-session.schema.json +34 -0
  194. package/schemas/vNext/eval-snapshot.schema.json +77 -0
  195. package/schemas/vNext/execution-plan.schema.json +50 -0
  196. package/schemas/vNext/gap-priority.schema.json +54 -0
  197. package/schemas/vNext/negative-search-record.schema.json +68 -0
  198. package/schemas/vNext/research-iteration.schema.json +87 -0
  199. package/schemas/vNext/research-strategy.schema.json +62 -0
  200. package/schemas/vNext/skill-experiment.schema.json +90 -0
  201. package/schemas/vNext/task-spec.schema.json +156 -0
  202. package/schemas/vNext/worker-result.schema.json +60 -0
  203. package/schemas/verdict.schema.json +164 -28
  204. package/scripts/benchmark_judge.py +2 -2
  205. package/scripts/benchmark_v3.py +26 -43
  206. package/scripts/build_esl_artifacts.py +4 -4
  207. package/scripts/build_evidence_library.py +2 -2
  208. package/scripts/build_gh_pages.py +98 -0
  209. package/scripts/build_readme_diagrams.py +72 -0
  210. package/scripts/build_report_variants.py +101 -0
  211. package/scripts/build_result.py +74 -9
  212. package/scripts/check_autoresearch_invariants.py +95 -0
  213. package/scripts/check_package_parity.py +85 -0
  214. package/scripts/check_protocol_alignment.py +375 -0
  215. package/scripts/check_versioned_schemas.py +254 -0
  216. package/scripts/claim_audit.py +13 -8
  217. package/scripts/compute_confidence.py +10 -0
  218. package/scripts/daily_evolve.py +30 -0
  219. package/scripts/dashboard_server.py +130 -101
  220. package/scripts/did_regression.py +17 -32
  221. package/scripts/enrich_projects_human_and_lieflat.py +1 -1
  222. package/scripts/evidence_score.py +5 -2
  223. package/scripts/generate_metrics.py +4 -3
  224. package/scripts/generate_new_projects.py +5 -5
  225. package/scripts/orchestrator.py +286 -36
  226. package/scripts/pre_verdict_gate.py +224 -26
  227. package/scripts/quickstart.py +18 -2
  228. package/scripts/rebake_all_5themes.py +1 -2
  229. package/scripts/research_auto_cli.py +475 -0
  230. package/scripts/run_workspace.py +24 -8
  231. package/scripts/search_provenance.py +64 -0
  232. package/scripts/serve_web.py +9 -10
  233. package/scripts/skill_lint.py +1 -1
  234. package/scripts/skill_payload.py +81 -0
  235. package/scripts/test_adversarial_empirical.py +26 -19
  236. package/scripts/validate_schema.py +46 -2
  237. package/scripts/vnext_cli.py +133 -0
  238. package/setup.py +12 -0
  239. package/skill/agents/evaluation-designer.md +20 -4
  240. package/skill/agents/evidence-analyst.md +19 -3
  241. package/skill/agents/evidence-judge.md +50 -2
  242. package/skill/agents/evidence-retriever.md +20 -3
  243. package/skill/agents/intervention-designer.md +20 -4
  244. package/skill/agents/method-reviewer.md +18 -2
  245. package/skill/agents/{education-planner.md → research-planner.md} +19 -3
  246. package/skill/agents/skeptic.md +18 -2
  247. package/skill/roles/registry.yaml +45 -0
  248. package/skill/sub-skills/aihot-trend-analysis/SKILL.md +28 -9
  249. package/skill/sub-skills/contradiction-analysis/SKILL.md +31 -11
  250. package/skill/sub-skills/data-analysis/SKILL.md +34 -15
  251. package/skill/sub-skills/ethics-review/SKILL.md +33 -10
  252. package/skill/sub-skills/evidence-extraction/SKILL.md +29 -11
  253. package/skill/sub-skills/evidence-review/SKILL.md +31 -12
  254. package/skill/sub-skills/gap-analysis/SKILL.md +31 -9
  255. package/skill/sub-skills/literature-review/SKILL.md +35 -14
  256. package/skill/sub-skills/methodology-audit/SKILL.md +29 -12
  257. package/skill/sub-skills/report-generation/SKILL.md +40 -6
  258. package/skill/sub-skills/research-planning/SKILL.md +41 -14
  259. package/skill/sub-skills/study-design/SKILL.md +30 -9
  260. package/skill/task-briefs/adjudicate.md +32 -7
  261. package/skill/task-briefs/applicability.md +38 -0
  262. package/skill/task-briefs/audit.md +32 -7
  263. package/skill/task-briefs/challenge.md +34 -5
  264. package/skill/task-briefs/evaluate.md +30 -5
  265. package/skill/task-briefs/extract.md +31 -8
  266. package/skill/task-briefs/frame.md +39 -10
  267. package/skill/task-briefs/intervene.md +32 -6
  268. package/skill/task-briefs/present.md +32 -8
  269. package/skill/task-briefs/projection.md +37 -0
  270. package/skill/task-briefs/retrieve.md +36 -6
  271. package/skill/workflows/decision-and-pilot.md +85 -0
  272. package/skill/workflows/evaluate-and-update.md +93 -0
  273. package/skill/workflows/evidence-review.md +117 -0
  274. package/visualization/eduevidence-report/assets/base.css +2 -2
  275. package/visualization/eduevidence-report/assets/reader.css +752 -0
  276. package/visualization/eduevidence-report/assets/reader.js +132 -0
  277. package/visualization/eduevidence-report/references/chart-selection-catalog.md +109 -0
  278. package/visualization/eduevidence-report/references/lieflat-composition.md +3 -1
  279. package/visualization/eduevidence-report/scripts/build_figures.py +25 -3
  280. package/visualization/eduevidence-report/scripts/build_infographics.py +5 -1
  281. package/visualization/eduevidence-report/scripts/build_report.py +561 -121
  282. package/visualization/eduevidence-report/scripts/charts_data.py +2 -0
  283. package/visualization/eduevidence-report/scripts/lieflat_engine.py +371 -136
  284. package/visualization/eduevidence-report/scripts/zh_labels.py +80 -1
  285. package/visualization/eduevidence-report/themes/academic.css +1 -1
  286. package/visualization/eduevidence-report/themes/claude.css +1 -1
  287. package/visualization/eduevidence-report/themes/datalab-dark.css +2 -2
  288. package/visualization/eduevidence-report/themes/datalab.css +2 -2
  289. package/visualization/eduevidence-report/themes/presentation.css +2 -2
  290. package/web/README.md +18 -0
  291. package/web/architecture.html +14885 -0
  292. package/web/index.html +53 -0
  293. package/web/studio/THIRD_PARTY_LICENSES.txt +146 -0
  294. package/web/studio/assets/index-B8tkF44Q.css +1 -0
  295. package/web/studio/assets/index-CQ6Keoyc.js +230 -0
  296. package/web/studio/config.json +1 -0
  297. package/web/studio/index.html +14 -0
  298. package/engine/__pycache__/__init__.cpython-312.pyc +0 -0
  299. package/engine/__pycache__/analysis.cpython-312.pyc +0 -0
  300. package/engine/__pycache__/bias.cpython-312.pyc +0 -0
  301. package/engine/__pycache__/briefs.cpython-312.pyc +0 -0
  302. package/engine/__pycache__/capabilities.cpython-312.pyc +0 -0
  303. package/engine/__pycache__/citation_check.cpython-312.pyc +0 -0
  304. package/engine/__pycache__/contracts.cpython-312.pyc +0 -0
  305. package/engine/__pycache__/datasets.cpython-312.pyc +0 -0
  306. package/engine/__pycache__/events.cpython-312.pyc +0 -0
  307. package/engine/__pycache__/evidence_graph.cpython-312.pyc +0 -0
  308. package/engine/__pycache__/evidence_review.cpython-312.pyc +0 -0
  309. package/engine/__pycache__/evidencecore.cpython-312.pyc +0 -0
  310. package/engine/__pycache__/gap_lens.cpython-312.pyc +0 -0
  311. package/engine/__pycache__/gaps.cpython-312.pyc +0 -0
  312. package/engine/__pycache__/graph_store.cpython-312.pyc +0 -0
  313. package/engine/__pycache__/graph_validate.cpython-312.pyc +0 -0
  314. package/engine/__pycache__/ids.cpython-312.pyc +0 -0
  315. package/engine/__pycache__/library.cpython-312.pyc +0 -0
  316. package/engine/__pycache__/library_builtin.cpython-312.pyc +0 -0
  317. package/engine/__pycache__/living.cpython-312.pyc +0 -0
  318. package/engine/__pycache__/log.cpython-312.pyc +0 -0
  319. package/engine/__pycache__/meta_analysis.cpython-312.pyc +0 -0
  320. package/engine/__pycache__/meta_synthesis.cpython-312.pyc +0 -0
  321. package/engine/__pycache__/migration.cpython-312.pyc +0 -0
  322. package/engine/__pycache__/mode_router.cpython-312.pyc +0 -0
  323. package/engine/__pycache__/paths.cpython-312.pyc +0 -0
  324. package/engine/__pycache__/pilot.cpython-312.pyc +0 -0
  325. package/engine/__pycache__/planner.cpython-312.pyc +0 -0
  326. package/engine/__pycache__/project.cpython-312.pyc +0 -0
  327. package/engine/__pycache__/projections.cpython-312.pyc +0 -0
  328. package/engine/__pycache__/robustness.cpython-312.pyc +0 -0
  329. package/engine/__pycache__/run.cpython-312.pyc +0 -0
  330. package/engine/__pycache__/semantics.cpython-312.pyc +0 -0
  331. package/engine/__pycache__/study_design.cpython-312.pyc +0 -0
  332. package/engine/__pycache__/synthesis.cpython-312.pyc +0 -0
  333. package/engine/__pycache__/tribunal.cpython-312.pyc +0 -0
  334. package/engine/__pycache__/update.cpython-312.pyc +0 -0
  335. package/engine/__pycache__/versions.cpython-312.pyc +0 -0
  336. package/integrations/__pycache__/__init__.cpython-312.pyc +0 -0
  337. package/integrations/__pycache__/agent_mcp.cpython-312.pyc +0 -0
  338. package/integrations/__pycache__/smart_web_fetch.cpython-312.pyc +0 -0
  339. package/retrieval/__pycache__/__init__.cpython-312.pyc +0 -0
  340. package/retrieval/__pycache__/corpus_store.cpython-312.pyc +0 -0
  341. package/retrieval/__pycache__/dedupe.cpython-312.pyc +0 -0
  342. package/retrieval/__pycache__/failures.cpython-312.pyc +0 -0
  343. package/retrieval/__pycache__/fetch.cpython-312.pyc +0 -0
  344. package/retrieval/__pycache__/search.cpython-312.pyc +0 -0
  345. package/retrieval/__pycache__/source.cpython-312.pyc +0 -0
  346. package/retrieval/__pycache__/validate.cpython-312.pyc +0 -0
  347. package/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
  348. package/scripts/__pycache__/benchmark.cpython-312.pyc +0 -0
  349. package/scripts/__pycache__/benchmark_evaluator.cpython-312.pyc +0 -0
  350. package/scripts/__pycache__/benchmark_judge.cpython-312.pyc +0 -0
  351. package/scripts/__pycache__/benchmark_routing.cpython-312.pyc +0 -0
  352. package/scripts/__pycache__/benchmark_v2.cpython-312.pyc +0 -0
  353. package/scripts/__pycache__/benchmark_v3.cpython-312.pyc +0 -0
  354. package/scripts/__pycache__/build_result.cpython-312.pyc +0 -0
  355. package/scripts/__pycache__/claim_audit.cpython-312.pyc +0 -0
  356. package/scripts/__pycache__/complexity_gate.cpython-312.pyc +0 -0
  357. package/scripts/__pycache__/compute_confidence.cpython-312.pyc +0 -0
  358. package/scripts/__pycache__/dashboard_server.cpython-312.pyc +0 -0
  359. package/scripts/__pycache__/did_regression.cpython-312.pyc +0 -0
  360. package/scripts/__pycache__/effect_calculator.cpython-312.pyc +0 -0
  361. package/scripts/__pycache__/evidence_matrix.cpython-312.pyc +0 -0
  362. package/scripts/__pycache__/evidence_score.cpython-312.pyc +0 -0
  363. package/scripts/__pycache__/evidence_semantics.cpython-312.pyc +0 -0
  364. package/scripts/__pycache__/fetch_benchmark.cpython-312.pyc +0 -0
  365. package/scripts/__pycache__/lint_report_layout.cpython-312.pyc +0 -0
  366. package/scripts/__pycache__/orchestrator.cpython-312.pyc +0 -0
  367. package/scripts/__pycache__/pre_verdict_gate.cpython-312.pyc +0 -0
  368. package/scripts/__pycache__/recompute_demo_quality.cpython-312.pyc +0 -0
  369. package/scripts/__pycache__/render_report.cpython-312.pyc +0 -0
  370. package/scripts/__pycache__/render_report_html.cpython-312.pyc +0 -0
  371. package/scripts/__pycache__/run_workspace.cpython-312.pyc +0 -0
  372. package/scripts/__pycache__/skill_lint.cpython-312.pyc +0 -0
  373. package/scripts/__pycache__/startup_probe.cpython-312.pyc +0 -0
  374. package/scripts/__pycache__/sync_killer_demo_report.cpython-312.pyc +0 -0
  375. package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.0.2.pyc +0 -0
  376. package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.1.1.pyc +0 -0
  377. package/scripts/__pycache__/validate_schema.cpython-312.pyc +0 -0
  378. package/visualization/eduevidence-report/scripts/__pycache__/adapter_contract.cpython-312.pyc +0 -0
  379. package/visualization/eduevidence-report/scripts/__pycache__/build_artifact_manifest.cpython-312.pyc +0 -0
  380. package/visualization/eduevidence-report/scripts/__pycache__/build_charts.cpython-312.pyc +0 -0
  381. package/visualization/eduevidence-report/scripts/__pycache__/build_figures.cpython-312.pyc +0 -0
  382. package/visualization/eduevidence-report/scripts/__pycache__/build_infographics.cpython-312.pyc +0 -0
  383. package/visualization/eduevidence-report/scripts/__pycache__/build_report.cpython-312.pyc +0 -0
  384. package/visualization/eduevidence-report/scripts/__pycache__/charts_data.cpython-312.pyc +0 -0
  385. package/visualization/eduevidence-report/scripts/__pycache__/lieflat_engine.cpython-312.pyc +0 -0
  386. package/visualization/eduevidence-report/scripts/__pycache__/zh_labels.cpython-312.pyc +0 -0
@@ -0,0 +1,67 @@
1
+ # Report Copy Style — 读者向文案规范
2
+
3
+ 本规范约束**报告里面向读者的文字**(第一屏决策叙事、裁决列表、章节引导语、图表标题与说明)。它服务于一个判断标准:**读者能否一次读懂,且能核对**。规则由 `skill/agents/evidence-judge.md`(写作前)与 `visualization/eduevidence-report/scripts/build_report.py` 的语言门禁(交付后)双向执行。
4
+
5
+ ## 1. 谁写什么
6
+
7
+ | 内容 | 作者 | 说明 |
8
+ |---|---|---|
9
+ | 决策叙事四件套(`strongest_support` / `key_uncertainty` / `main_risk` / `next_action`) | **裁决角色(模型)撰写** | 渲染器只呈现,缺字段就报缺,禁止用列表片段拼装句子 |
10
+ | `decision_rationale` | 裁决角色 | ≤4 句,可独立阅读 |
11
+ | 章节引导语、图例、按钮、兜底句 | 渲染器(固定文案表) | 不随研究内容变化 |
12
+ | 证据与主张原文 | 抽取/裁决角色 | 渲染器不改写 |
13
+
14
+ **为什么写死这条**:报告第一屏曾由渲染器从 `what_can_be_claimed[0]` 之类的片段拼出,读起来像拼装而非撰写,且掩盖了「该字段没人产出」这一事实。撰写责任在模型,呈现责任在渲染器。
15
+
16
+ ## 2. 语言
17
+
18
+ - 面向**非本领域的决策者**:不假设读者熟悉术语。
19
+ - 一句话一个意思;先结论、后依据。
20
+ - 中文用中文,英文用英文;两种语言各自成篇,不做逐字直译。
21
+ - 允许保留原文的例外:AI、RCT、DOI、WWC、GRADE、论文原标题(须标注「原文标题」)。
22
+
23
+ ## 3. 字数上限
24
+
25
+ | 字段 | 上限 | 约合 |
26
+ |---|---|---|
27
+ | `strongest_support` | 60 字 | 2 句 |
28
+ | `key_uncertainty` | 70 字 | 2–3 句 |
29
+ | `main_risk` | 60 字 | 2 句 |
30
+ | `next_action` | 80 字 | 2–3 句 |
31
+ | `decision_rationale` | 160 字 | ≤4 句 |
32
+
33
+ 英文字数上限按等义折算(约为中文字数 ÷ 3 个单词)。
34
+
35
+ ## 4. 术语对照
36
+
37
+ 同一概念全文只用一个说法:
38
+
39
+ | 内部字段 | 中文 | 英文 |
40
+ |---|---|---|
41
+ | `outcome` / `outcome_type` | 结果 | outcome |
42
+ | `claim` | 主张 | claim |
43
+ | `evidence` | 证据 | evidence |
44
+ | `relation_to_claim` | 与主张的关系 | relation to claim |
45
+ | `effect_direction` | 效应方向 | effect direction |
46
+ | `directness` | 直接性 | directness |
47
+ | `task_performance` | 任务表现 | task performance |
48
+ | `retention` | 保持 | retention |
49
+ | `transfer` | 迁移 | transfer |
50
+ | `applicability` | 适用性 | applicability |
51
+
52
+ ## 5. 禁止
53
+
54
+ - 内部字段名与存储标识(`effect_direction`、`first_programming_course_...`)出现在叙述句里;它们只能出现在「原始标识」提示或溯源展开区。
55
+ - 证据 ID 堆砌(`E-001、E-006`)出现在决策叙事里;引用研究用「作者-年份 + 人话描述」。
56
+ - 图表标题写公式(如 `position = (positive − negative) ÷ count`);用自然语言说明图形含义。
57
+ - 半句截断、`null` 残留、中英夹生。
58
+ - 用兜底句掩盖缺失:字段没产出就如实说没产出。
59
+
60
+ ## 6. 门禁如何执行
61
+
62
+ - `check_language_parallel()`:叙述字段必须为对应语言、双语不得完全相同、不得含内部键名。
63
+ - 原始标识检查:`research_frame` 与决策字段中的多词串若含未注册的 `snake_case`,记为缺陷。
64
+ - 字数检查:四件套超过上限即失败。
65
+ - 一致性检查:`result.json` 与 `result.zh.json` 的证据在 id/来源/研究/样本/方向/关系六个结构字段上必须逐条相等(自由文本可不同)。
66
+
67
+ 相关:`visualization/eduevidence-report/references/bilingual-style.md`(双语渲染约定)、`references/scientific-invariants.md`(科学不变量)。
@@ -0,0 +1,75 @@
1
+ # Retrieval Compliance Policy(检索合规政策)
2
+
3
+ 本条政策约束 EduEvidence 的一切外部检索与抓取行为。它服务于两件事:**研究的可复现性**(每条来源都能被第三方重新定位)与**对他方服务与作者的尊重**(不越权、不滥用、不掩盖出处)。违反本条政策的检索结果不得进入证据链。
4
+
5
+ ## 1. 定位与适用范围
6
+
7
+ 适用于 `retrieval/` 下的全部通道:
8
+
9
+ | 通道 | 类型 | 是否需 key |
10
+ |---|---|---|
11
+ | OpenAlex / Semantic Scholar / CrossRef | 学术元数据 API | 否 |
12
+ | AIHot | 行业动态源 | 否 |
13
+ | AgentSearch / ArXiv | 预印本检索 | 否 |
14
+ | DuckDuckGo | 通用网页回退 | 否 |
15
+ | Sciverse | 学术检索(含全文) | 是(`SCIVERSE_API_TOKEN`) |
16
+ | Tavily / Brave | 商业搜索 API | 是 |
17
+ | 抓取链(builtin / jina_reader / defuddle / markdown_new / raw_html) | 正文读取 | 否 |
18
+
19
+ ## 2. 抓取前:robots 与访问边界
20
+
21
+ - **尊重 robots.txt**:目标站点在 robots.txt 中禁止抓取的路径不得作为抓取目标;需要该内容时改用其官方 API 或元数据记录,并在筛选中注明"仅元数据"。
22
+ - **不绕过访问控制**:登录墙、付费墙、验证码页面一律视为不可读内容。抓取链会在校验门中把这些页面判为无效(`is_login_page` / `is_captcha_page`),此时正确做法是回到检索阶段寻找开放版本(预印本、机构库、作者主页),**而不是尝试绕过**。
23
+ - **不伪造可读性**:`FETCH_FAILED` / `FETCH_PARTIAL` 的内容不得作为证据;不得由模型根据标题或摘要"补写"正文。
24
+ - **私有地址不外发**:指向本机/私网的目标只允许本地读取,绝不提交给第三方清洗服务(`retrieval/fetch.py` 的 `LOCAL_PROVIDERS` 约束)。
25
+
26
+ ## 3. 限速与重试
27
+
28
+ - 检索按查询批次串行执行,每次 provider 尝试最多重试 1 次(`AuditedSearchExecutor(max_retries=1)`);失败即记录并切换通道,不做无限重试。
29
+ - 单次运行的检索预算由 `SearchPlan.provider_budget` 限定;S/M/L 分级只影响预算,不影响协议。
30
+ - 超时:检索 12 秒、抓取 20 秒;超时按失败处理并进入降级链。
31
+ - 商用 API 通道按其配额与速率限制使用;配额耗尽是运营问题,不构成放宽其他通道约束的理由。
32
+
33
+ ### Sciverse 通道附加约定
34
+
35
+ - 单次 `agentic-search` 的 `top_k` 上限 100,且同一篇论文最多返回约 3 个 chunk;`balanced` 模式服务端约截断至 50 条。
36
+ - `filters` 为**软过滤**语义:chunk 侧元数据缺失的文档不会被排除。按年份等条件过滤时,结论表述必须写"近似范围";需要严格范围时改用 `meta-search` 的结构化字段并核对返回记录。
37
+ - `offset` / `limit` 以 **Unicode 码点**计(与 Python `len` 一致),不是字节;翻页使用返回的 `next_offset`。
38
+ - 引用必须回指论文本身(DOI 或 `unique_id`),**不得把 Sciverse 记为引用目标**——它是读取路径,不是来源。
39
+
40
+ ## 4. 署名与引用
41
+
42
+ - 引用目标永远是被引文献本身(DOI / 正式 URL / 数据库标识),绝不是检索或清洗通道(`r.jina.ai`、`markdown.new`、Sciverse 等)。
43
+ - 作者、年份、标题、期刊按原文记录,不改写、不翻译、不合并同名作者。
44
+ - 使用的每个来源都要能给出可核验的 `source_location`;没有位置的记录标 `needs_manual_location` 进入人工筛选。
45
+ - 撤稿与更正:已引 DOI 通过 `scripts/retraction_watch.py` 定期核查,命中即移除其支撑作用并重新裁决。
46
+
47
+ ## 5. 缓存与留存
48
+
49
+ - 抓取正文在 run workspace 的 `fetch/` 下保存(raw + clean + provenance + fallback_chain),用于复现与审计;该目录随 run 生命周期管理。
50
+ - 检索审计(`search-provenance.json` / `search-attempts.jsonl` / `source-screening.csv` / `exclusion-log.csv`)与 run 同寿命,用于证明"检索确实发生过、范围如何"。
51
+ - 不长期镜像第三方全文;需要长期复用时保存定位信息与哈希,而非内容副本。
52
+ - 私有项目、用户上传数据与本地运行历史一律不进入公共产物(提交包由 `scripts/skill_payload.py` 的显式白名单构建)。
53
+
54
+ ## 6. 凭据处理
55
+
56
+ - API key 只从环境变量读取(`SCIVERSE_API_TOKEN` / `TAVILY_API_KEY` / `BRAVE_API_KEY`);不写入仓库、不写入产物、不进日志。
57
+ - 错误信息只保留状态与简短描述,不携带 Authorization 头或 token 片段。
58
+ - 缺少 key 时通道静默失活并如实上报状态,不影响零配置通道与科学门。
59
+
60
+ ## 7. 失败与例外
61
+
62
+ | 情形 | 处理 |
63
+ |---|---|
64
+ | 站点禁止抓取 | 退回元数据记录并标注;不得绕过。 |
65
+ | 付费墙 | 寻找开放版本;找不到则记 `needs_manual_location`。 |
66
+ | 配额耗尽 | 记录该次尝试;切换零配置通道继续。 |
67
+ | 合规与时效冲突 | 以合规为准;宁可结论标注"证据不足"。 |
68
+
69
+ ## 8. 交叉引用
70
+
71
+ - 检索协议:`references/retrieval-protocol.md`(查询构造、来源分级、饱和规则)
72
+ - 来源有效性:`references/source-validity.md`
73
+ - 抓取与校验实现:`retrieval/fetch.py`、`retrieval/validate.py`
74
+ - Sciverse 通道:`docs/sciverse-api.md`
75
+
@@ -68,6 +68,24 @@ source-validity.md)后才能进入 Evidence Extraction。
68
68
  | RP-03 | 厂商/行业声明(如 Copilot 官方博客、AI 产品宣传页)**一律不得**作为独立证据,即使域名是 `.edu` / `.gov`(需核查内容是否厂商资助)。 |
69
69
  | RP-04 | 无法回溯到原始来源的二手转述,不得进入 Evidence Matrix。 |
70
70
 
71
+ ### 3.1 Sciverse 通道使用规则(key-based 学术通道)
72
+
73
+ Sciverse(`retrieval/sciverse.py`,需 `SCIVERSE_API_TOKEN`)提供引用级学术检索与全文定位。使用时必须遵守以下语义,否则结论口径会被静默夸大:
74
+
75
+ | 编号 | 规则 |
76
+ | --- | --- |
77
+ | RP-SV-01 | `/agentic-search` 返回的 chunk 是**定位子**(`doc_id` + Unicode 码点 `offset`),必须经 `/content` 读出正文并通过 `retrieval/validate.py` 校验门,才可进入 Extract(RULE 2 的机器化)。定位写入 `chunks.jsonl`,标注 `discovery_only_requires_content_fetch`。 |
78
+ | RP-SV-02 | `filters` 是**软过滤**:chunk 元数据缺失的文档不会被排除。按年份等条件过滤时,结论与筛选表必须写"近似范围";需要严格范围时改用 `/meta-search` 的结构化过滤并逐条核对返回记录。 |
79
+ | RP-SV-03 | `offset` / `limit` 以 **Unicode 码点**计(与 Python `len` 一致);翻页使用返回的 `next_offset`,不得用 `bytes_returned` 推算。 |
80
+ | RP-SV-04 | 调用 `/content` 必须显式传 `offset`(省略会返回整篇全文并忽略 `limit`)。 |
81
+ | RP-SV-05 | 同一篇论文最多返回约 3 个 chunk,`balanced` 模式服务端约截断至 50 条;高 `top_k` 需要足够多的不同论文,不得用同一篇的多个 chunk 充当多项独立证据。 |
82
+ | RP-SV-06 | 引用目标永远是论文本身(DOI / `unique_id`),**不得把 Sciverse 记为来源或抓取渠道**。 |
83
+ | RP-SV-07 | 无 DOI、无 URL 的记录标 `needs_manual_location` 进入人工筛选,**禁止伪造定位**。 |
84
+ | RP-SV-08 | 引文链(`/meta-paper-relations`)用于饱和判断与滚雪球检索,对应 `SearchQuery.purpose = citation_chain`,其方向语义(CITATIONS 被引 / REFERENCES 参考文献)必须在记录中保留。 |
85
+ | RP-SV-09 | 通道不可用(无 token / 401 / 429 / 5xx / 网络失败)时按定型状态记录并切换其他通道;不得因配额耗尽放宽证据标准。 |
86
+
87
+ 端点契约与限制见 `docs/sciverse-api.md`;配额、缓存与署名见 `references/retrieval-compliance.md`。
88
+
71
89
  ## 4. 检索轮次与饱和规则
72
90
 
73
91
  ### 4.1 最小轮次
@@ -140,3 +158,5 @@ Skeptic 的 9 项固定任务(skeptic-protocol.md)需要对应的独立查
140
158
  | RP-09 | snippet 与摘要不得直接作为证据内容(RULE 2);检索阶段产物只能是线索。 |
141
159
  | RP-10 | 厂商声明与二手转述不得作为独立证据(RP-03 / RP-04)。 |
142
160
  | RP-11 | 达到饱和规则或数量下限后仍不足的,如实输出 `INSUFFICIENT_SOURCES`,禁止降低纳入标准凑数。 |
161
+
162
+ 来源合规(robots、限速、paywall、署名与凭据)统一见 `references/retrieval-compliance.md`;Sciverse 通道附加约定见本文 §3.1。
@@ -0,0 +1,19 @@
1
+ # EduEvidence Scientific Invariants
2
+
3
+ These rules are protected scientific constraints. Autoresearch may optimize the research process, but must never optimize the evidence set toward a preferred conclusion.
4
+
5
+ 1. **Optimize for decision integrity, not answer confidence.**
6
+ 2. **Optimize the research process, never the conclusion.**
7
+ 3. **Every iteration must improve the evidence state, improve the research system, or teach why an attempted path failed.**
8
+ 4. Validated evidence is append-only regardless of whether it supports, contradicts, is null, or is neutral toward the current decision.
9
+ 5. Search snippets are discovery metadata, never direct evidence.
10
+ 6. Task performance is not learning; direct learning/retention/independent-transfer evidence is required before ADOPT for education interventions.
11
+ 7. Missing uncertainty statistics must never be fabricated.
12
+ 8. Causal estimators fail closed on invalid or unidentified designs.
13
+ 9. StudyDesign requires an explicit evidence-grounded KnowledgeGap.
14
+ 10. Negative search results describe the bounded search scope only; never infer that evidence does not exist globally.
15
+ 11. Evidence Autoresearch may update project research state but may not modify Skill/repository code.
16
+ 12. Skill Autoresearch may modify approved mutable repository surfaces but may not touch real user research state.
17
+ 13. Holdout/gold/evaluator/schema/scientific-invariant surfaces are protected from automatic mutation.
18
+ 14. Canonical project state has a single writer. Subagents return staging artifacts only.
19
+ 15. No autonomous human-subject study launch, policy deployment, main-branch merge, or release.
@@ -0,0 +1,178 @@
1
+ """Auditable, bounded search execution for evidence retrieval.
2
+
3
+ Provider implementations remain deliberately separate. This module records
4
+ the query intent, every provider attempt, screening decisions and saturation
5
+ stop reason so a search result is never mistaken for unobserved provenance.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import csv
10
+ import json
11
+ import time
12
+ from dataclasses import asdict, dataclass, field
13
+ from pathlib import Path
14
+ from typing import Any, Iterable
15
+
16
+ from retrieval.search import SearchHit
17
+ from retrieval.source import parse_doi_from_url, title_fingerprint
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class SearchQuery:
22
+ query_id: str
23
+ query: str
24
+ purpose: str # core | expansion | counter_evidence | citation_chain
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class SearchPlan:
29
+ question: str
30
+ domain: str
31
+ concepts: tuple[str, ...]
32
+ synonyms: tuple[str, ...]
33
+ inclusion_criteria: tuple[str, ...]
34
+ exclusion_criteria: tuple[str, ...]
35
+ queries: tuple[SearchQuery, ...]
36
+ provider_budget: int = 10
37
+ policy_version: str = "2026.09"
38
+
39
+ @classmethod
40
+ def from_question(cls, question: str, *, domain: str = "education",
41
+ concepts: Iterable[str] = (), synonyms: Iterable[str] = ()) -> "SearchPlan":
42
+ terms = tuple(dict.fromkeys(x.strip() for x in (*concepts, *synonyms) if x.strip()))
43
+ core = " ".join(terms) or question
44
+ return cls(question, domain, tuple(concepts), tuple(synonyms), (), (), (
45
+ SearchQuery("Q1", core, "core"),
46
+ SearchQuery("Q2", f"{core} systematic review OR meta-analysis", "expansion"),
47
+ SearchQuery("Q3", f"{core} null negative harm bias limitation", "counter_evidence"),
48
+ ))
49
+
50
+
51
+ @dataclass
52
+ class SearchAttempt:
53
+ attempt_id: str
54
+ query_id: str
55
+ provider: str
56
+ started_at: float
57
+ ended_at: float
58
+ status: str
59
+ result_count: int
60
+ error: str = ""
61
+ retry_index: int = 0
62
+
63
+ def to_dict(self) -> dict[str, Any]:
64
+ value = asdict(self)
65
+ value["latency_ms"] = round((self.ended_at - self.started_at) * 1000, 2)
66
+ return value
67
+
68
+
69
+ def _hit_key(hit: SearchHit) -> tuple[str, str, str]:
70
+ doi = (hit.doi or parse_doi_from_url(hit.url) or "").lower()
71
+ return doi, hit.url.rstrip("/").lower(), title_fingerprint(hit.title)
72
+
73
+
74
+ def dedupe_hits(hits: Iterable[SearchHit]) -> list[SearchHit]:
75
+ """Deduplicate DOI, canonical URL and normalized title, retaining priority."""
76
+ seen: set[tuple[str, str]] = set()
77
+ kept: list[SearchHit] = []
78
+ for hit in sorted(hits, key=lambda item: (item.score, item.citation_count or 0), reverse=True):
79
+ doi, url, title = _hit_key(hit)
80
+ keys = [("doi", doi), ("url", url), ("title", title)]
81
+ nonempty = [key for key in keys if key[1]]
82
+ if any(key in seen for key in nonempty):
83
+ continue
84
+ seen.update(nonempty)
85
+ kept.append(hit)
86
+ return kept
87
+
88
+
89
+ class AuditedSearchExecutor:
90
+ """Runs an explicit plan with bounded retries and durable audit exports."""
91
+
92
+ def __init__(self, providers: Iterable[Any], *, max_retries: int = 1):
93
+ self.providers = list(providers)
94
+ self.max_retries = max_retries
95
+
96
+ def execute(self, plan: SearchPlan, output_dir: Path, *, limit: int = 10) -> list[SearchHit]:
97
+ output_dir = Path(output_dir)
98
+ output_dir.mkdir(parents=True, exist_ok=True)
99
+ attempts: list[SearchAttempt] = []
100
+ hits: list[SearchHit] = []
101
+ attempted: set[tuple[str, str]] = set()
102
+ counter_hits = 0
103
+ for query in plan.queries:
104
+ for provider in self.providers:
105
+ name = getattr(provider, "name", provider.__class__.__name__)
106
+ fingerprint = (name, query.query)
107
+ if fingerprint in attempted:
108
+ continue
109
+ attempted.add(fingerprint)
110
+ for retry in range(self.max_retries + 1):
111
+ started = time.time()
112
+ try:
113
+ result = provider.search(query.query, limit=limit)
114
+ ended = time.time()
115
+ attempts.append(SearchAttempt(
116
+ f"A-{len(attempts) + 1:04d}", query.query_id, name, started, ended,
117
+ "success", len(result), retry_index=retry,
118
+ ))
119
+ hits.extend(result)
120
+ if query.purpose == "counter_evidence":
121
+ counter_hits += len(result)
122
+ break
123
+ except Exception as exc: # failures are recorded, never swallowed
124
+ ended = time.time()
125
+ attempts.append(SearchAttempt(
126
+ f"A-{len(attempts) + 1:04d}", query.query_id, name, started, ended,
127
+ "failed", 0, error=f"{type(exc).__name__}: {exc}", retry_index=retry,
128
+ ))
129
+ unique = dedupe_hits(hits)
130
+ self._write_exports(output_dir, plan, attempts, unique, counter_hits)
131
+ return unique[:limit]
132
+
133
+ @staticmethod
134
+ def _write_exports(output_dir: Path, plan: SearchPlan, attempts: list[SearchAttempt],
135
+ hits: list[SearchHit], counter_hits: int) -> None:
136
+ (output_dir / "search-provenance.json").write_text(json.dumps({
137
+ "plan": {**asdict(plan), "queries": [asdict(q) for q in plan.queries]},
138
+ "search_completed": True,
139
+ "counter_evidence_queries_executed": sum(q.purpose == "counter_evidence" for q in plan.queries),
140
+ "counter_evidence_hit_count": counter_hits,
141
+ "stop_reason": "planned_queries_completed",
142
+ }, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
143
+ (output_dir / "search-attempts.jsonl").write_text(
144
+ "".join(json.dumps(item.to_dict(), ensure_ascii=False) + "\n" for item in attempts), encoding="utf-8")
145
+ # Sciverse chunk locators are a working set, never evidence: /content
146
+ # must expand them and the fetch/validate gate must pass first (RULE 2).
147
+ chunk_records = [
148
+ {"chunk_id": hit.chunk_id, "doc_id": hit.doc_id, "offset": hit.offset,
149
+ "title": hit.title, "doi": hit.doi, "provider": hit.provider,
150
+ "score": hit.score, "locator_state": "discovery_only_requires_content_fetch"}
151
+ for hit in hits
152
+ if getattr(hit, "doc_id", None)
153
+ ]
154
+ if chunk_records:
155
+ (output_dir / "chunks.jsonl").write_text(
156
+ "".join(json.dumps(record, ensure_ascii=False) + "\n" for record in chunk_records),
157
+ encoding="utf-8")
158
+ with (output_dir / "source-screening.csv").open("w", newline="", encoding="utf-8") as fh:
159
+ writer = csv.DictWriter(fh, fieldnames=["title", "doi", "url", "provider", "year",
160
+ "doc_id", "chunk_id", "offset", "screening_status", "reason"])
161
+ writer.writeheader()
162
+ for hit in hits:
163
+ doi = hit.doi or parse_doi_from_url(hit.url) or ""
164
+ url = hit.url
165
+ # Never fabricate a location for a DOI-less record; the explicit
166
+ # marker routes it to manual screening instead.
167
+ if not url and not doi:
168
+ url = "needs_manual_location"
169
+ writer.writerow({"title": hit.title, "doi": doi, "url": url,
170
+ "provider": hit.provider, "year": hit.year or "",
171
+ "doc_id": getattr(hit, "doc_id", None) or "",
172
+ "chunk_id": getattr(hit, "chunk_id", None) or "",
173
+ "offset": getattr(hit, "offset", None) if getattr(hit, "offset", None) is not None else "",
174
+ "screening_status": "candidate",
175
+ "reason": "discovery metadata only; fetch and validation required before evidence extraction"})
176
+ with (output_dir / "exclusion-log.csv").open("w", newline="", encoding="utf-8") as fh:
177
+ writer = csv.DictWriter(fh, fieldnames=["identifier", "reason"])
178
+ writer.writeheader()
@@ -210,6 +210,98 @@ def _fetch_raw_html(url: str, timeout: int) -> tuple[int, str, str]:
210
210
  return _http_get(url, timeout=timeout)
211
211
 
212
212
 
213
+ def fetch_sciverse_content(
214
+ doc_id: str,
215
+ *,
216
+ offset: int = 0,
217
+ limit: int = 4096,
218
+ canonical_url: str | None = None,
219
+ expect_title: str | None = None,
220
+ ) -> dict[str, Any]:
221
+ """Read a Sciverse full-text slice through the same FetchResult contract.
222
+
223
+ This is the machine-enforced half of RULE 2: a ``/agentic-search`` chunk is
224
+ a locator (doc_id + code-point offset), and only the text returned here —
225
+ after passing the Fetch Validation Gate — may enter Evidence Extraction.
226
+
227
+ ``offset``/``limit`` count Unicode code points, exactly like Python ``len``.
228
+ The locator is recorded inside ``extensions`` because the FetchResult
229
+ contract is closed (``additionalProperties: false``). ``canonical_url``
230
+ carries the source's own citation pointer when one exists; the locator URL
231
+ is used otherwise and is a provenance pointer, never a citation target.
232
+ """
233
+ from retrieval.sciverse import (
234
+ STATUS_OK,
235
+ STATUS_UNAVAILABLE,
236
+ read_content,
237
+ )
238
+
239
+ fetched_at = datetime.now(timezone.utc).isoformat()
240
+ resolved_url = canonical_url or f"https://sciverse.space/doc/{doc_id}"
241
+ response = read_content(doc_id, offset=offset, limit=limit)
242
+
243
+ if response.status == STATUS_UNAVAILABLE:
244
+ return FetchResult(
245
+ original_url=resolved_url,
246
+ fetch_provider="sciverse_content",
247
+ fetch_status="FETCH_FAILED",
248
+ fetched_at=fetched_at,
249
+ validation={"passed": False,
250
+ "checks": {"http_success": False, "body_length_ok": False},
251
+ "issues": ["SCIVERSE_UNAVAILABLE: no API token configured"]},
252
+ extensions={"sciverse": {"doc_id": doc_id, "offset": offset,
253
+ "status": response.status, "error": response.error}},
254
+ ).to_dict()
255
+
256
+ if response.status != STATUS_OK:
257
+ return FetchResult(
258
+ original_url=resolved_url,
259
+ fetch_provider="sciverse_content",
260
+ fetch_status="FETCH_FAILED",
261
+ fetched_at=fetched_at,
262
+ validation={"passed": False,
263
+ "checks": {"http_success": False, "body_length_ok": False},
264
+ "issues": [f"{response.status}: {response.error}".strip(": ")]},
265
+ extensions={"sciverse": {"doc_id": doc_id, "offset": offset,
266
+ "status": response.status,
267
+ "http_status": response.http_status,
268
+ "error": response.error}},
269
+ ).to_dict()
270
+
271
+ content = str(response.data.get("text") or "")
272
+ next_offset = response.data.get("next_offset")
273
+ more = bool(response.data.get("more"))
274
+ candidate = FetchResult(
275
+ original_url=resolved_url,
276
+ resolved_url=resolved_url,
277
+ fetch_method="smart_web_fetch",
278
+ fetch_provider="sciverse_content",
279
+ fetch_status="FETCH_VALID" if content.strip() else "FETCH_FAILED",
280
+ fetched_at=fetched_at,
281
+ raw_size=len(content.encode("utf-8")),
282
+ content=content,
283
+ extensions={"sciverse": {
284
+ "doc_id": doc_id,
285
+ "offset": offset,
286
+ "limit": limit,
287
+ "next_offset": next_offset,
288
+ "more": more,
289
+ "location_unit": "unicode_code_point",
290
+ }},
291
+ )
292
+ candidate.clean_size = candidate.raw_size
293
+ candidate.content_length = candidate.raw_size
294
+ candidate.content_hash = _hash(content) if content else ""
295
+ # A Sciverse locator URL is not an http(s) target: the scheme/URL-match and
296
+ # private-target checks do not apply; length and error-page checks do.
297
+ candidate.validation = validate_fetch_result(
298
+ candidate.to_dict(), expect_title=expect_title,
299
+ )
300
+ if not candidate.validation.get("passed") and candidate.fetch_status == "FETCH_VALID":
301
+ candidate.fetch_status = "FETCH_PARTIAL"
302
+ return candidate.to_dict()
303
+
304
+
213
305
  _PROVIDER_FETCHERS: dict[str, Callable[[str, int], tuple[int, str, str]]] = {
214
306
  "builtin": _fetch_builtin,
215
307
  "jina_reader": _fetch_jina_reader,
@@ -299,6 +391,9 @@ class FetchResult:
299
391
  fallback_chain: list[str] = field(default_factory=list)
300
392
  content: str = ""
301
393
  validation: dict[str, Any] = field(default_factory=dict)
394
+ #: Structured extension container (schema: fetch-result.extensions).
395
+ #: Sciverse locators live here because the top level is closed.
396
+ extensions: dict[str, Any] = field(default_factory=dict)
302
397
 
303
398
  def to_dict(self) -> dict[str, Any]:
304
399
  return {
@@ -317,6 +412,7 @@ class FetchResult:
317
412
  "fallback_chain": self.fallback_chain,
318
413
  "content": self.content if self.fetch_status != "FETCH_FAILED" else "",
319
414
  "validation": self.validation,
415
+ "extensions": self.extensions,
320
416
  }
321
417
 
322
418