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,371 @@
1
+ #!/usr/bin/env python3
2
+ """benchmark_evaluator.py - gold-based evaluator for Layer B empirical runs (v3).
3
+
4
+ Metrics are computed against gold annotations (benchmarks/annotations/gold-<id>.json),
5
+ never against the model's own claims:
6
+
7
+ outcome_separation_accuracy Jaccard(response outcomes, gold correct_outcome_types)
8
+ decision_calibration 1 if response action in gold expected_decision_range else 0
9
+ contradiction_recall fraction of gold known_contradictions detected in response
10
+ contradiction_precision fraction of response contradiction units that match a gold item
11
+ citation_support_recall fraction of gold key_supporting_sources mentioned in response
12
+ scope_calibration 1 if response scope section bounds claims (can/cannot/boundary)
13
+
14
+ Matching is deterministic token-overlap (CJK bigram + word) - no LLM judge is
15
+ required; method:heuristic is recorded on every metric so the report never
16
+ overstates precision. Means are reported with a normal-approximation 95% CI.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import math
22
+ import re
23
+ import sys
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
28
+
29
+ from benchmark import OUTCOME_SET # noqa: E402
30
+
31
+ ACTION_TOKENS = ("adopt", "pilot", "reject", "insufficient_evidence")
32
+ ACTION_ZH = {"adopt": "采用", "pilot": "试点", "reject": "拒绝",
33
+ "insufficient_evidence": "证据不足"}
34
+
35
+ # Chinese result descriptors -> Outcome Taxonomy token (evaluator language
36
+ # fairness: responses are mostly Chinese; matching English tokens only would
37
+ # systematically under-score both baselines).
38
+ ZH_OUTCOME_MAP = {
39
+ "正确率": "accuracy", "答题准确率": "accuracy", "准确性": "accuracy",
40
+ "完成时间": "completion_time", "速度": "completion_time",
41
+ "保持率": "retention", "记忆保持": "retention", "保持": "retention",
42
+ "长期保持": "retention", "保留": "retention",
43
+ "迁移": "transfer", "远迁移": "transfer", "近迁移": "transfer",
44
+ "独立解题": "independent_problem_solving", "独立问题解决": "independent_problem_solving",
45
+ "独立调试": "independent_problem_solving", "独立能力": "independent_problem_solving",
46
+ "概念理解": "concept_understanding",
47
+ "知识获得": "knowledge_gain", "知识获取": "knowledge_gain",
48
+ "期末成绩": "assignment_score", "考试成绩": "assignment_score",
49
+ "作业成绩": "assignment_score","学业成绩": "assignment_score",
50
+ "动机": "motivation", "学习动机": "motivation",
51
+ "参与度": "engagement","课堂参与": "engagement",
52
+ "认知负荷": "cognitive_load", "负荷": "cognitive_load",
53
+ "元认知": "metacognition",
54
+ "AI依赖": "ai_dependency","工具依赖": "ai_dependency",
55
+ "过度依赖": "over_reliance", "过度信任": "over_reliance",
56
+ "努力投入": "reduced_effort", "减少努力": "reduced_effort", "努力": "reduced_effort",
57
+ "迁移受损": "reduced_transfer",
58
+ "学术诚信": "academic_integrity_risk", "诚信": "academic_integrity_risk",
59
+ "虚假自信": "false_confidence", "过度自信": "false_confidence", "虚假信心": "false_confidence",
60
+ "代码质量": "code_quality",
61
+ "求助行为": "help_seeking", "求助": "help_seeking",
62
+ }
63
+ SCOPE_BOUNDARY_MARKERS = ("不能主张", "不能", "不适用", "范围", "超出", "仅限",
64
+ "cannot", "not extend", "beyond", "boundary", "only",
65
+ "不扩展到", "不推断")
66
+ CONTRADICTION_HINT = (
67
+ "contradict", "negative", "null", "但", "然而", "未发现", "没有显著", "下降",
68
+ "负向", "不一致", "反方", "however", "no significant", "did not",
69
+ )
70
+
71
+ _ID_RE = re.compile(r"\b([A-Za-z][A-Za-z-]{1,40})\b")
72
+ _YEAR_RE = re.compile(r"\b(19|20)\d{2}\b")
73
+
74
+
75
+ def _cjk_bigrams(text: str) -> set[str]:
76
+ chars = [c for c in text if "\u4e00" <= c <= "\u9fff"]
77
+ return {chars[i] + chars[i + 1] for i in range(len(chars) - 1)}
78
+
79
+
80
+ def _words(text: str) -> set[str]:
81
+ return {w.lower() for w in _ID_RE.findall(text) if len(w) > 2}
82
+
83
+
84
+ def _tokenize(text: str) -> set[str]:
85
+ return _cjk_bigrams(text) | _words(text)
86
+
87
+
88
+ def _overlap(a: str, b: str) -> float:
89
+ ta, tb = _tokenize(a), _tokenize(b)
90
+ if not ta or not tb:
91
+ return 0.0
92
+ return len(ta & tb) / max(1, min(len(ta), len(tb)))
93
+
94
+
95
+ def extract_json_block(text: str) -> dict[str, Any] | None:
96
+ """Best-effort JSON extraction: prefers a fenced json block, else the
97
+ first balanced {...} block (Chinese responses often embed prose before
98
+ the JSON)."""
99
+ fence = text.find("```json")
100
+ start = text.find("{", fence + 7 if fence >= 0 else 0)
101
+ if start < 0:
102
+ return None
103
+ depth = 0
104
+ for i in range(start, len(text)):
105
+ if text[i] == "{":
106
+ depth += 1
107
+ elif text[i] == "}":
108
+ depth -= 1
109
+ if depth == 0:
110
+ block = text[start:i + 1]
111
+ try:
112
+ data = json.loads(block)
113
+ return data if isinstance(data, dict) else None
114
+ except json.JSONDecodeError:
115
+ return None
116
+ return None
117
+
118
+
119
+ def _flatten(obj: Any) -> str:
120
+ if isinstance(obj, str):
121
+ return obj
122
+ if isinstance(obj, list):
123
+ return " ".join(_flatten(x) for x in obj)
124
+ if isinstance(obj, dict):
125
+ return " ".join(_flatten(v) for v in obj.values())
126
+ return str(obj)
127
+
128
+
129
+ def extract_outcomes(text: str) -> set[str]:
130
+ found = {o for o in OUTCOME_SET if o in text}
131
+ # Chinese result descriptors -> taxonomy tokens (language fairness)
132
+ for zh, token in ZH_OUTCOME_MAP.items():
133
+ if zh in text:
134
+ found.add(token)
135
+ data = extract_json_block(text)
136
+ if data:
137
+ for c in data.get("claims") or []:
138
+ ot = (c or {}).get("outcome_type")
139
+ if ot in OUTCOME_SET:
140
+ found.add(ot)
141
+ elif isinstance(ot, str) and ot in ZH_OUTCOME_MAP:
142
+ found.add(ZH_OUTCOME_MAP[ot])
143
+ return found
144
+
145
+
146
+ def extract_action(text: str) -> str | None:
147
+ data = extract_json_block(text)
148
+ if data:
149
+ for key in ("recommended_action", "final_recommendation", "decision"):
150
+ val = data.get(key)
151
+ if isinstance(val, str) and val in ACTION_TOKENS:
152
+ return val
153
+ low = text.lower()
154
+ for token in ACTION_TOKENS:
155
+ if token in low:
156
+ return token
157
+ for token, zh in ACTION_ZH.items():
158
+ if zh in text:
159
+ return token
160
+ return None
161
+
162
+
163
+ def _split_contradiction_units(text: str) -> list[str]:
164
+ """Heuristic units: JSON contradictions list, else hint sentences."""
165
+ data = extract_json_block(text)
166
+ if data:
167
+ for key in ("contradictions", "contradictory_evidence", "risks"):
168
+ val = data.get(key)
169
+ if isinstance(val, list):
170
+ units = [_flatten(x).strip() for x in val]
171
+ if any(units):
172
+ return [u for u in units if u]
173
+ sentences = re.split(r"[。!?!?;;\n]", text)
174
+ return [s.strip() for s in sentences
175
+ if s.strip() and any(h in s.lower() for h in CONTRADICTION_HINT)]
176
+
177
+
178
+ def _mentions_source(resp_text: str, gold_source: str) -> bool:
179
+ """Does the response plausibly cite this gold source? Heuristic: strong
180
+ token overlap with the source string, or author name + year co-occurrence."""
181
+ if _overlap(resp_text, gold_source) >= 0.5:
182
+ return True
183
+ years = _YEAR_RE.findall(gold_source)
184
+ names = [w for w in _words(gold_source) if w not in ("the", "and", "et", "al", "study")]
185
+ if names and years:
186
+ return any(n in resp_text.lower() for n in names) and any(y in resp_text for y in years)
187
+ return False
188
+
189
+
190
+ def evaluate_attempt(response_text: str, gold: dict[str, Any]) -> dict[str, Any]:
191
+ resp = response_text or ""
192
+ gold_outcomes = set(gold.get("correct_outcome_types") or [])
193
+ resp_outcomes = extract_outcomes(resp)
194
+ # Outcome separation accuracy = gold coverage (recall): did the response
195
+ # identify the gold outcome types? Jaccard would punish thorough analyses
196
+ # that also mention adjacent outcomes (see docs/benchmark.md fairness note).
197
+ if gold_outcomes:
198
+ outcome_jaccard = len(gold_outcomes & resp_outcomes) / len(gold_outcomes)
199
+ else:
200
+ outcome_jaccard = 1.0 if not resp_outcomes else 0.0
201
+
202
+ action = extract_action(resp)
203
+ decision_ok = action in set(gold.get("expected_decision_range") or [])
204
+
205
+ gold_contra = [str(x) for x in (gold.get("known_contradictions") or []) if str(x).strip()]
206
+ resp_units = _split_contradiction_units(resp)
207
+
208
+ # Chinese sentence matching: bigram overlap OR shared 4+ char phrase
209
+ # (language fairness; a strict 0.35 bigram gate misses paraphrases).
210
+ def _contra_hit(unit: str, g: str) -> bool:
211
+ if _overlap(unit, g) >= 0.20:
212
+ return True
213
+ for n in (6, 5, 4):
214
+ gs = set(g[i:i + n] for i in range(len(g) - n + 1))
215
+ if any(p in unit for p in gs):
216
+ return True
217
+ return False
218
+
219
+ detected = [g for g in gold_contra if any(_contra_hit(u, g) for u in resp_units)]
220
+ contra_recall = len(detected) / len(gold_contra) if gold_contra else 1.0
221
+ contra_precision = (
222
+ sum(1 for u in resp_units if any(_contra_hit(u, g) for g in gold_contra))
223
+ / len(resp_units) if resp_units else 1.0)
224
+
225
+ gold_sources = [str(x) for x in (gold.get("key_supporting_sources") or []) if str(x).strip()]
226
+ cited = [s for s in gold_sources if _mentions_source(resp, s)]
227
+ citation_recall = len(cited) / len(gold_sources) if gold_sources else 1.0
228
+
229
+ scope_text = resp.lower()
230
+ scope_ok = any(m in scope_text for m in SCOPE_BOUNDARY_MARKERS)
231
+
232
+ return {
233
+ "outcome_separation_accuracy": round(outcome_jaccard, 4),
234
+ "decision_calibration": 1.0 if decision_ok else 0.0,
235
+ "contradiction_recall": round(contra_recall, 4),
236
+ "contradiction_precision": round(contra_precision, 4),
237
+ "citation_support_recall": round(citation_recall, 4),
238
+ "scope_calibration": 1.0 if scope_ok else 0.0,
239
+ "detected_outcomes": sorted(resp_outcomes),
240
+ "detected_action": action,
241
+ "method": "heuristic",
242
+ }
243
+
244
+
245
+ METRIC_KEYS = ("outcome_separation_accuracy", "decision_calibration",
246
+ "contradiction_recall", "contradiction_precision",
247
+ "citation_support_recall", "scope_calibration")
248
+
249
+
250
+ def _mean(values: list[float]) -> float:
251
+ return sum(values) / len(values) if values else 0.0
252
+
253
+
254
+ def _ci95(values: list[float]) -> float:
255
+ n = len(values)
256
+ if n < 2:
257
+ return 0.0
258
+ m = _mean(values)
259
+ sd = math.sqrt(sum((v - m) ** 2 for v in values) / (n - 1))
260
+ return 1.96 * sd / math.sqrt(n)
261
+
262
+
263
+ def evaluate_run(run_dir: Path, manifest: dict[str, Any], annotations_dir: Path) -> dict[str, Any]:
264
+ golds: dict[str, dict[str, Any]] = {}
265
+ for path in sorted(Path(annotations_dir).glob("gold-*.json")):
266
+ g = json.loads(path.read_text(encoding="utf-8"))
267
+ golds[g.get("id", path.stem.replace("gold-", ""))] = g
268
+
269
+ per_attempt: list[dict[str, Any]] = []
270
+ for entry in manifest.get("attempts", []):
271
+ row = {"attempt_id": entry["attempt_id"], "question_id": entry["question_id"],
272
+ "baseline": entry["baseline"], "attempt": entry["attempt"],
273
+ "status": entry["status"]}
274
+ if entry["status"] != "completed":
275
+ per_attempt.append(row)
276
+ continue
277
+ artifact_name = (entry.get("artifacts") or [None])[0]
278
+ if not artifact_name:
279
+ per_attempt.append(row)
280
+ continue
281
+ artifact = json.loads((run_dir / artifact_name).read_text(encoding="utf-8"))
282
+ gold = golds.get(entry["question_id"])
283
+ if not gold:
284
+ row["metrics"] = None
285
+ row["error"] = "no gold annotation"
286
+ per_attempt.append(row)
287
+ continue
288
+ metrics = evaluate_attempt(artifact.get("response", ""), gold)
289
+ row["metrics"] = {k: metrics[k] for k in METRIC_KEYS}
290
+ row["detected_action"] = metrics["detected_action"]
291
+ row["cost_usd"] = entry.get("cost_usd")
292
+ per_attempt.append(row)
293
+
294
+ by_baseline: dict[str, list[dict[str, Any]]] = {}
295
+ for row in per_attempt:
296
+ if row["status"] == "completed" and row.get("metrics"):
297
+ by_baseline.setdefault(row["baseline"], []).append(row)
298
+
299
+ per_baseline: dict[str, dict[str, Any]] = {}
300
+ for baseline, rows in by_baseline.items():
301
+ metrics: dict[str, dict[str, float]] = {}
302
+ for key in METRIC_KEYS:
303
+ values = [r["metrics"][key] for r in rows]
304
+ metrics[key] = {"mean": round(_mean(values), 4),
305
+ "ci95": round(_ci95(values), 4), "n": len(values)}
306
+ per_baseline[baseline] = {
307
+ "metrics": metrics,
308
+ "n": len(rows),
309
+ "total_cost_usd": round(sum(r.get("cost_usd") or 0.0 for r in rows), 4),
310
+ }
311
+
312
+ return {
313
+ "run_id": manifest.get("run_id"),
314
+ "run_mode": manifest.get("run_mode"),
315
+ "environment": manifest.get("environment"),
316
+ "per_baseline": per_baseline,
317
+ "per_attempt": per_attempt,
318
+ }
319
+
320
+
321
+ def report_from_run(run_dir: Path, manifest: dict[str, Any], out_path: Path) -> str:
322
+ """Render the empirical benchmark report. The header states the run mode
323
+ explicitly so SIMULATED data can never be read as real performance."""
324
+ eval_path = run_dir / "evaluation.json"
325
+ if eval_path.is_file():
326
+ summary = json.loads(eval_path.read_text(encoding="utf-8"))
327
+ else:
328
+ summary = evaluate_run(run_dir, manifest, run_dir.parent.parent / "annotations")
329
+
330
+ mode = summary.get("run_mode", "unknown")
331
+ env = summary.get("environment", {}) or {}
332
+ attempts = manifest.get("attempts", [])
333
+ n_failed = sum(1 for a in attempts if a.get("status") == "failed")
334
+ n_budget = sum(1 for a in attempts if a.get("status") == "budget_stopped")
335
+ notes = manifest.get("notes") or ""
336
+ lines = [
337
+ "# EduEvidence Benchmark Report (v3)",
338
+ "",
339
+ f"- run_id: {summary.get('run_id')}",
340
+ f"- mode: **{'SIMULATED - harness validation only, NOT model performance' if mode == 'simulated' else 'EMPIRICAL'}**",
341
+ f"- driver: {env.get('driver')} | model: {env.get('model_family')} "
342
+ f"({env.get('model_version')}) | temperature: {env.get('temperature')}",
343
+ f"- tools: {', '.join(env.get('tools') or []) or 'none'} | "
344
+ f"search_provider: {env.get('search_provider')} | agent_mcp_used: {env.get('agent_mcp_used')}",
345
+ f"- attempts: {len(attempts)} total | failed: {n_failed} | budget_stopped: {n_budget}",
346
+ f"- notes: {notes or 'none'}",
347
+ "- cost: usage not metered by the cli/api driver (reported as 0.0 = NOT CAPTURED, not free)",
348
+ "",
349
+ "| Baseline | n | outcome_sep | decision_cal | contra_recall | contra_precision | citation_recall | scope_cal | cost_usd |",
350
+ "|---|---|---|---|---|---|---|---|---|",
351
+ ]
352
+ for baseline in sorted(summary.get("per_baseline", {})):
353
+ m = summary["per_baseline"][baseline]["metrics"]
354
+ line = (f"| {baseline} | {m['outcome_separation_accuracy']['n']} "
355
+ f"| {m['outcome_separation_accuracy']['mean']:.3f}+-{m['outcome_separation_accuracy']['ci95']:.3f} "
356
+ f"| {m['decision_calibration']['mean']:.3f}+-{m['decision_calibration']['ci95']:.3f} "
357
+ f"| {m['contradiction_recall']['mean']:.3f}+-{m['contradiction_recall']['ci95']:.3f} "
358
+ f"| {m['contradiction_precision']['mean']:.3f}+-{m['contradiction_precision']['ci95']:.3f} "
359
+ f"| {m['citation_support_recall']['mean']:.3f}+-{m['citation_support_recall']['ci95']:.3f} "
360
+ f"| {m['scope_calibration']['mean']:.3f}+-{m['scope_calibration']['ci95']:.3f} "
361
+ f"| {summary['per_baseline'][baseline]['total_cost_usd']} |")
362
+ lines.append(line)
363
+ if mode == "simulated":
364
+ lines += [
365
+ "",
366
+ "> **SIMULATED**: deterministic synthetic data. This report validates the harness only "
367
+ "and must never be presented as model performance (docs/benchmark.md Layer A vs Layer B).",
368
+ ]
369
+ markdown = "\n".join(lines) + "\n"
370
+ out_path.write_text(markdown, encoding="utf-8")
371
+ return markdown