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,30 @@
1
+ from __future__ import annotations
2
+ import argparse
3
+ import json
4
+ from engine.autoevolve import DailyEvolutionRunner, DailyProfile
5
+
6
+
7
+ def main(argv=None):
8
+ parser = argparse.ArgumentParser(description="Run bounded branch-only EduEvidence evolution")
9
+ parser.add_argument("--repo", default=".")
10
+ parser.add_argument("--agent-command", required=True)
11
+ parser.add_argument("--eval-command", required=True)
12
+ parser.add_argument("--run-tag")
13
+ parser.add_argument("--max-experiments", type=int, default=20)
14
+ parser.add_argument("--max-cost-usd", type=float, default=5)
15
+ parser.add_argument("--max-wall-minutes", type=int, default=180)
16
+ parser.add_argument("--push-branch", action="store_true")
17
+ args = parser.parse_args(argv)
18
+ profile = DailyProfile(args.max_experiments, args.max_cost_usd, args.max_wall_minutes)
19
+ report = DailyEvolutionRunner(args.repo, profile=profile).run(
20
+ agent_command=args.agent_command,
21
+ eval_command=args.eval_command,
22
+ run_tag=args.run_tag,
23
+ push_branch=args.push_branch,
24
+ )
25
+ print(json.dumps(report, indent=2))
26
+ return 0
27
+
28
+
29
+ if __name__ == "__main__":
30
+ raise SystemExit(main())
@@ -18,20 +18,29 @@ Usage:
18
18
  from __future__ import annotations
19
19
 
20
20
  import argparse
21
+ import errno
22
+ import mimetypes
21
23
  import http.server
22
24
  import json
25
+ import os
23
26
  import sys
24
27
  import urllib.parse
25
28
  from pathlib import Path
29
+ from typing import Any, Dict, List, Optional
26
30
 
27
31
  ROOT = Path(__file__).resolve().parent.parent
28
- WEB_DIR = ROOT / "web"
29
- EXAMPLES_DIR = ROOT / "examples"
30
32
 
31
33
  if str(ROOT) not in sys.path:
32
34
  sys.path.insert(0, str(ROOT))
33
35
 
36
+ from engine._resources import resource_root # noqa: E402
37
+ ROOT = resource_root()
38
+ WEB_DIR = ROOT / "web"
39
+ EXAMPLES_DIR = ROOT / "examples"
40
+ RESEARCH_HOME = Path(os.environ.get("EDUEVIDENCE_HOME", Path.cwd() / ".eduevidence"))
41
+
34
42
  from engine.evidence_graph import EvidenceGraph # noqa: E402
43
+ from engine.studio_read_model import StudioReader, numeric_effect # noqa: E402
35
44
 
36
45
  import sys as _sys
37
46
  _VIZ_SCRIPTS = ROOT / "visualization" / "eduevidence-report" / "scripts"
@@ -39,8 +48,6 @@ if str(_VIZ_SCRIPTS) not in _sys.path:
39
48
  _sys.path.insert(0, str(_VIZ_SCRIPTS))
40
49
  from zh_labels import OUTCOME_ZH, ACTION_ZH, STUDY_ZH, AUTHORITY_ZH, CONFIDENCE_ZH # noqa: E402
41
50
 
42
- # Friendly display names for known demo projects; everything else falls back to
43
- # the question text inside result.json so new projects need no manual entry.
44
51
  PROJECT_TITLES: Dict[str, str] = {
45
52
  "ai-coding-assistant-evidence": "高校大一引入 AI 编程助手(真实文献旗舰示例)",
46
53
  "highschool-math-ai-tutor": "高中数学引入大模型自适应 AI Tutor 评估",
@@ -51,7 +58,6 @@ PROJECT_TITLES: Dict[str, str] = {
51
58
 
52
59
 
53
60
  def unquote_path(path: str) -> str:
54
- """Decode a URL path once and reject encoded separators / NUL bytes."""
55
61
  decoded = urllib.parse.unquote(path)
56
62
  if "\x00" in decoded or "\\" in decoded:
57
63
  raise ValueError("unsafe path characters")
@@ -66,7 +72,6 @@ def _read_json(path: Path) -> Optional[Any]:
66
72
 
67
73
 
68
74
  def _decision_fields(decision: Any) -> tuple[Any, Any]:
69
- """Return (verdict, confidence) tolerating the three decision shapes in the wild."""
70
75
  if not isinstance(decision, dict):
71
76
  return None, None
72
77
  verdict = decision.get("verdict") or decision.get("recommended_action") or decision.get("decision")
@@ -77,21 +82,11 @@ def _decision_fields(decision: Any) -> tuple[Any, Any]:
77
82
 
78
83
 
79
84
  def _extract_effect(ev: Dict[str, Any]) -> tuple[Any, Any, Any]:
80
- """Return (value, ci_lower, ci_upper) from both nested and flat effect_size shapes."""
81
- es = ev.get("effect_size")
82
- if isinstance(es, dict):
83
- return es.get("value"), (es.get("ci_lower") or es.get("ci_lo")), (es.get("ci_upper") or es.get("ci_hi"))
84
- value = es if isinstance(es, (int, float)) else None
85
- if value is None:
86
- for key in ("hedges_g", "g", "effect_size_value"):
87
- if isinstance(ev.get(key), (int, float)):
88
- value = ev[key]
89
- break
90
- return value, ev.get("ci_lower"), ev.get("ci_upper")
85
+ numeric = numeric_effect(ev)
86
+ return numeric["value"], numeric["ci_lower"], numeric["ci_upper"]
91
87
 
92
88
 
93
89
  def _graph_node_count(path: Path) -> int:
94
- """与 evidence_graph.export_echarts_graph() 同口径的节点数。"""
95
90
  try:
96
91
  graph = EvidenceGraph.from_json(path.read_text(encoding="utf-8"))
97
92
  export = graph.export_echarts_graph()
@@ -108,13 +103,12 @@ def _graph_node_count(path: Path) -> int:
108
103
 
109
104
 
110
105
  def _direction_counts(evidence: list) -> Dict[str, int]:
111
- """按 evidence 的 relation_to_claim / direction / effect_direction 聚合。"""
112
106
  counts = {"support": 0, "contradict": 0, "neutral": 0}
113
107
  for ev in evidence:
114
108
  if not isinstance(ev, dict):
115
109
  continue
116
110
  d = (ev.get("relation_to_claim") or ev.get("direction")
117
- or ev.get("effect_direction") or "").lower()
111
+ or "").lower()
118
112
  if d in ("support", "supports", "positive", "pos"):
119
113
  counts["support"] += 1
120
114
  elif d in ("contradict", "contradicts", "negative", "neg"):
@@ -125,14 +119,13 @@ def _direction_counts(evidence: list) -> Dict[str, int]:
125
119
 
126
120
 
127
121
  def _outcome_rollup(evidence: list) -> List[Dict[str, Any]]:
128
- """无 outcome_mapping 时按 outcome_type 聚合方向计数(回退链)。"""
129
122
  roll: Dict[str, Dict[str, int]] = {}
130
123
  for ev in evidence:
131
124
  if not isinstance(ev, dict):
132
125
  continue
133
126
  ot = ev.get("outcome_type") or ev.get("outcome") or "other"
134
127
  d = (ev.get("relation_to_claim") or ev.get("direction")
135
- or ev.get("effect_direction") or "").lower()
128
+ or "").lower()
136
129
  bucket = roll.setdefault(ot, {"support": 0, "contradict": 0, "neutral": 0})
137
130
  if d in ("support", "supports", "positive", "pos"):
138
131
  bucket["support"] += 1
@@ -144,7 +137,6 @@ def _outcome_rollup(evidence: list) -> List[Dict[str, Any]]:
144
137
 
145
138
 
146
139
  def _known_project_ids() -> set:
147
- """Project ids = directories with a real result.json (not the deduped display list)."""
148
140
  if not EXAMPLES_DIR.exists():
149
141
  return set()
150
142
  return {d.name for d in EXAMPLES_DIR.iterdir()
@@ -157,18 +149,19 @@ def scan_local_projects() -> List[Dict[str, Any]]:
157
149
  return projects
158
150
 
159
151
  for proj_dir in sorted(EXAMPLES_DIR.iterdir()):
152
+ # List canonical examples once; legacy aliases remain valid lookup IDs.
153
+ if proj_dir.is_symlink():
154
+ continue
160
155
  if not proj_dir.is_dir():
161
156
  continue
162
157
  result_path = proj_dir / "result.json"
163
158
  if not result_path.exists():
164
159
  continue
165
-
166
160
  result = _read_json(result_path) or {}
167
161
  meta = result.get("meta") or {}
168
162
  frame = result.get("research_frame") or {}
169
163
  question = meta.get("question") or frame.get("question") or ""
170
164
  verdict, confidence = _decision_fields(result.get("decision"))
171
-
172
165
  evidence = result.get("evidence") or []
173
166
  forest = result.get("forest_plot_data") or []
174
167
  effect_values: List[float] = []
@@ -179,48 +172,48 @@ def scan_local_projects() -> List[Dict[str, Any]]:
179
172
  for f in forest:
180
173
  if isinstance(f.get("effect_size"), (int, float)):
181
174
  effect_values.append(float(f["effect_size"]))
182
-
183
175
  graph_path = proj_dir / "evidence_graph.json"
184
- html_path = proj_dir / "EduEvidence_Report.html"
176
+ html_path = proj_dir / "reports-5themes" / "EduEvidence_Report_claude.html"
177
+ if not html_path.is_file():
178
+ html_path = proj_dir / "EduEvidence_Report.html"
185
179
  report_variants: List[Dict[str, str]] = []
186
180
  themes_dir = proj_dir / "reports-5themes"
187
181
  if themes_dir.is_dir():
188
182
  for variant_file in sorted(list(themes_dir.glob("EduEvidence_Report_*.html")) + list(themes_dir.glob("report_*.html"))):
189
183
  theme_name = variant_file.stem.replace("EduEvidence_Report_", "").replace("report_", "")
190
184
  if not any(v["theme"] == theme_name for v in report_variants):
191
- report_variants.append({
192
- "theme": theme_name,
193
- "path": str(variant_file),
194
- })
195
-
185
+ report_variants.append({"theme": theme_name, "path": str(variant_file)})
196
186
  zh_result = _read_json(proj_dir / "result.zh.json") or {}
197
187
  zh_question = ((zh_result.get("meta") or {}).get("question")
198
188
  or (zh_result.get("research_frame") or {}).get("question") or "")
199
-
200
189
  projects.append({
201
190
  "id": proj_dir.name,
202
191
  "title": PROJECT_TITLES.get(proj_dir.name) or (zh_question[:72] if zh_question else (question[:72] or proj_dir.name)),
203
192
  "title_zh": zh_question[:72] if zh_question else None,
204
- "domain": meta.get("domain") or "education",
193
+ "domain": meta.get("domain") or result.get("research_frame", {}).get("extensions", {}).get("domain") or "education",
205
194
  "question": question,
206
195
  "verdict": verdict,
207
196
  "confidence": confidence,
208
197
  "evidence_count": len(evidence) if isinstance(evidence, list) else 0,
209
198
  "forest_count": len(forest) if isinstance(forest, list) else 0,
210
- "effect_count": len(effect_values),
211
- "mean_effect_size": round(sum(effect_values) / len(effect_values), 3) if effect_values else None,
212
- "direction_counts": _direction_counts(evidence) if isinstance(evidence, list) else
213
- {"support": 0, "contradict": 0, "neutral": 0},
199
+ "effect_count": len({
200
+ ev.get("evidence_id") or ev.get("finding_id") or f"e-{i}"
201
+ for i, ev in enumerate(evidence) if _extract_effect(ev)[0] is not None
202
+ } | {
203
+ f.get("evidence_id") or f.get("finding_id") or f"f-{i}"
204
+ for i, f in enumerate(forest) if _extract_effect(f)[0] is not None
205
+ }),
206
+ "mean_effect_size": None, # Distinct outcomes/metrics cannot be averaged by a viewer.
207
+ "direction_counts": _direction_counts(evidence) if isinstance(evidence, list) else {"support": 0, "contradict": 0, "neutral": 0},
214
208
  "has_graph": graph_path.exists(),
215
209
  "node_count": _graph_node_count(graph_path) if graph_path.exists() else 0,
216
210
  "html_report_path": str(html_path) if html_path.exists() else None,
217
211
  "report_variants": report_variants,
218
212
  })
219
- # Canonical question deduplication: keep the project with the highest evidence count
220
213
  deduped: Dict[str, Dict[str, Any]] = {}
221
214
  for p in projects:
222
215
  norm_q = (p.get("question") or p["id"]).strip().lower()
223
- if norm_q not in deduped or (p["evidence_count"] > deduped[norm_q]["evidence_count"]):
216
+ if norm_q not in deduped or p["evidence_count"] > deduped[norm_q]["evidence_count"]:
224
217
  deduped[norm_q] = p
225
218
  return list(deduped.values())
226
219
 
@@ -235,12 +228,6 @@ def build_stats(projects: List[Dict[str, Any]]) -> Dict[str, Any]:
235
228
 
236
229
 
237
230
  def get_aggregate_stats(projects: List[Dict[str, Any]]) -> Dict[str, Any]:
238
- """V2 兼容封装(旧 API/测试沿用):build_stats 字段。
239
-
240
- Token / 成本矩阵已移除(provenance 纠偏):历史版本在此返回硬编码的
241
- 假 token 用量与模型成本,没有任何真实运行记录支撑。运行时未采集
242
- usage 前,这些指标一律如实标注 NOT_CAPTURED,不再虚构数值。
243
- """
244
231
  stats = build_stats(projects)
245
232
  stats["total_tokens"] = None
246
233
  stats["usage_measurement_status"] = "NOT_CAPTURED"
@@ -255,7 +242,6 @@ def build_viz_payload(proj_id: str) -> Dict[str, Any]:
255
242
  question = meta.get("question") or frame.get("question") or ""
256
243
  verdict, confidence = _decision_fields(result.get("decision"))
257
244
  title = PROJECT_TITLES.get(proj_id) or question[:72] or proj_id
258
-
259
245
  forest_items: List[Dict[str, Any]] = []
260
246
  for f in (result.get("forest_plot_data") or []):
261
247
  forest_items.append({
@@ -269,7 +255,6 @@ def build_viz_payload(proj_id: str) -> Dict[str, Any]:
269
255
  "direction": f.get("direction"),
270
256
  "wwc_rating": f.get("wwc_rating") or "",
271
257
  })
272
-
273
258
  effect_items: List[Dict[str, Any]] = []
274
259
  for ev in (result.get("evidence") or []):
275
260
  value, lo, hi = _extract_effect(ev)
@@ -293,7 +278,6 @@ def build_viz_payload(proj_id: str) -> Dict[str, Any]:
293
278
  "outcome_dimension": f.get("outcome_dimension") or f.get("outcome_metric") or "",
294
279
  "direction": f.get("direction"),
295
280
  })
296
-
297
281
  evidence_list = result.get("evidence") or []
298
282
  outcome_items: List[Dict[str, Any]] = []
299
283
  om = result.get("outcome_mapping") or {}
@@ -308,13 +292,8 @@ def build_viz_payload(proj_id: str) -> Dict[str, Any]:
308
292
  "neutral_count": e.get("neutral_count") or 0,
309
293
  })
310
294
  else:
311
- # 回退链:无 outcome_mapping 时按 evidence 方向聚合
312
295
  outcome_items = _outcome_rollup(evidence_list)
313
-
314
- # 方向分布(任何课题都有值)
315
- direction_counts = _direction_counts(evidence_list) if isinstance(evidence_list, list) else \
316
- {"support": 0, "contradict": 0, "neutral": 0}
317
-
296
+ direction_counts = _direction_counts(evidence_list) if isinstance(evidence_list, list) else {"support": 0, "contradict": 0, "neutral": 0}
318
297
  graph = None
319
298
  graph_path = proj_dir / "evidence_graph.json"
320
299
  if graph_path.exists():
@@ -322,7 +301,6 @@ def build_viz_payload(proj_id: str) -> Dict[str, Any]:
322
301
  graph = EvidenceGraph.from_json(graph_path.read_text(encoding="utf-8")).export_echarts_graph()
323
302
  except Exception:
324
303
  graph = None
325
-
326
304
  return {
327
305
  "id": proj_id,
328
306
  "title": title,
@@ -344,26 +322,18 @@ def build_viz_payload(proj_id: str) -> Dict[str, Any]:
344
322
  }
345
323
 
346
324
 
347
-
348
-
349
-
350
-
351
-
352
-
353
325
  class StudioHandler(http.server.SimpleHTTPRequestHandler):
354
- """Serves web/ statically and exposes the artifact JSON API."""
355
-
356
326
  def __init__(self, *args: Any, **kwargs: Any) -> None:
357
327
  super().__init__(*args, directory=str(WEB_DIR), **kwargs)
358
328
 
359
- # -- response helpers -------------------------------------------------
360
329
  def _send_bytes(self, data: bytes, content_type: str, status: int = 200,
361
330
  extra_headers: Optional[Dict[str, str]] = None) -> None:
362
331
  self.send_response(status)
363
332
  self.send_header("Content-Type", content_type)
364
333
  self.send_header("Content-Length", str(len(data)))
365
- # 允许落地页(独立静态源)跨源探测/拉取只读 API 与控制台资源
366
- self.send_header("Access-Control-Allow-Origin", "*")
334
+ self.send_header("X-Content-Type-Options", "nosniff")
335
+ self.send_header("Referrer-Policy", "no-referrer")
336
+ self.send_header("Cache-Control", "no-store")
367
337
  if extra_headers:
368
338
  for name, value in extra_headers.items():
369
339
  self.send_header(name, value)
@@ -371,8 +341,6 @@ class StudioHandler(http.server.SimpleHTTPRequestHandler):
371
341
  self.wfile.write(data)
372
342
 
373
343
  def _send_report_bytes(self, data: bytes) -> None:
374
- # Reports are static, self-contained documents: no remote origins, no
375
- # same-origin access; sandbox-friendly CSP + X-Content-Type-Options.
376
344
  self._send_bytes(data, "text/html; charset=utf-8", extra_headers={
377
345
  "Content-Security-Policy":
378
346
  "default-src 'none'; script-src 'unsafe-inline'; "
@@ -391,27 +359,101 @@ class StudioHandler(http.server.SimpleHTTPRequestHandler):
391
359
  else:
392
360
  self._send_json({"error": "not found"}, 404)
393
361
 
394
-
395
- # -- POST: disabled (read-only Studio) --------------------------------
396
362
  def do_POST(self) -> None:
397
363
  self._send_json({"error": "method not allowed"}, 405)
398
364
 
399
- # -- GET --------------------------------------------------------------
400
365
  def do_GET(self) -> None:
401
366
  parsed = urllib.parse.urlparse(self.path)
402
367
  path = parsed.path
403
368
  query = urllib.parse.parse_qs(parsed.query)
404
-
405
- # Only the three-page Studio entry + the read-only artifact API.
369
+ try:
370
+ host = urllib.parse.urlsplit("//" + self.headers.get("Host", "")).hostname
371
+ except ValueError:
372
+ self._send_json({"error": "invalid host"}, 403)
373
+ return
374
+ allowed_hosts = {"localhost", "127.0.0.1", "::1", str(self.server.server_address[0])}
375
+ if path.startswith("/api/") and host not in allowed_hosts:
376
+ self._send_json({"error": "untrusted host"}, 403)
377
+ return
378
+ if path.startswith("/api/") and self.headers.get("Sec-Fetch-Site") == "cross-site":
379
+ self._send_json({"error": "cross-origin access denied"}, 403)
380
+ return
381
+ if path == "/api/studio/catalog":
382
+ self._send_json(StudioReader(EXAMPLES_DIR, RESEARCH_HOME).catalog())
383
+ return
384
+ if path == "/api/studio/evolution":
385
+ self._send_json(StudioReader(EXAMPLES_DIR, RESEARCH_HOME).evolution())
386
+ return
387
+ if path.startswith("/api/studio/projects/"):
388
+ reader = StudioReader(EXAMPLES_DIR, RESEARCH_HOME)
389
+ suffix = urllib.parse.unquote(path[len("/api/studio/projects/"):])
390
+ parts = suffix.split("/")
391
+ try:
392
+ if len(parts) == 1:
393
+ self._send_json(reader.detail(parts[0]))
394
+ elif len(parts) == 2 and parts[1] == "report":
395
+ report = reader.report_path(parts[0], theme=query.get("theme", ["claude"])[0],
396
+ filename=query.get("file", [None])[0])
397
+ self._send_report_bytes(report.read_bytes())
398
+ else:
399
+ self._send_json({"error": "not found"}, 404)
400
+ except FileNotFoundError:
401
+ self._send_json({"error": "not found"}, 404)
402
+ except (OSError, ValueError, TypeError, AttributeError):
403
+ self._send_json({"error": "research projection unavailable"}, 422)
404
+ return
405
+ if path.startswith("/studio/"):
406
+ try:
407
+ asset = (WEB_DIR / unquote_path(path).lstrip("/")).resolve()
408
+ if not asset.is_relative_to((WEB_DIR / "studio").resolve()):
409
+ raise ValueError("unsafe path")
410
+ if asset.is_dir():
411
+ asset = asset / "index.html"
412
+ if asset.suffix not in {".html", ".js", ".css", ".json", ".svg", ".png", ".woff2", ".txt"}:
413
+ raise ValueError("unsupported asset")
414
+ self._serve_file(asset, mimetypes.guess_type(asset)[0] or "application/octet-stream")
415
+ except (ValueError, OSError):
416
+ self._send_json({"error": "not found"}, 404)
417
+ return
406
418
  if path in ("/", "/index.html", "/dashboard", "/studio", "/console"):
407
- self._serve_file(WEB_DIR / "index.html", "text/html; charset=utf-8")
419
+ built = WEB_DIR / "studio" / "index.html"
420
+ if built.is_file():
421
+ page = built.read_text(encoding="utf-8").replace("<head>", '<head><base href="/studio/">', 1)
422
+ self._send_bytes(page.encode("utf-8"), "text/html; charset=utf-8")
423
+ else:
424
+ self._serve_file(WEB_DIR / "index.html", "text/html; charset=utf-8")
408
425
  return
409
-
410
426
  if path == "/api/projects":
411
427
  projects = scan_local_projects()
412
428
  self._send_json({"projects": projects, "stats": build_stats(projects)})
413
429
  return
414
-
430
+ if path == "/api/research/projects":
431
+ self._send_json({"projects": [p for p in StudioReader(EXAMPLES_DIR, RESEARCH_HOME).catalog()["projects"] if p["kind"] == "project"]})
432
+ return
433
+ if path.startswith("/api/research/projects/"):
434
+ suffix = urllib.parse.unquote(path[len("/api/research/projects/"):]).strip("/")
435
+ parts = suffix.split("/")
436
+ project_id = parts[0]
437
+ if not project_id.startswith("PRJ-"):
438
+ self._send_json({"error": "unknown project"}, 404)
439
+ return
440
+ try:
441
+ detail = StudioReader(EXAMPLES_DIR, RESEARCH_HOME).detail("project--" + project_id)
442
+ if len(parts) == 2 and parts[1] == "runs":
443
+ self._send_json({"runs": detail["runs"]})
444
+ return
445
+ if len(parts) == 2 and parts[1] == "artifacts":
446
+ self._send_json({"artifacts": detail["artifacts"]})
447
+ return
448
+ if len(parts) == 2 and parts[1] == "events":
449
+ after = int(query.get("after_seq", ["0"])[0])
450
+ self._send_json({"events": [e for e in reversed(detail["events"]) if e.get("seq", 0) > max(after, 0)]})
451
+ return
452
+ except (FileNotFoundError, ValueError):
453
+ self._send_json({"error": "unknown project"}, 404)
454
+ return
455
+ self._send_json({"error": "not found"}, 404)
456
+ return
415
457
  if path == "/api/labels":
416
458
  self._send_json({
417
459
  "outcomes": OUTCOME_ZH,
@@ -421,7 +463,6 @@ class StudioHandler(http.server.SimpleHTTPRequestHandler):
421
463
  "confidence": CONFIDENCE_ZH,
422
464
  })
423
465
  return
424
-
425
466
  if path.startswith("/api/projects/"):
426
467
  suffix = urllib.parse.unquote(path[len("/api/projects/"):]).rstrip("/")
427
468
  if suffix.endswith("/viz"):
@@ -433,21 +474,15 @@ class StudioHandler(http.server.SimpleHTTPRequestHandler):
433
474
  return
434
475
  self._send_json({"error": "not found"}, 404)
435
476
  return
436
-
437
477
  if path == "/report":
438
478
  self._serve_report(query)
439
479
  return
440
-
441
- # Static assets only from web/ (allowlisted), with URL-decode once and
442
- # containment; everything else is a JSON 404 (never super().do_GET()).
443
- if path.startswith("/js/") or path.startswith("/css/") or path == "/styles.css":
480
+ if path.startswith("/js/") or path.startswith("/css/") or path in ("/styles.css", "/mobile-studio.css"):
444
481
  self._serve_web_asset(path)
445
482
  return
446
-
447
483
  self._send_json({"error": "not found"}, 404)
448
484
 
449
485
  def _serve_web_asset(self, path: str) -> None:
450
- """Serve a static asset strictly inside WEB_DIR (decode once, no escapes)."""
451
486
  try:
452
487
  candidate = (WEB_DIR / unquote_path(path).lstrip("/")).resolve()
453
488
  except (ValueError, OSError):
@@ -460,22 +495,20 @@ class StudioHandler(http.server.SimpleHTTPRequestHandler):
460
495
  if not candidate.is_file() or candidate.suffix not in (".js", ".css"):
461
496
  self._send_json({"error": "not found"}, 404)
462
497
  return
463
- ctype = "application/javascript; charset=utf-8" if candidate.suffix == ".js" \
464
- else "text/css; charset=utf-8"
498
+ ctype = "application/javascript; charset=utf-8" if candidate.suffix == ".js" else "text/css; charset=utf-8"
465
499
  self._send_bytes(candidate.read_bytes(), ctype)
466
500
 
467
501
  def _serve_report(self, query: Dict[str, List[str]]) -> None:
468
- """Serve a baked report for a known project and explicit theme."""
469
502
  proj_id = query.get("id", [""])[0]
470
503
  theme = query.get("theme", ["default"])[0]
471
-
472
504
  if proj_id not in _known_project_ids():
473
505
  self._send_json({"error": "unknown project"}, 404)
474
506
  return
475
-
476
507
  proj_dir = EXAMPLES_DIR / proj_id
477
508
  if theme == "default":
478
- html_path = proj_dir / "EduEvidence_Report.html"
509
+ html_path = proj_dir / "reports-5themes" / "EduEvidence_Report_claude.html"
510
+ if not html_path.is_file():
511
+ html_path = proj_dir / "EduEvidence_Report.html"
479
512
  if not html_path.is_file():
480
513
  self._send_json({"error": "report not found"}, 404)
481
514
  return
@@ -485,13 +518,12 @@ class StudioHandler(http.server.SimpleHTTPRequestHandler):
485
518
  return
486
519
  variants_dir = proj_dir / "reports-5themes"
487
520
  html_path = variants_dir / f"EduEvidence_Report_{theme}.html"
488
- if (not html_path.is_file() or
489
- html_path.resolve().parent != variants_dir.resolve()):
521
+ if not html_path.is_file() or html_path.resolve().parent != variants_dir.resolve():
490
522
  self._send_json({"error": "unknown report theme"}, 404)
491
523
  return
492
-
493
524
  self._send_report_bytes(html_path.read_bytes())
494
525
 
526
+
495
527
  def run_dashboard_server(host: str = "127.0.0.1", port: int = 8765) -> None:
496
528
  server = None
497
529
  actual_port = port
@@ -503,20 +535,17 @@ def run_dashboard_server(host: str = "127.0.0.1", port: int = 8765) -> None:
503
535
  actual_port = candidate
504
536
  break
505
537
  except OSError as e:
506
- if e.errno == 48:
538
+ if e.errno == errno.EADDRINUSE:
507
539
  continue
508
540
  raise
509
-
510
541
  if server is None:
511
542
  print(f"❌ 端口 {port}-{port + 9} 均被占用。")
512
543
  return
513
-
514
544
  print("============================================================")
515
545
  print(f"🚀 EduEvidence Web Studio running at http://{host}:{actual_port}/")
516
- print(" 📊 仪表盘 /dashboard")
517
- print(" 📄 报告浏览 /report")
518
- print(" 📈 数据可视化 /#viz (Web UI)")
519
- print(" 📦 数据契约 /api/projects · /api/projects/<id>/viz")
546
+ print(" Research Studio /studio/ (read-only)")
547
+ print(" Projects, evidence, revisions and five-theme reports")
548
+ print(" Projection API /api/studio/catalog")
520
549
  print("============================================================")
521
550
  try:
522
551
  server.serve_forever()