eduevidence 5.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 (312) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +506 -0
  3. package/README.zh-CN.md +434 -0
  4. package/SKILL.md +195 -0
  5. package/bin/eduevidence.js +127 -0
  6. package/domains/education/manifest.json +183 -0
  7. package/domains/education/outcome_taxonomy.json +127 -0
  8. package/domains/manifest.json +26 -0
  9. package/domains/policy/frame.schema.json +234 -0
  10. package/domains/policy/manifest.json +10 -0
  11. package/domains/policy/methodology_checklist.json +109 -0
  12. package/domains/policy/outcome_taxonomy.json +53 -0
  13. package/domains/policy/references/causal-identification.md +45 -0
  14. package/domains/policy/references/cost-evidence.md +44 -0
  15. package/domains/policy/references/equity.md +42 -0
  16. package/domains/policy/references/evidence-hierarchy.md +41 -0
  17. package/domains/policy/references/implementation-evidence.md +47 -0
  18. package/eduevidence_cli.py +26 -0
  19. package/engine/__init__.py +11 -0
  20. package/engine/__pycache__/__init__.cpython-312.pyc +0 -0
  21. package/engine/__pycache__/analysis.cpython-312.pyc +0 -0
  22. package/engine/__pycache__/bias.cpython-312.pyc +0 -0
  23. package/engine/__pycache__/briefs.cpython-312.pyc +0 -0
  24. package/engine/__pycache__/capabilities.cpython-312.pyc +0 -0
  25. package/engine/__pycache__/citation_check.cpython-312.pyc +0 -0
  26. package/engine/__pycache__/contracts.cpython-312.pyc +0 -0
  27. package/engine/__pycache__/datasets.cpython-312.pyc +0 -0
  28. package/engine/__pycache__/events.cpython-312.pyc +0 -0
  29. package/engine/__pycache__/evidence_graph.cpython-312.pyc +0 -0
  30. package/engine/__pycache__/evidence_review.cpython-312.pyc +0 -0
  31. package/engine/__pycache__/evidencecore.cpython-312.pyc +0 -0
  32. package/engine/__pycache__/gap_lens.cpython-312.pyc +0 -0
  33. package/engine/__pycache__/gaps.cpython-312.pyc +0 -0
  34. package/engine/__pycache__/graph_store.cpython-312.pyc +0 -0
  35. package/engine/__pycache__/graph_validate.cpython-312.pyc +0 -0
  36. package/engine/__pycache__/ids.cpython-312.pyc +0 -0
  37. package/engine/__pycache__/library.cpython-312.pyc +0 -0
  38. package/engine/__pycache__/library_builtin.cpython-312.pyc +0 -0
  39. package/engine/__pycache__/living.cpython-312.pyc +0 -0
  40. package/engine/__pycache__/log.cpython-312.pyc +0 -0
  41. package/engine/__pycache__/meta_analysis.cpython-312.pyc +0 -0
  42. package/engine/__pycache__/meta_synthesis.cpython-312.pyc +0 -0
  43. package/engine/__pycache__/migration.cpython-312.pyc +0 -0
  44. package/engine/__pycache__/mode_router.cpython-312.pyc +0 -0
  45. package/engine/__pycache__/paths.cpython-312.pyc +0 -0
  46. package/engine/__pycache__/pilot.cpython-312.pyc +0 -0
  47. package/engine/__pycache__/planner.cpython-312.pyc +0 -0
  48. package/engine/__pycache__/project.cpython-312.pyc +0 -0
  49. package/engine/__pycache__/projections.cpython-312.pyc +0 -0
  50. package/engine/__pycache__/robustness.cpython-312.pyc +0 -0
  51. package/engine/__pycache__/run.cpython-312.pyc +0 -0
  52. package/engine/__pycache__/semantics.cpython-312.pyc +0 -0
  53. package/engine/__pycache__/study_design.cpython-312.pyc +0 -0
  54. package/engine/__pycache__/synthesis.cpython-312.pyc +0 -0
  55. package/engine/__pycache__/tribunal.cpython-312.pyc +0 -0
  56. package/engine/__pycache__/update.cpython-312.pyc +0 -0
  57. package/engine/__pycache__/versions.cpython-312.pyc +0 -0
  58. package/engine/analysis.py +308 -0
  59. package/engine/bias.py +178 -0
  60. package/engine/briefs.py +106 -0
  61. package/engine/capabilities.py +99 -0
  62. package/engine/citation_check.py +192 -0
  63. package/engine/contracts.py +117 -0
  64. package/engine/datasets.py +165 -0
  65. package/engine/events.py +67 -0
  66. package/engine/evidence_graph.py +571 -0
  67. package/engine/evidence_review.py +88 -0
  68. package/engine/evidencecore.py +182 -0
  69. package/engine/gap_lens.py +132 -0
  70. package/engine/gaps.py +169 -0
  71. package/engine/graph_store.py +335 -0
  72. package/engine/graph_validate.py +87 -0
  73. package/engine/ids.py +77 -0
  74. package/engine/library.py +268 -0
  75. package/engine/library_builtin.py +301 -0
  76. package/engine/living.py +671 -0
  77. package/engine/log.py +39 -0
  78. package/engine/meta_analysis.py +333 -0
  79. package/engine/meta_synthesis.py +111 -0
  80. package/engine/migration.py +397 -0
  81. package/engine/mode_router.py +72 -0
  82. package/engine/paths.py +15 -0
  83. package/engine/pilot.py +368 -0
  84. package/engine/planner.py +126 -0
  85. package/engine/project.py +118 -0
  86. package/engine/projections.py +240 -0
  87. package/engine/robustness.py +109 -0
  88. package/engine/run.py +85 -0
  89. package/engine/semantics.py +135 -0
  90. package/engine/study_design.py +87 -0
  91. package/engine/synthesis.py +187 -0
  92. package/engine/tribunal.py +408 -0
  93. package/engine/update.py +113 -0
  94. package/engine/versions.py +12 -0
  95. package/install.sh +510 -0
  96. package/integrations/__init__.py +1 -0
  97. package/integrations/__pycache__/__init__.cpython-312.pyc +0 -0
  98. package/integrations/__pycache__/agent_mcp.cpython-312.pyc +0 -0
  99. package/integrations/__pycache__/smart_web_fetch.cpython-312.pyc +0 -0
  100. package/integrations/agent_mcp.py +856 -0
  101. package/integrations/smart_web_fetch.py +59 -0
  102. package/package.json +50 -0
  103. package/pyproject.toml +55 -0
  104. package/references/applicability-policy.md +88 -0
  105. package/references/education-framing.md +132 -0
  106. package/references/effect_size_formulas.md +35 -0
  107. package/references/evaluation-design.md +111 -0
  108. package/references/evidence-quality.md +79 -0
  109. package/references/grade_framework.md +29 -0
  110. package/references/intervention-design.md +98 -0
  111. package/references/methodology-audit.md +103 -0
  112. package/references/outcome-taxonomy.md +106 -0
  113. package/references/retrieval-protocol.md +142 -0
  114. package/references/skeptic-protocol.md +93 -0
  115. package/references/social_science_pitfalls.md +48 -0
  116. package/references/source-validity.md +140 -0
  117. package/references/tribunal-policy.md +112 -0
  118. package/references/wwc_standards.md +29 -0
  119. package/retrieval/__init__.py +1 -0
  120. package/retrieval/__pycache__/__init__.cpython-312.pyc +0 -0
  121. package/retrieval/__pycache__/corpus_store.cpython-312.pyc +0 -0
  122. package/retrieval/__pycache__/dedupe.cpython-312.pyc +0 -0
  123. package/retrieval/__pycache__/failures.cpython-312.pyc +0 -0
  124. package/retrieval/__pycache__/fetch.cpython-312.pyc +0 -0
  125. package/retrieval/__pycache__/search.cpython-312.pyc +0 -0
  126. package/retrieval/__pycache__/source.cpython-312.pyc +0 -0
  127. package/retrieval/__pycache__/validate.cpython-312.pyc +0 -0
  128. package/retrieval/corpus_store.py +181 -0
  129. package/retrieval/dedupe.py +127 -0
  130. package/retrieval/failures.py +90 -0
  131. package/retrieval/fetch.py +435 -0
  132. package/retrieval/search.py +493 -0
  133. package/retrieval/source.py +160 -0
  134. package/retrieval/validate.py +257 -0
  135. package/schemas/agent-mcp-approval.schema.json +57 -0
  136. package/schemas/chart-spec.schema.json +88 -0
  137. package/schemas/cross-model-review.schema.json +28 -0
  138. package/schemas/education-frame.schema.json +106 -0
  139. package/schemas/evaluation.schema.json +35 -0
  140. package/schemas/evidence.schema.json +81 -0
  141. package/schemas/fetch-result.schema.json +119 -0
  142. package/schemas/intervention.schema.json +46 -0
  143. package/schemas/methodology.schema.json +102 -0
  144. package/schemas/report-result.schema.json +381 -0
  145. package/schemas/report-spec.schema.json +130 -0
  146. package/schemas/source.schema.json +311 -0
  147. package/schemas/v2/analysis-plan.schema.json +28 -0
  148. package/schemas/v2/analysis-run.schema.json +33 -0
  149. package/schemas/v2/claim.schema.json +26 -0
  150. package/schemas/v2/dataset-asset.schema.json +40 -0
  151. package/schemas/v2/decision-snapshot.schema.json +53 -0
  152. package/schemas/v2/evidence-link.schema.json +38 -0
  153. package/schemas/v2/finding.schema.json +47 -0
  154. package/schemas/v2/graph-revision.schema.json +30 -0
  155. package/schemas/v2/knowledge-gap.schema.json +40 -0
  156. package/schemas/v2/methodology-audit.schema.json +30 -0
  157. package/schemas/v2/outcome.schema.json +18 -0
  158. package/schemas/v2/project.schema.json +31 -0
  159. package/schemas/v2/research-intent.schema.json +24 -0
  160. package/schemas/v2/run.schema.json +43 -0
  161. package/schemas/v2/source.schema.json +24 -0
  162. package/schemas/v2/study-design.schema.json +67 -0
  163. package/schemas/v2/study.schema.json +37 -0
  164. package/schemas/v3/pilot-outcome.schema.json +132 -0
  165. package/schemas/v3/run-manifest.schema.json +193 -0
  166. package/schemas/v3/synthesis.schema.json +86 -0
  167. package/schemas/v4/drift-report.schema.json +66 -0
  168. package/schemas/v4/evidence-library.schema.json +42 -0
  169. package/schemas/v4/living-subscription.schema.json +55 -0
  170. package/schemas/v4/meta-analysis.schema.json +152 -0
  171. package/schemas/verdict.schema.json +56 -0
  172. package/scripts/__init__.py +0 -0
  173. package/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
  174. package/scripts/__pycache__/benchmark.cpython-312.pyc +0 -0
  175. package/scripts/__pycache__/benchmark_evaluator.cpython-312.pyc +0 -0
  176. package/scripts/__pycache__/benchmark_judge.cpython-312.pyc +0 -0
  177. package/scripts/__pycache__/benchmark_routing.cpython-312.pyc +0 -0
  178. package/scripts/__pycache__/benchmark_v2.cpython-312.pyc +0 -0
  179. package/scripts/__pycache__/benchmark_v3.cpython-312.pyc +0 -0
  180. package/scripts/__pycache__/build_result.cpython-312.pyc +0 -0
  181. package/scripts/__pycache__/claim_audit.cpython-312.pyc +0 -0
  182. package/scripts/__pycache__/complexity_gate.cpython-312.pyc +0 -0
  183. package/scripts/__pycache__/compute_confidence.cpython-312.pyc +0 -0
  184. package/scripts/__pycache__/dashboard_server.cpython-312.pyc +0 -0
  185. package/scripts/__pycache__/did_regression.cpython-312.pyc +0 -0
  186. package/scripts/__pycache__/effect_calculator.cpython-312.pyc +0 -0
  187. package/scripts/__pycache__/evidence_matrix.cpython-312.pyc +0 -0
  188. package/scripts/__pycache__/evidence_score.cpython-312.pyc +0 -0
  189. package/scripts/__pycache__/evidence_semantics.cpython-312.pyc +0 -0
  190. package/scripts/__pycache__/fetch_benchmark.cpython-312.pyc +0 -0
  191. package/scripts/__pycache__/lint_report_layout.cpython-312.pyc +0 -0
  192. package/scripts/__pycache__/orchestrator.cpython-312.pyc +0 -0
  193. package/scripts/__pycache__/pre_verdict_gate.cpython-312.pyc +0 -0
  194. package/scripts/__pycache__/recompute_demo_quality.cpython-312.pyc +0 -0
  195. package/scripts/__pycache__/render_report.cpython-312.pyc +0 -0
  196. package/scripts/__pycache__/render_report_html.cpython-312.pyc +0 -0
  197. package/scripts/__pycache__/run_workspace.cpython-312.pyc +0 -0
  198. package/scripts/__pycache__/skill_lint.cpython-312.pyc +0 -0
  199. package/scripts/__pycache__/startup_probe.cpython-312.pyc +0 -0
  200. package/scripts/__pycache__/sync_killer_demo_report.cpython-312.pyc +0 -0
  201. package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.0.2.pyc +0 -0
  202. package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.1.1.pyc +0 -0
  203. package/scripts/__pycache__/validate_schema.cpython-312.pyc +0 -0
  204. package/scripts/audit_dois.py +292 -0
  205. package/scripts/bake_pack.sh +37 -0
  206. package/scripts/benchmark.py +183 -0
  207. package/scripts/benchmark_evaluator.py +371 -0
  208. package/scripts/benchmark_judge.py +535 -0
  209. package/scripts/benchmark_routing.py +120 -0
  210. package/scripts/benchmark_v2.py +304 -0
  211. package/scripts/benchmark_v3.py +552 -0
  212. package/scripts/build_esl_artifacts.py +1921 -0
  213. package/scripts/build_evidence_library.py +307 -0
  214. package/scripts/build_killer_demo.py +295 -0
  215. package/scripts/build_result.py +311 -0
  216. package/scripts/check_version_consistency.py +96 -0
  217. package/scripts/citation_check.py +123 -0
  218. package/scripts/claim_audit.py +157 -0
  219. package/scripts/complexity_gate.py +180 -0
  220. package/scripts/compute_confidence.py +176 -0
  221. package/scripts/dashboard_server.py +536 -0
  222. package/scripts/did_regression.py +315 -0
  223. package/scripts/effect_calculator.py +99 -0
  224. package/scripts/enrich_projects_human_and_lieflat.py +315 -0
  225. package/scripts/evidence_matrix.py +129 -0
  226. package/scripts/evidence_score.py +234 -0
  227. package/scripts/evidence_semantics.py +87 -0
  228. package/scripts/fetch_benchmark.py +177 -0
  229. package/scripts/generate_metrics.py +99 -0
  230. package/scripts/generate_new_projects.py +686 -0
  231. package/scripts/generate_promo.py +192 -0
  232. package/scripts/lint_report_layout.py +182 -0
  233. package/scripts/orchestrator.py +1456 -0
  234. package/scripts/pre_verdict_gate.py +513 -0
  235. package/scripts/quickstart.py +121 -0
  236. package/scripts/rebake_all_5themes.py +88 -0
  237. package/scripts/recompute_demo_quality.py +205 -0
  238. package/scripts/render_report.py +270 -0
  239. package/scripts/render_report_html.py +356 -0
  240. package/scripts/retraction_watch.py +110 -0
  241. package/scripts/run_workspace.py +337 -0
  242. package/scripts/serve_web.py +54 -0
  243. package/scripts/skill_lint.py +150 -0
  244. package/scripts/startup_probe.py +265 -0
  245. package/scripts/sync_killer_demo_report.py +270 -0
  246. package/scripts/test_adversarial_empirical.py +541 -0
  247. package/scripts/validate_schema.py +256 -0
  248. package/skill/agents/education-planner.md +80 -0
  249. package/skill/agents/evaluation-designer.md +74 -0
  250. package/skill/agents/evidence-analyst.md +106 -0
  251. package/skill/agents/evidence-judge.md +111 -0
  252. package/skill/agents/evidence-retriever.md +80 -0
  253. package/skill/agents/intervention-designer.md +82 -0
  254. package/skill/agents/method-reviewer.md +104 -0
  255. package/skill/agents/skeptic.md +89 -0
  256. package/skill/sub-skills/aihot-trend-analysis/SKILL.md +31 -0
  257. package/skill/sub-skills/contradiction-analysis/SKILL.md +17 -0
  258. package/skill/sub-skills/data-analysis/SKILL.md +23 -0
  259. package/skill/sub-skills/ethics-review/SKILL.md +25 -0
  260. package/skill/sub-skills/evidence-extraction/SKILL.md +19 -0
  261. package/skill/sub-skills/evidence-review/SKILL.md +18 -0
  262. package/skill/sub-skills/gap-analysis/SKILL.md +25 -0
  263. package/skill/sub-skills/literature-review/SKILL.md +21 -0
  264. package/skill/sub-skills/methodology-audit/SKILL.md +20 -0
  265. package/skill/sub-skills/report-generation/SKILL.md +51 -0
  266. package/skill/sub-skills/research-planning/SKILL.md +21 -0
  267. package/skill/sub-skills/study-design/SKILL.md +16 -0
  268. package/skill/task-briefs/adjudicate.md +17 -0
  269. package/skill/task-briefs/audit.md +15 -0
  270. package/skill/task-briefs/challenge.md +15 -0
  271. package/skill/task-briefs/evaluate.md +13 -0
  272. package/skill/task-briefs/extract.md +16 -0
  273. package/skill/task-briefs/frame.md +17 -0
  274. package/skill/task-briefs/intervene.md +14 -0
  275. package/skill/task-briefs/present.md +16 -0
  276. package/skill/task-briefs/retrieve.md +15 -0
  277. package/visualization/eduevidence-report/assets/base.css +337 -0
  278. package/visualization/eduevidence-report/motion/motion.css +157 -0
  279. package/visualization/eduevidence-report/motion/motion.js +107 -0
  280. package/visualization/eduevidence-report/references/bilingual-style.md +7 -0
  281. package/visualization/eduevidence-report/references/component-catalog.md +145 -0
  282. package/visualization/eduevidence-report/references/evidence-expansion.md +65 -0
  283. package/visualization/eduevidence-report/references/full-report-outline.md +86 -0
  284. package/visualization/eduevidence-report/references/layout-constraints.md +63 -0
  285. package/visualization/eduevidence-report/references/lieflat-composition.md +79 -0
  286. package/visualization/eduevidence-report/references/motion-system.md +31 -0
  287. package/visualization/eduevidence-report/schemas/adapter-envelope.schema.json +22 -0
  288. package/visualization/eduevidence-report/schemas/visual-layout.schema.json +87 -0
  289. package/visualization/eduevidence-report/scripts/__pycache__/adapter_contract.cpython-312.pyc +0 -0
  290. package/visualization/eduevidence-report/scripts/__pycache__/build_artifact_manifest.cpython-312.pyc +0 -0
  291. package/visualization/eduevidence-report/scripts/__pycache__/build_charts.cpython-312.pyc +0 -0
  292. package/visualization/eduevidence-report/scripts/__pycache__/build_figures.cpython-312.pyc +0 -0
  293. package/visualization/eduevidence-report/scripts/__pycache__/build_infographics.cpython-312.pyc +0 -0
  294. package/visualization/eduevidence-report/scripts/__pycache__/build_report.cpython-312.pyc +0 -0
  295. package/visualization/eduevidence-report/scripts/__pycache__/charts_data.cpython-312.pyc +0 -0
  296. package/visualization/eduevidence-report/scripts/__pycache__/lieflat_engine.cpython-312.pyc +0 -0
  297. package/visualization/eduevidence-report/scripts/__pycache__/zh_labels.cpython-312.pyc +0 -0
  298. package/visualization/eduevidence-report/scripts/adapter_contract.py +72 -0
  299. package/visualization/eduevidence-report/scripts/build_artifact_manifest.py +70 -0
  300. package/visualization/eduevidence-report/scripts/build_charts.py +283 -0
  301. package/visualization/eduevidence-report/scripts/build_figures.py +515 -0
  302. package/visualization/eduevidence-report/scripts/build_infographics.py +268 -0
  303. package/visualization/eduevidence-report/scripts/build_report.py +3211 -0
  304. package/visualization/eduevidence-report/scripts/charts_data.py +617 -0
  305. package/visualization/eduevidence-report/scripts/check_mobile_layout.js +220 -0
  306. package/visualization/eduevidence-report/scripts/lieflat_engine.py +852 -0
  307. package/visualization/eduevidence-report/scripts/zh_labels.py +245 -0
  308. package/visualization/eduevidence-report/themes/academic.css +94 -0
  309. package/visualization/eduevidence-report/themes/claude.css +96 -0
  310. package/visualization/eduevidence-report/themes/datalab-dark.css +147 -0
  311. package/visualization/eduevidence-report/themes/datalab.css +151 -0
  312. package/visualization/eduevidence-report/themes/presentation.css +140 -0
@@ -0,0 +1,536 @@
1
+ """scripts/dashboard_server.py — EduEvidence 5.0 Local Web Studio.
2
+
3
+ Lightweight static + API server for the 3-page Web Studio (web/):
4
+
5
+ 1. 仪表盘 (Dashboard) — cross-project KPIs, effect-size comparison, asset matrix
6
+ 2. 报告浏览 (Report Browser) — browse baked report files (default + reports-5themes/* variants)
7
+ in an iframe; themes are fixed at generation time
8
+ 3. 数据可视化 (Data Visualization) — per-project forest plot / effect-size distribution /
9
+ outcome-dimension summary / SSOT evidence graph
10
+
11
+ No agent dispatch, no subprocess streaming. Every value is read directly from the
12
+ project artifacts the skill pipeline emits (examples/<id>/result.json +
13
+ evidence_graph.json), so a newly completed project appears automatically.
14
+
15
+ Usage:
16
+ python3 scripts/dashboard_server.py --host 127.0.0.1 --port 8765
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import http.server
22
+ import json
23
+ import sys
24
+ import urllib.parse
25
+ from pathlib import Path
26
+
27
+ ROOT = Path(__file__).resolve().parent.parent
28
+ WEB_DIR = ROOT / "web"
29
+ EXAMPLES_DIR = ROOT / "examples"
30
+
31
+ if str(ROOT) not in sys.path:
32
+ sys.path.insert(0, str(ROOT))
33
+
34
+ from engine.evidence_graph import EvidenceGraph # noqa: E402
35
+
36
+ import sys as _sys
37
+ _VIZ_SCRIPTS = ROOT / "visualization" / "eduevidence-report" / "scripts"
38
+ if str(_VIZ_SCRIPTS) not in _sys.path:
39
+ _sys.path.insert(0, str(_VIZ_SCRIPTS))
40
+ from zh_labels import OUTCOME_ZH, ACTION_ZH, STUDY_ZH, AUTHORITY_ZH, CONFIDENCE_ZH # noqa: E402
41
+
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
+ PROJECT_TITLES: Dict[str, str] = {
45
+ "ai-coding-assistant-evidence": "高校大一引入 AI 编程助手(真实文献旗舰示例)",
46
+ "highschool-math-ai-tutor": "高中数学引入大模型自适应 AI Tutor 评估",
47
+ "esl-academic-writing-ai": "大学 ESL 学术英语写作与同行评审 AI 评估",
48
+ "ai-tutor": "大学高数课程 AI Tutor 评估",
49
+ "ai-writing-assistant": "AI 写作助手评估",
50
+ }
51
+
52
+
53
+ def unquote_path(path: str) -> str:
54
+ """Decode a URL path once and reject encoded separators / NUL bytes."""
55
+ decoded = urllib.parse.unquote(path)
56
+ if "\x00" in decoded or "\\" in decoded:
57
+ raise ValueError("unsafe path characters")
58
+ return decoded
59
+
60
+
61
+ def _read_json(path: Path) -> Optional[Any]:
62
+ try:
63
+ return json.loads(path.read_text(encoding="utf-8"))
64
+ except Exception:
65
+ return None
66
+
67
+
68
+ def _decision_fields(decision: Any) -> tuple[Any, Any]:
69
+ """Return (verdict, confidence) tolerating the three decision shapes in the wild."""
70
+ if not isinstance(decision, dict):
71
+ return None, None
72
+ verdict = decision.get("verdict") or decision.get("recommended_action") or decision.get("decision")
73
+ confidence = decision.get("confidence_score")
74
+ if not isinstance(confidence, (int, float)):
75
+ confidence = decision.get("confidence")
76
+ return verdict, confidence
77
+
78
+
79
+ 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")
91
+
92
+
93
+ def _graph_node_count(path: Path) -> int:
94
+ """与 evidence_graph.export_echarts_graph() 同口径的节点数。"""
95
+ try:
96
+ graph = EvidenceGraph.from_json(path.read_text(encoding="utf-8"))
97
+ export = graph.export_echarts_graph()
98
+ nodes = export.get("nodes") or []
99
+ return len(nodes)
100
+ except Exception:
101
+ data = _read_json(path) or {}
102
+
103
+ def _size(key: str) -> int:
104
+ value = data.get(key) or {}
105
+ return len(value) if isinstance(value, dict) else len(value)
106
+
107
+ return _size("papers") + _size("evidence") + _size("claims")
108
+
109
+
110
+ def _direction_counts(evidence: list) -> Dict[str, int]:
111
+ """按 evidence 的 relation_to_claim / direction / effect_direction 聚合。"""
112
+ counts = {"support": 0, "contradict": 0, "neutral": 0}
113
+ for ev in evidence:
114
+ if not isinstance(ev, dict):
115
+ continue
116
+ d = (ev.get("relation_to_claim") or ev.get("direction")
117
+ or ev.get("effect_direction") or "").lower()
118
+ if d in ("support", "supports", "positive", "pos"):
119
+ counts["support"] += 1
120
+ elif d in ("contradict", "contradicts", "negative", "neg"):
121
+ counts["contradict"] += 1
122
+ else:
123
+ counts["neutral"] += 1
124
+ return counts
125
+
126
+
127
+ def _outcome_rollup(evidence: list) -> List[Dict[str, Any]]:
128
+ """无 outcome_mapping 时按 outcome_type 聚合方向计数(回退链)。"""
129
+ roll: Dict[str, Dict[str, int]] = {}
130
+ for ev in evidence:
131
+ if not isinstance(ev, dict):
132
+ continue
133
+ ot = ev.get("outcome_type") or ev.get("outcome") or "other"
134
+ d = (ev.get("relation_to_claim") or ev.get("direction")
135
+ or ev.get("effect_direction") or "").lower()
136
+ bucket = roll.setdefault(ot, {"support": 0, "contradict": 0, "neutral": 0})
137
+ if d in ("support", "supports", "positive", "pos"):
138
+ bucket["support"] += 1
139
+ elif d in ("contradict", "contradicts", "negative", "neg"):
140
+ bucket["contradict"] += 1
141
+ else:
142
+ bucket["neutral"] += 1
143
+ return [{"outcome_type": k, **v} for k, v in roll.items()]
144
+
145
+
146
+ def _known_project_ids() -> set:
147
+ """Project ids = directories with a real result.json (not the deduped display list)."""
148
+ if not EXAMPLES_DIR.exists():
149
+ return set()
150
+ return {d.name for d in EXAMPLES_DIR.iterdir()
151
+ if d.is_dir() and (d / "result.json").exists()}
152
+
153
+
154
+ def scan_local_projects() -> List[Dict[str, Any]]:
155
+ projects: List[Dict[str, Any]] = []
156
+ if not EXAMPLES_DIR.exists():
157
+ return projects
158
+
159
+ for proj_dir in sorted(EXAMPLES_DIR.iterdir()):
160
+ if not proj_dir.is_dir():
161
+ continue
162
+ result_path = proj_dir / "result.json"
163
+ if not result_path.exists():
164
+ continue
165
+
166
+ result = _read_json(result_path) or {}
167
+ meta = result.get("meta") or {}
168
+ frame = result.get("research_frame") or {}
169
+ question = meta.get("question") or frame.get("question") or ""
170
+ verdict, confidence = _decision_fields(result.get("decision"))
171
+
172
+ evidence = result.get("evidence") or []
173
+ forest = result.get("forest_plot_data") or []
174
+ effect_values: List[float] = []
175
+ for ev in evidence:
176
+ value, _, _ = _extract_effect(ev)
177
+ if isinstance(value, (int, float)):
178
+ effect_values.append(float(value))
179
+ for f in forest:
180
+ if isinstance(f.get("effect_size"), (int, float)):
181
+ effect_values.append(float(f["effect_size"]))
182
+
183
+ graph_path = proj_dir / "evidence_graph.json"
184
+ html_path = proj_dir / "EduEvidence_Report.html"
185
+ report_variants: List[Dict[str, str]] = []
186
+ themes_dir = proj_dir / "reports-5themes"
187
+ if themes_dir.is_dir():
188
+ for variant_file in sorted(list(themes_dir.glob("EduEvidence_Report_*.html")) + list(themes_dir.glob("report_*.html"))):
189
+ theme_name = variant_file.stem.replace("EduEvidence_Report_", "").replace("report_", "")
190
+ 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
+
196
+ zh_result = _read_json(proj_dir / "result.zh.json") or {}
197
+ zh_question = ((zh_result.get("meta") or {}).get("question")
198
+ or (zh_result.get("research_frame") or {}).get("question") or "")
199
+
200
+ projects.append({
201
+ "id": proj_dir.name,
202
+ "title": PROJECT_TITLES.get(proj_dir.name) or (zh_question[:72] if zh_question else (question[:72] or proj_dir.name)),
203
+ "title_zh": zh_question[:72] if zh_question else None,
204
+ "domain": meta.get("domain") or "education",
205
+ "question": question,
206
+ "verdict": verdict,
207
+ "confidence": confidence,
208
+ "evidence_count": len(evidence) if isinstance(evidence, list) else 0,
209
+ "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},
214
+ "has_graph": graph_path.exists(),
215
+ "node_count": _graph_node_count(graph_path) if graph_path.exists() else 0,
216
+ "html_report_path": str(html_path) if html_path.exists() else None,
217
+ "report_variants": report_variants,
218
+ })
219
+ # Canonical question deduplication: keep the project with the highest evidence count
220
+ deduped: Dict[str, Dict[str, Any]] = {}
221
+ for p in projects:
222
+ 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"]):
224
+ deduped[norm_q] = p
225
+ return list(deduped.values())
226
+
227
+
228
+ def build_stats(projects: List[Dict[str, Any]]) -> Dict[str, Any]:
229
+ return {
230
+ "total_projects": len(projects),
231
+ "total_evidence": sum(p["evidence_count"] for p in projects),
232
+ "total_effect_sizes": sum(p["effect_count"] for p in projects),
233
+ "total_nodes": sum(p["node_count"] for p in projects),
234
+ }
235
+
236
+
237
+ 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
+ stats = build_stats(projects)
245
+ stats["total_tokens"] = None
246
+ stats["usage_measurement_status"] = "NOT_CAPTURED"
247
+ return stats
248
+
249
+
250
+ def build_viz_payload(proj_id: str) -> Dict[str, Any]:
251
+ proj_dir = EXAMPLES_DIR / proj_id
252
+ result = _read_json(proj_dir / "result.json") or {}
253
+ meta = result.get("meta") or {}
254
+ frame = result.get("research_frame") or {}
255
+ question = meta.get("question") or frame.get("question") or ""
256
+ verdict, confidence = _decision_fields(result.get("decision"))
257
+ title = PROJECT_TITLES.get(proj_id) or question[:72] or proj_id
258
+
259
+ forest_items: List[Dict[str, Any]] = []
260
+ for f in (result.get("forest_plot_data") or []):
261
+ forest_items.append({
262
+ "study_label": f.get("study_label") or f.get("evidence_id") or "",
263
+ "venue": f.get("venue") or "",
264
+ "outcome_dimension": f.get("outcome_dimension") or f.get("outcome_metric") or "",
265
+ "effect_size": f.get("effect_size"),
266
+ "ci_lower": f.get("ci_lower"),
267
+ "ci_upper": f.get("ci_upper"),
268
+ "sample_size": f.get("sample_size"),
269
+ "direction": f.get("direction"),
270
+ "wwc_rating": f.get("wwc_rating") or "",
271
+ })
272
+
273
+ effect_items: List[Dict[str, Any]] = []
274
+ for ev in (result.get("evidence") or []):
275
+ value, lo, hi = _extract_effect(ev)
276
+ if isinstance(value, (int, float)):
277
+ effect_items.append({
278
+ "study_label": ev.get("study_label") or ev.get("title") or ev.get("evidence_id") or "",
279
+ "value": value,
280
+ "ci_lower": lo,
281
+ "ci_upper": hi,
282
+ "outcome_dimension": ev.get("outcome_dimension") or ev.get("outcome_type") or "",
283
+ "direction": ev.get("relation_to_claim") or ev.get("direction") or ev.get("effect_direction") or "",
284
+ })
285
+ if not effect_items:
286
+ for f in (result.get("forest_plot_data") or []):
287
+ if isinstance(f.get("effect_size"), (int, float)):
288
+ effect_items.append({
289
+ "study_label": f.get("study_label") or f.get("evidence_id") or "",
290
+ "value": f["effect_size"],
291
+ "ci_lower": f.get("ci_lower"),
292
+ "ci_upper": f.get("ci_upper"),
293
+ "outcome_dimension": f.get("outcome_dimension") or f.get("outcome_metric") or "",
294
+ "direction": f.get("direction"),
295
+ })
296
+
297
+ evidence_list = result.get("evidence") or []
298
+ outcome_items: List[Dict[str, Any]] = []
299
+ om = result.get("outcome_mapping") or {}
300
+ entries = (om.get("entries") or []) if isinstance(om, dict) else (om if isinstance(om, list) else [])
301
+ if entries:
302
+ for e in entries:
303
+ outcome_items.append({
304
+ "outcome_type": e.get("outcome_type") or e.get("outcome") or "",
305
+ "status": e.get("status") or "",
306
+ "support_count": e.get("support_count") or 0,
307
+ "contradict_count": e.get("contradict_count") or 0,
308
+ "neutral_count": e.get("neutral_count") or 0,
309
+ })
310
+ else:
311
+ # 回退链:无 outcome_mapping 时按 evidence 方向聚合
312
+ 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
+
318
+ graph = None
319
+ graph_path = proj_dir / "evidence_graph.json"
320
+ if graph_path.exists():
321
+ try:
322
+ graph = EvidenceGraph.from_json(graph_path.read_text(encoding="utf-8")).export_echarts_graph()
323
+ except Exception:
324
+ graph = None
325
+
326
+ return {
327
+ "id": proj_id,
328
+ "title": title,
329
+ "question": question,
330
+ "verdict": verdict,
331
+ "confidence": confidence,
332
+ "forest": forest_items,
333
+ "effect_sizes": effect_items,
334
+ "outcome_mapping": outcome_items,
335
+ "direction_counts": direction_counts,
336
+ "labels": {
337
+ "outcomes": OUTCOME_ZH,
338
+ "actions": ACTION_ZH,
339
+ "studies": STUDY_ZH,
340
+ "authority": AUTHORITY_ZH,
341
+ "confidence": CONFIDENCE_ZH,
342
+ },
343
+ "graph": graph,
344
+ }
345
+
346
+
347
+
348
+
349
+
350
+
351
+
352
+
353
+ class StudioHandler(http.server.SimpleHTTPRequestHandler):
354
+ """Serves web/ statically and exposes the artifact JSON API."""
355
+
356
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
357
+ super().__init__(*args, directory=str(WEB_DIR), **kwargs)
358
+
359
+ # -- response helpers -------------------------------------------------
360
+ def _send_bytes(self, data: bytes, content_type: str, status: int = 200,
361
+ extra_headers: Optional[Dict[str, str]] = None) -> None:
362
+ self.send_response(status)
363
+ self.send_header("Content-Type", content_type)
364
+ self.send_header("Content-Length", str(len(data)))
365
+ # 允许落地页(独立静态源)跨源探测/拉取只读 API 与控制台资源
366
+ self.send_header("Access-Control-Allow-Origin", "*")
367
+ if extra_headers:
368
+ for name, value in extra_headers.items():
369
+ self.send_header(name, value)
370
+ self.end_headers()
371
+ self.wfile.write(data)
372
+
373
+ 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
+ self._send_bytes(data, "text/html; charset=utf-8", extra_headers={
377
+ "Content-Security-Policy":
378
+ "default-src 'none'; script-src 'unsafe-inline'; "
379
+ "style-src 'unsafe-inline'; img-src data:; "
380
+ "font-src data:; connect-src 'none'; frame-ancestors 'self'",
381
+ "X-Content-Type-Options": "nosniff",
382
+ })
383
+
384
+ def _send_json(self, payload: Any, status: int = 200) -> None:
385
+ self._send_bytes(json.dumps(payload, ensure_ascii=False).encode("utf-8"),
386
+ "application/json; charset=utf-8", status)
387
+
388
+ def _serve_file(self, path: Path, content_type: str) -> None:
389
+ if path.exists():
390
+ self._send_bytes(path.read_bytes(), content_type)
391
+ else:
392
+ self._send_json({"error": "not found"}, 404)
393
+
394
+
395
+ # -- POST: disabled (read-only Studio) --------------------------------
396
+ def do_POST(self) -> None:
397
+ self._send_json({"error": "method not allowed"}, 405)
398
+
399
+ # -- GET --------------------------------------------------------------
400
+ def do_GET(self) -> None:
401
+ parsed = urllib.parse.urlparse(self.path)
402
+ path = parsed.path
403
+ query = urllib.parse.parse_qs(parsed.query)
404
+
405
+ # Only the three-page Studio entry + the read-only artifact API.
406
+ if path in ("/", "/index.html", "/dashboard", "/studio", "/console"):
407
+ self._serve_file(WEB_DIR / "index.html", "text/html; charset=utf-8")
408
+ return
409
+
410
+ if path == "/api/projects":
411
+ projects = scan_local_projects()
412
+ self._send_json({"projects": projects, "stats": build_stats(projects)})
413
+ return
414
+
415
+ if path == "/api/labels":
416
+ self._send_json({
417
+ "outcomes": OUTCOME_ZH,
418
+ "actions": ACTION_ZH,
419
+ "studies": STUDY_ZH,
420
+ "authority": AUTHORITY_ZH,
421
+ "confidence": CONFIDENCE_ZH,
422
+ })
423
+ return
424
+
425
+ if path.startswith("/api/projects/"):
426
+ suffix = urllib.parse.unquote(path[len("/api/projects/"):]).rstrip("/")
427
+ if suffix.endswith("/viz"):
428
+ suffix = suffix[:-len("/viz")].rstrip("/")
429
+ if suffix not in _known_project_ids():
430
+ self._send_json({"error": "unknown project"}, 404)
431
+ return
432
+ self._send_json(build_viz_payload(suffix))
433
+ return
434
+ self._send_json({"error": "not found"}, 404)
435
+ return
436
+
437
+ if path == "/report":
438
+ self._serve_report(query)
439
+ 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":
444
+ self._serve_web_asset(path)
445
+ return
446
+
447
+ self._send_json({"error": "not found"}, 404)
448
+
449
+ def _serve_web_asset(self, path: str) -> None:
450
+ """Serve a static asset strictly inside WEB_DIR (decode once, no escapes)."""
451
+ try:
452
+ candidate = (WEB_DIR / unquote_path(path).lstrip("/")).resolve()
453
+ except (ValueError, OSError):
454
+ self._send_json({"error": "not found"}, 404)
455
+ return
456
+ base = WEB_DIR.resolve()
457
+ if base != candidate and base not in candidate.parents:
458
+ self._send_json({"error": "not found"}, 404)
459
+ return
460
+ if not candidate.is_file() or candidate.suffix not in (".js", ".css"):
461
+ self._send_json({"error": "not found"}, 404)
462
+ return
463
+ ctype = "application/javascript; charset=utf-8" if candidate.suffix == ".js" \
464
+ else "text/css; charset=utf-8"
465
+ self._send_bytes(candidate.read_bytes(), ctype)
466
+
467
+ def _serve_report(self, query: Dict[str, List[str]]) -> None:
468
+ """Serve a baked report for a known project and explicit theme."""
469
+ proj_id = query.get("id", [""])[0]
470
+ theme = query.get("theme", ["default"])[0]
471
+
472
+ if proj_id not in _known_project_ids():
473
+ self._send_json({"error": "unknown project"}, 404)
474
+ return
475
+
476
+ proj_dir = EXAMPLES_DIR / proj_id
477
+ if theme == "default":
478
+ html_path = proj_dir / "EduEvidence_Report.html"
479
+ if not html_path.is_file():
480
+ self._send_json({"error": "report not found"}, 404)
481
+ return
482
+ else:
483
+ if not theme or not all(c.isalnum() or c in "-_" for c in theme):
484
+ self._send_json({"error": "unknown report theme"}, 404)
485
+ return
486
+ variants_dir = proj_dir / "reports-5themes"
487
+ 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()):
490
+ self._send_json({"error": "unknown report theme"}, 404)
491
+ return
492
+
493
+ self._send_report_bytes(html_path.read_bytes())
494
+
495
+ def run_dashboard_server(host: str = "127.0.0.1", port: int = 8765) -> None:
496
+ server = None
497
+ actual_port = port
498
+ for offset in range(10):
499
+ try:
500
+ candidate = port + offset
501
+ server = http.server.ThreadingHTTPServer((host, candidate), StudioHandler)
502
+ server.allow_reuse_address = True
503
+ actual_port = candidate
504
+ break
505
+ except OSError as e:
506
+ if e.errno == 48:
507
+ continue
508
+ raise
509
+
510
+ if server is None:
511
+ print(f"❌ 端口 {port}-{port + 9} 均被占用。")
512
+ return
513
+
514
+ print("============================================================")
515
+ 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")
520
+ print("============================================================")
521
+ try:
522
+ server.serve_forever()
523
+ except KeyboardInterrupt:
524
+ server.server_close()
525
+
526
+
527
+ def main() -> None:
528
+ parser = argparse.ArgumentParser()
529
+ parser.add_argument("--host", default="127.0.0.1")
530
+ parser.add_argument("--port", type=int, default=8765)
531
+ args = parser.parse_args()
532
+ run_dashboard_server(args.host, args.port)
533
+
534
+
535
+ if __name__ == "__main__":
536
+ main()