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,292 @@
1
+ #!/usr/bin/env python3
2
+ """DOI audit: verify every DOI cited in examples/ against its registry.
3
+
4
+ R1 of docs/plans/v5.2-v6.0-iteration-plan.md. For every DOI found under
5
+ examples/**/*.json(on):
6
+
7
+ - Crossref hosts most DOIs; arXiv's 10.48550/* lives on DataCite.
8
+ - ok resolves, and (when a local title is paired) titles overlap
9
+ above the threshold
10
+ - mismatch resolves but the registered title looks like a different work
11
+ - not_found registry has no such DOI — fabricated or malformed
12
+ - error network / unexpected failure
13
+
14
+ Writes benchmarks/doi-audit/report.json + report.md (with per-file
15
+ provenance) and exits 1 when any not_found/mismatch/error exists unless
16
+ --no-fail. Stdlib only.
17
+
18
+ Security notes: DOIs come from repo files, so requests are pinned to two
19
+ allow-listed HTTPS registry hosts, the DOI must match a strict shape check,
20
+ and redirects are only followed within those hosts.
21
+
22
+ Usage:
23
+ python3 scripts/audit_dois.py [--examples-dir examples] [--sleep 0.3]
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import argparse
29
+ import http.client
30
+ import json
31
+ import re
32
+ import sys
33
+ import time
34
+ import urllib.error
35
+ import urllib.parse
36
+ import urllib.request
37
+ from collections import defaultdict
38
+ from pathlib import Path
39
+
40
+ REPO_ROOT = Path(__file__).resolve().parent.parent
41
+
42
+ DOI_RE = re.compile(r"\b(10\.\d{4,9}/[A-Za-z0-9._()/:;-]+)")
43
+ _STRICT_DOI_RE = re.compile(r"^10\.\d{4,9}/[A-Za-z0-9._()/:-]+$")
44
+ DOI_KEY_RE = re.compile(r"(^|_)(doi)$")
45
+ TITLE_KEY_RE = re.compile(r"(^|_)(title)$")
46
+ STRIP_SUFFIX = ".,;)"
47
+
48
+ CROSSREF_API = "https://api.crossref.org/works/{doi}"
49
+ DATACITE_API = "https://api.datacite.org/dois/{doi}"
50
+ ALLOWED_HOSTS = {"api.crossref.org", "api.datacite.org"}
51
+ # arXiv mints DOIs via DataCite, not Crossref — route them accordingly.
52
+ DATACITE_PREFIXES = {"10.48550"}
53
+ MAILTO = "doi-audit@eduevidence.local"
54
+ OVERLAP_THRESHOLD = 0.5
55
+
56
+
57
+ def _clean(doi: str) -> str:
58
+ return doi.rstrip(STRIP_SUFFIX)
59
+
60
+
61
+ def _tokens(text: str) -> set[str]:
62
+ out: set[str] = set()
63
+ for tok in re.split(r"[^a-z0-9]+", text.lower()):
64
+ if len(tok) > 2:
65
+ out.add(tok)
66
+ if tok.endswith("s") and len(tok) > 3:
67
+ out.add(tok[:-1]) # crude plural fold
68
+ return out
69
+
70
+
71
+ def _overlap(a: str | None, b: str | None) -> float | None:
72
+ if not a or not b:
73
+ return None
74
+ ta, tb = _tokens(a), _tokens(b)
75
+ if not ta or not tb:
76
+ return None
77
+ return len(ta & tb) / min(len(ta), len(tb))
78
+
79
+
80
+ def walk_records(obj: object) -> dict[str, str | None]:
81
+ """Yield doi -> paired title (or None) for every DOI-bearing dict.
82
+
83
+ DOIs appearing bare inside string values are collected too (paired with
84
+ no local title), so prose citations are audited as well.
85
+ """
86
+ found: dict[str, str | None] = {}
87
+
88
+ def add(doi: str, title: str | None) -> None:
89
+ if title and not found.get(doi):
90
+ found[doi] = title
91
+ elif doi not in found:
92
+ found[doi] = None
93
+
94
+ if isinstance(obj, dict):
95
+ doi_val = None
96
+ title_val = None
97
+ for key, val in obj.items():
98
+ if isinstance(val, str):
99
+ key_l = key.strip().lower()
100
+ if DOI_KEY_RE.search(key_l) and DOI_RE.match(val.strip()):
101
+ doi_val = _clean(val.strip())
102
+ elif TITLE_KEY_RE.search(key_l) and title_val is None:
103
+ title_val = val.strip()
104
+ else:
105
+ for raw in DOI_RE.findall(val):
106
+ add(_clean(raw), None)
107
+ elif isinstance(val, list) and TITLE_KEY_RE.search(key.strip().lower()):
108
+ # Crossref-style [[ "Title" ]] containers
109
+ for item in val:
110
+ if isinstance(item, str) and title_val is None:
111
+ title_val = item
112
+ break
113
+ if doi_val:
114
+ add(doi_val, title_val)
115
+ for val in obj.values():
116
+ for sub_doi, sub_title in walk_records(val).items():
117
+ add(sub_doi, sub_title)
118
+ elif isinstance(obj, list):
119
+ for item in obj:
120
+ found.update(walk_records(item))
121
+ return found
122
+
123
+
124
+ def collect(records_dir: Path) -> tuple[dict[str, dict], int]:
125
+ """Return doi -> {"titles": Counter-like, "files": [..]} plus occurrences."""
126
+ paired: dict[str, dict] = {}
127
+
128
+ def add(doi: str, title: str | None, rel: str) -> None:
129
+ entry = paired.setdefault(doi, {"titles": {}, "files": []})
130
+ if title:
131
+ entry["titles"][title] = entry["titles"].get(title, 0) + 1
132
+ if rel not in entry["files"]:
133
+ entry["files"].append(rel)
134
+
135
+ occurrences = 0
136
+ for path in sorted(records_dir.rglob("*")):
137
+ if path.suffix not in {".json", ".jsonl"} or not path.is_file():
138
+ continue
139
+ rel = str(path.relative_to(records_dir.parent))
140
+ try:
141
+ if path.suffix == ".jsonl":
142
+ docs = [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
143
+ else:
144
+ docs = [json.loads(path.read_text(encoding="utf-8"))]
145
+ except (json.JSONDecodeError, UnicodeDecodeError) as exc:
146
+ print(f"WARN: skip unparseable {rel}: {exc}", file=sys.stderr)
147
+ continue
148
+ for doc in docs:
149
+ recs = walk_records(doc)
150
+ occurrences += len(recs)
151
+ for doi, title in recs.items():
152
+ add(doi, title, rel)
153
+ return paired, occurrences
154
+
155
+
156
+ class _SameHostRedirectHandler(urllib.request.HTTPRedirectHandler):
157
+ """Only follow redirects that stay on the allow-listed registry hosts."""
158
+
159
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
160
+ if urllib.parse.urlparse(newurl).hostname not in ALLOWED_HOSTS:
161
+ return None
162
+ return super().redirect_request(req, fp, code, msg, headers, newurl)
163
+
164
+
165
+ class RegistryClient:
166
+ def __init__(self, sleep_s: float) -> None:
167
+ self.sleep_s = sleep_s
168
+ self.cache: dict[str, tuple[str, dict | None]] = {}
169
+ self.opener = urllib.request.build_opener(_SameHostRedirectHandler)
170
+
171
+ def fetch(self, doi: str) -> tuple[str, dict | None]:
172
+ if doi in self.cache:
173
+ return self.cache[doi]
174
+ # Defense-in-depth: the DOI comes from repo files, so pin its shape
175
+ # and only ever hit the two allow-listed hosts over HTTPS.
176
+ if not _STRICT_DOI_RE.match(doi):
177
+ result: tuple[str, dict | None] = ("error", {"detail": f"strict shape check failed: {doi!r}"})
178
+ self.cache[doi] = result
179
+ return result
180
+ prefix = doi.split("/", 1)[0]
181
+ template = DATACITE_API if prefix in DATACITE_PREFIXES else CROSSREF_API
182
+ url = template.format(doi=urllib.request.quote(doi, safe=""))
183
+ req = urllib.request.Request(url, headers={"User-Agent": f"EduEvidence-doi-audit/1.0 (mailto:{MAILTO})"})
184
+ result = ("error", None)
185
+ for attempt in range(2):
186
+ try:
187
+ with self.opener.open(req, timeout=20) as resp:
188
+ payload = json.loads(resp.read().decode("utf-8"))
189
+ message = payload.get("message", payload.get("data", {}))
190
+ if isinstance(message, dict) and "attributes" in message:
191
+ attrs = message["attributes"] # DataCite JSON:API envelope
192
+ titles = [t.get("title") for t in attrs.get("titles", []) if t.get("title")]
193
+ message = {"title": titles}
194
+ result = ("ok", message)
195
+ break
196
+ except urllib.error.HTTPError as exc:
197
+ result = ("not_found", {"status": exc.code}) if exc.code == 404 else ("error", {"status": exc.code})
198
+ break
199
+ except (urllib.error.URLError, http.client.RemoteDisconnected, ConnectionError,
200
+ TimeoutError, OSError, json.JSONDecodeError) as exc:
201
+ result = ("error", {"detail": str(exc)})
202
+ if attempt == 0:
203
+ time.sleep(1.5)
204
+ time.sleep(self.sleep_s)
205
+ self.cache[doi] = result
206
+ return result
207
+
208
+
209
+ def main() -> int:
210
+ parser = argparse.ArgumentParser(description=__doc__)
211
+ parser.add_argument("--examples-dir", default="examples")
212
+ parser.add_argument("--sleep", type=float, default=0.3, help="seconds between registry calls")
213
+ parser.add_argument("--no-fail", action="store_true", help="always exit 0")
214
+ args = parser.parse_args()
215
+
216
+ records_dir = REPO_ROOT / args.examples_dir
217
+ if not records_dir.is_dir():
218
+ print(f"FAIL: {records_dir} not found")
219
+ return 2
220
+
221
+ paired, occurrences = collect(records_dir)
222
+ print(f"collected {len(paired)} unique DOIs ({occurrences} occurrences) under {records_dir}")
223
+
224
+ client = RegistryClient(args.sleep)
225
+ rows = []
226
+ counts: defaultdict[str, int] = defaultdict(int)
227
+ items = sorted(paired.items())
228
+ for i, (doi, info) in enumerate(items, 1):
229
+ local_title = max(info["titles"], key=lambda t: info["titles"][t]) if info["titles"] else None
230
+ status, msg = client.fetch(doi)
231
+ cr_title = None
232
+ if isinstance(msg, dict):
233
+ titles = msg.get("title") or []
234
+ cr_title = titles[0] if titles else None
235
+ similarity = _overlap(local_title, cr_title) if status == "ok" else None
236
+ if status == "ok" and similarity is not None and similarity < OVERLAP_THRESHOLD:
237
+ status = "mismatch"
238
+ counts[status] += 1
239
+ rows.append({
240
+ "doi": doi,
241
+ "status": status,
242
+ "local_titles": sorted(info["titles"]),
243
+ "files": info["files"],
244
+ "crossref_title": cr_title,
245
+ "title_overlap": round(similarity, 3) if similarity is not None else None,
246
+ })
247
+ print(f"[{i}/{len(items)}] {status:9s} {doi}")
248
+
249
+ out_dir = REPO_ROOT / "benchmarks" / "doi-audit"
250
+ out_dir.mkdir(parents=True, exist_ok=True)
251
+ (out_dir / "report.json").write_text(
252
+ json.dumps({"generated_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
253
+ "unique_dois": len(paired),
254
+ "counts": dict(counts),
255
+ "results": rows}, ensure_ascii=False, indent=2),
256
+ encoding="utf-8")
257
+
258
+ lines = ["# DOI Audit Report", "",
259
+ f"生成时间:{time.strftime('%Y-%m-%d %H:%M %Z')};数据源:api.crossref.org / api.datacite.org",
260
+ f"唯一 DOI:{len(paired)};分类:{dict(counts)}",
261
+ "判定说明:not_found=注册表无此 DOI;mismatch=解析成功但标题指向其他论文(阈值 "
262
+ f"{OVERLAP_THRESHOLD},需人工复核边界案例)。", ""]
263
+ bad = [r for r in rows if r["status"] != "ok"]
264
+ if bad:
265
+ lines += ["## 需处理(mismatch / not_found / error)", ""]
266
+ for r in bad:
267
+ lt = "; ".join(r["local_titles"])[:90].replace("|", "\\|") or "(无配对标题)"
268
+ ct = (r["crossref_title"] or "")[:70].replace("|", "\\|")
269
+ lines.append(f"- **{r['status']}** `{r['doi']}`")
270
+ lines.append(f" - 本地标题:{lt}")
271
+ if ct:
272
+ lines.append(f" - 注册表标题:{ct}")
273
+ lines.append(f" - 出现文件:{', '.join(r['files'])}")
274
+ good = [r for r in rows if r["status"] == "ok"]
275
+ if good:
276
+ lines += ["", f"## 通过({len(good)} 条)", "", "| DOI | 标题重合度 | 出现文件 |", "|---|---|---|"]
277
+ for r in good:
278
+ sim = "—" if r["title_overlap"] is None else r["title_overlap"]
279
+ files = ", ".join(r["files"])
280
+ lines.append(f"| `{r['doi']}` | {sim} | {files} |")
281
+ (out_dir / "report.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
282
+
283
+ print(f"\nsummary: {dict(counts)} -> {out_dir}/report.(json|md)")
284
+ fatal = sum(counts[s] for s in ("not_found", "mismatch", "error"))
285
+ if fatal and not args.no_fail:
286
+ print(f"FAIL: {fatal} DOI(s) need attention")
287
+ return 1
288
+ return 0
289
+
290
+
291
+ if __name__ == "__main__":
292
+ sys.exit(main())
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env bash
2
+ # bake_pack.sh — Render main + 5-theme HTML reports and refresh artifact
3
+ # manifest for one example pack. Usage:
4
+ # bash scripts/bake_pack.sh examples/ai-coding-assistant-evidence
5
+ set -euo pipefail
6
+
7
+ PACK="${1:?usage: bake_pack.sh examples/<pack>}"
8
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
9
+ BR="$ROOT/visualization/eduevidence-report/scripts"
10
+ PY="${PYTHON:-python3}"
11
+
12
+ R_EN="$PACK/result.json"
13
+ R_ZH="$PACK/result.zh.json"
14
+ THEMES_DIR="$PACK/reports-5themes"
15
+
16
+ test -f "$ROOT/$R_EN" || { echo "missing $R_EN"; exit 1; }
17
+ test -f "$ROOT/$R_ZH" || { echo "missing $R_ZH"; exit 1; }
18
+ mkdir -p "$ROOT/$THEMES_DIR"
19
+
20
+ cd "$ROOT"
21
+
22
+ echo "== main report (claude) =="
23
+ "$PY" "$BR/build_report.py" --result "$R_EN" --result-zh "$R_ZH" \
24
+ --out "$PACK/EduEvidence_Report.html"
25
+
26
+ for t in claude academic datalab datalab-dark presentation; do
27
+ echo "== theme $t =="
28
+ "$PY" "$BR/build_report.py" --result "$R_EN" --result-zh "$R_ZH" \
29
+ --theme "$t" --out "$THEMES_DIR/report_$t.html"
30
+ cp "$THEMES_DIR/report_$t.html" "$THEMES_DIR/EduEvidence_Report_$t.html"
31
+ done
32
+
33
+ echo "== artifact manifest =="
34
+ "$PY" "$BR/build_artifact_manifest.py" --result "$R_EN" --result-zh "$R_ZH" \
35
+ --html-dir "$THEMES_DIR" --out "$PACK/artifact_manifest.json"
36
+
37
+ echo "bake complete: $PACK"
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env python3
2
+ """benchmark.py — Run the EduEvidence benchmark harness (plan sections 30-36).
3
+
4
+ First version: 30 questions (S x10, M x10, L x10), with at least 10 gold-annotated
5
+ questions. Baselines B0-B4 and ablation A1-A7 are defined here as metadata; actual
6
+ LLM runs are executed externally and results stored under benchmarks/results/.
7
+
8
+ Core metrics computed from result files:
9
+ - Citation Support Precision
10
+ - Unsupported Claim Rate
11
+ - Contradiction Discovery Rate
12
+ - Outcome Separation Accuracy
13
+ - Scope Calibration
14
+
15
+ Usage:
16
+ python scripts/benchmark.py --questions benchmarks/questions.jsonl
17
+ python scripts/benchmark.py --questions benchmarks/questions.jsonl --results benchmarks/results/sample.json --annotations benchmarks/annotations
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import json
23
+ import sys
24
+ from pathlib import Path
25
+
26
+ LEVELS = ("S", "M", "L")
27
+ DOMAINS = ("ai_higher_education", "teaching_methods", "learning_psychology", "assessment_edtech")
28
+ OUTCOME_SET = {
29
+ "knowledge_gain", "concept_understanding", "retention", "transfer",
30
+ "independent_problem_solving", "completion_time", "accuracy",
31
+ "code_quality", "assignment_score", "engagement", "motivation",
32
+ "cognitive_load", "help_seeking", "metacognition", "ai_dependency",
33
+ "over_reliance", "reduced_effort", "reduced_transfer",
34
+ "academic_integrity_risk", "false_confidence",
35
+ }
36
+ BASELINES = ("B0_direct_llm", "B1_search_llm", "B2_standard_agent",
37
+ "B3_eduevidence_single", "B4_eduevidence_agent_mcp")
38
+ ABLATIONS = ("A1_no_skeptic", "A2_no_method_reviewer", "A3_no_tribunal",
39
+ "A4_no_applicability", "A5_no_claim_audit", "A6_no_multi_agent",
40
+ "A7_no_complexity_gate")
41
+
42
+
43
+ def load_questions(path: Path) -> list[dict]:
44
+ questions = []
45
+ for lineno, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
46
+ line = line.strip()
47
+ if not line:
48
+ continue
49
+ try:
50
+ q = json.loads(line)
51
+ except json.JSONDecodeError as exc:
52
+ raise ValueError(f"{path}:{lineno}: invalid JSON: {exc}") from exc
53
+ questions.append(q)
54
+ return questions
55
+
56
+
57
+ def validate_questions(questions: list[dict]) -> list[str]:
58
+ """Structural validation of the question set. Returns a list of issues (empty = valid)."""
59
+ issues = []
60
+ ids = [q.get("id") for q in questions]
61
+ if len(ids) != len(set(ids)):
62
+ issues.append("duplicate question ids")
63
+ if len(questions) < 30:
64
+ issues.append(f"expected >= 30 questions, got {len(questions)}")
65
+ for q in questions:
66
+ if q.get("level") not in LEVELS:
67
+ issues.append(f"{q.get('id')}: bad level {q.get('level')!r}")
68
+ if q.get("domain") not in DOMAINS:
69
+ issues.append(f"{q.get('id')}: bad domain {q.get('domain')!r}")
70
+ for o in q.get("expected_outcomes", []):
71
+ if o not in OUTCOME_SET:
72
+ issues.append(f"{q.get('id')}: bad outcome {o!r}")
73
+ # Level distribution
74
+ for level in LEVELS:
75
+ count = sum(1 for q in questions if q.get("level") == level)
76
+ if count < 10:
77
+ issues.append(f"level {level}: expected >= 10, got {count}")
78
+ # Domain distribution (>= 15 ai_higher_education)
79
+ ai = sum(1 for q in questions if q.get("domain") == "ai_higher_education")
80
+ if ai < 15:
81
+ issues.append(f"domain ai_higher_education: expected >= 15, got {ai}")
82
+ return issues
83
+
84
+
85
+ def metric_citation_support_precision(result: dict) -> float:
86
+ """Proportion of cited sources that truly support the claims they are attached to."""
87
+ cited = result.get("citations", [])
88
+ if not cited:
89
+ return 0.0
90
+ supported = sum(1 for c in cited if c.get("supports_claim") is True)
91
+ return supported / len(cited)
92
+
93
+
94
+ def metric_unsupported_claim_rate(result: dict) -> float:
95
+ """Proportion of important claims that cannot be bound to a reliable source."""
96
+ claims = result.get("claims", [])
97
+ if not claims:
98
+ return 0.0
99
+ unsupported = sum(1 for c in claims if c.get("status") == "UNSUPPORTED")
100
+ return unsupported / len(claims)
101
+
102
+
103
+ def metric_contradiction_discovery(result: dict, annotation: dict) -> float:
104
+ """Fraction of known contradictions (from gold annotation) discovered by the system.
105
+
106
+ Semantics: if the gold annotation lists no known contradictions, the metric
107
+ scores 1.0 only when the system also reported none (nothing to find); if the
108
+ system reported contradictions where none are known, that counts as failure.
109
+ Membership test is restricted to string entries (model-generated results may
110
+ contain non-string objects).
111
+ """
112
+ known = annotation.get("known_contradictions", [])
113
+ found = result.get("discovered_contradictions", [])
114
+ found_strings = [f for f in found if isinstance(f, str)]
115
+ if not known:
116
+ return 1.0 if not found else 0.0
117
+ hits = sum(1 for k in known if any(k in f for f in found_strings))
118
+ return hits / len(known)
119
+
120
+
121
+ def metric_outcome_separation(result: dict) -> float:
122
+ """Fraction of evidence rows where outcome_type belongs to the taxonomy and is correctly typed."""
123
+ rows = result.get("evidence", [])
124
+ if not rows:
125
+ return 0.0
126
+ correct = sum(1 for r in rows if r.get("outcome_type") in OUTCOME_SET)
127
+ return correct / len(rows)
128
+
129
+
130
+ def metric_scope_calibration(result: dict) -> float:
131
+ """Fraction of verdicts that do not overstate the scope of the sources."""
132
+ verdicts = result.get("verdicts", [])
133
+ if not verdicts:
134
+ return 0.0
135
+ ok = sum(1 for v in verdicts if not v.get("exceeds_evidence_boundary", False))
136
+ return ok / len(verdicts)
137
+
138
+
139
+ def evaluate(result: dict, annotation: dict | None = None) -> dict:
140
+ """Compute the core metrics for one result (optionally against a gold annotation)."""
141
+ metrics = {
142
+ "citation_support_precision": round(metric_citation_support_precision(result), 3),
143
+ "unsupported_claim_rate": round(metric_unsupported_claim_rate(result), 3),
144
+ "outcome_separation_accuracy": round(metric_outcome_separation(result), 3),
145
+ "scope_calibration": round(metric_scope_calibration(result), 3),
146
+ }
147
+ if annotation is not None:
148
+ metrics["contradiction_discovery_rate"] = round(
149
+ metric_contradiction_discovery(result, annotation), 3)
150
+ return metrics
151
+
152
+
153
+ def main() -> int:
154
+ parser = argparse.ArgumentParser(description="EduEvidence benchmark harness")
155
+ parser.add_argument("--questions", required=True, help="benchmarks/questions.jsonl")
156
+ parser.add_argument("--results", help="optional result JSON to score")
157
+ parser.add_argument("--annotations", help="optional annotations directory")
158
+ args = parser.parse_args()
159
+
160
+ questions = load_questions(Path(args.questions))
161
+ issues = validate_questions(questions)
162
+ if issues:
163
+ for issue in issues:
164
+ print(f"ISSUE: {issue}", file=sys.stderr)
165
+ return 1
166
+
167
+ dist = {level: sum(1 for q in questions if q["level"] == level) for level in LEVELS}
168
+ print(f"OK: {len(questions)} questions | levels {dist}")
169
+
170
+ if args.results:
171
+ result = json.loads(Path(args.results).read_text(encoding="utf-8"))
172
+ annotation = None
173
+ if args.annotations:
174
+ ann_path = Path(args.annotations) / f"gold-{result.get('id', '')}.json"
175
+ if ann_path.exists():
176
+ annotation = json.loads(ann_path.read_text(encoding="utf-8"))
177
+ metrics = evaluate(result, annotation)
178
+ print(json.dumps(metrics, ensure_ascii=False, indent=2))
179
+ return 0
180
+
181
+
182
+ if __name__ == "__main__":
183
+ sys.exit(main())