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.
- package/LICENSE +21 -0
- package/README.md +506 -0
- package/README.zh-CN.md +434 -0
- package/SKILL.md +195 -0
- package/bin/eduevidence.js +127 -0
- package/domains/education/manifest.json +183 -0
- package/domains/education/outcome_taxonomy.json +127 -0
- package/domains/manifest.json +26 -0
- package/domains/policy/frame.schema.json +234 -0
- package/domains/policy/manifest.json +10 -0
- package/domains/policy/methodology_checklist.json +109 -0
- package/domains/policy/outcome_taxonomy.json +53 -0
- package/domains/policy/references/causal-identification.md +45 -0
- package/domains/policy/references/cost-evidence.md +44 -0
- package/domains/policy/references/equity.md +42 -0
- package/domains/policy/references/evidence-hierarchy.md +41 -0
- package/domains/policy/references/implementation-evidence.md +47 -0
- package/eduevidence_cli.py +26 -0
- package/engine/__init__.py +11 -0
- package/engine/__pycache__/__init__.cpython-312.pyc +0 -0
- package/engine/__pycache__/analysis.cpython-312.pyc +0 -0
- package/engine/__pycache__/bias.cpython-312.pyc +0 -0
- package/engine/__pycache__/briefs.cpython-312.pyc +0 -0
- package/engine/__pycache__/capabilities.cpython-312.pyc +0 -0
- package/engine/__pycache__/citation_check.cpython-312.pyc +0 -0
- package/engine/__pycache__/contracts.cpython-312.pyc +0 -0
- package/engine/__pycache__/datasets.cpython-312.pyc +0 -0
- package/engine/__pycache__/events.cpython-312.pyc +0 -0
- package/engine/__pycache__/evidence_graph.cpython-312.pyc +0 -0
- package/engine/__pycache__/evidence_review.cpython-312.pyc +0 -0
- package/engine/__pycache__/evidencecore.cpython-312.pyc +0 -0
- package/engine/__pycache__/gap_lens.cpython-312.pyc +0 -0
- package/engine/__pycache__/gaps.cpython-312.pyc +0 -0
- package/engine/__pycache__/graph_store.cpython-312.pyc +0 -0
- package/engine/__pycache__/graph_validate.cpython-312.pyc +0 -0
- package/engine/__pycache__/ids.cpython-312.pyc +0 -0
- package/engine/__pycache__/library.cpython-312.pyc +0 -0
- package/engine/__pycache__/library_builtin.cpython-312.pyc +0 -0
- package/engine/__pycache__/living.cpython-312.pyc +0 -0
- package/engine/__pycache__/log.cpython-312.pyc +0 -0
- package/engine/__pycache__/meta_analysis.cpython-312.pyc +0 -0
- package/engine/__pycache__/meta_synthesis.cpython-312.pyc +0 -0
- package/engine/__pycache__/migration.cpython-312.pyc +0 -0
- package/engine/__pycache__/mode_router.cpython-312.pyc +0 -0
- package/engine/__pycache__/paths.cpython-312.pyc +0 -0
- package/engine/__pycache__/pilot.cpython-312.pyc +0 -0
- package/engine/__pycache__/planner.cpython-312.pyc +0 -0
- package/engine/__pycache__/project.cpython-312.pyc +0 -0
- package/engine/__pycache__/projections.cpython-312.pyc +0 -0
- package/engine/__pycache__/robustness.cpython-312.pyc +0 -0
- package/engine/__pycache__/run.cpython-312.pyc +0 -0
- package/engine/__pycache__/semantics.cpython-312.pyc +0 -0
- package/engine/__pycache__/study_design.cpython-312.pyc +0 -0
- package/engine/__pycache__/synthesis.cpython-312.pyc +0 -0
- package/engine/__pycache__/tribunal.cpython-312.pyc +0 -0
- package/engine/__pycache__/update.cpython-312.pyc +0 -0
- package/engine/__pycache__/versions.cpython-312.pyc +0 -0
- package/engine/analysis.py +308 -0
- package/engine/bias.py +178 -0
- package/engine/briefs.py +106 -0
- package/engine/capabilities.py +99 -0
- package/engine/citation_check.py +192 -0
- package/engine/contracts.py +117 -0
- package/engine/datasets.py +165 -0
- package/engine/events.py +67 -0
- package/engine/evidence_graph.py +571 -0
- package/engine/evidence_review.py +88 -0
- package/engine/evidencecore.py +182 -0
- package/engine/gap_lens.py +132 -0
- package/engine/gaps.py +169 -0
- package/engine/graph_store.py +335 -0
- package/engine/graph_validate.py +87 -0
- package/engine/ids.py +77 -0
- package/engine/library.py +268 -0
- package/engine/library_builtin.py +301 -0
- package/engine/living.py +671 -0
- package/engine/log.py +39 -0
- package/engine/meta_analysis.py +333 -0
- package/engine/meta_synthesis.py +111 -0
- package/engine/migration.py +397 -0
- package/engine/mode_router.py +72 -0
- package/engine/paths.py +15 -0
- package/engine/pilot.py +368 -0
- package/engine/planner.py +126 -0
- package/engine/project.py +118 -0
- package/engine/projections.py +240 -0
- package/engine/robustness.py +109 -0
- package/engine/run.py +85 -0
- package/engine/semantics.py +135 -0
- package/engine/study_design.py +87 -0
- package/engine/synthesis.py +187 -0
- package/engine/tribunal.py +408 -0
- package/engine/update.py +113 -0
- package/engine/versions.py +12 -0
- package/install.sh +510 -0
- package/integrations/__init__.py +1 -0
- package/integrations/__pycache__/__init__.cpython-312.pyc +0 -0
- package/integrations/__pycache__/agent_mcp.cpython-312.pyc +0 -0
- package/integrations/__pycache__/smart_web_fetch.cpython-312.pyc +0 -0
- package/integrations/agent_mcp.py +856 -0
- package/integrations/smart_web_fetch.py +59 -0
- package/package.json +50 -0
- package/pyproject.toml +55 -0
- package/references/applicability-policy.md +88 -0
- package/references/education-framing.md +132 -0
- package/references/effect_size_formulas.md +35 -0
- package/references/evaluation-design.md +111 -0
- package/references/evidence-quality.md +79 -0
- package/references/grade_framework.md +29 -0
- package/references/intervention-design.md +98 -0
- package/references/methodology-audit.md +103 -0
- package/references/outcome-taxonomy.md +106 -0
- package/references/retrieval-protocol.md +142 -0
- package/references/skeptic-protocol.md +93 -0
- package/references/social_science_pitfalls.md +48 -0
- package/references/source-validity.md +140 -0
- package/references/tribunal-policy.md +112 -0
- package/references/wwc_standards.md +29 -0
- package/retrieval/__init__.py +1 -0
- package/retrieval/__pycache__/__init__.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/corpus_store.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/dedupe.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/failures.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/fetch.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/search.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/source.cpython-312.pyc +0 -0
- package/retrieval/__pycache__/validate.cpython-312.pyc +0 -0
- package/retrieval/corpus_store.py +181 -0
- package/retrieval/dedupe.py +127 -0
- package/retrieval/failures.py +90 -0
- package/retrieval/fetch.py +435 -0
- package/retrieval/search.py +493 -0
- package/retrieval/source.py +160 -0
- package/retrieval/validate.py +257 -0
- package/schemas/agent-mcp-approval.schema.json +57 -0
- package/schemas/chart-spec.schema.json +88 -0
- package/schemas/cross-model-review.schema.json +28 -0
- package/schemas/education-frame.schema.json +106 -0
- package/schemas/evaluation.schema.json +35 -0
- package/schemas/evidence.schema.json +81 -0
- package/schemas/fetch-result.schema.json +119 -0
- package/schemas/intervention.schema.json +46 -0
- package/schemas/methodology.schema.json +102 -0
- package/schemas/report-result.schema.json +381 -0
- package/schemas/report-spec.schema.json +130 -0
- package/schemas/source.schema.json +311 -0
- package/schemas/v2/analysis-plan.schema.json +28 -0
- package/schemas/v2/analysis-run.schema.json +33 -0
- package/schemas/v2/claim.schema.json +26 -0
- package/schemas/v2/dataset-asset.schema.json +40 -0
- package/schemas/v2/decision-snapshot.schema.json +53 -0
- package/schemas/v2/evidence-link.schema.json +38 -0
- package/schemas/v2/finding.schema.json +47 -0
- package/schemas/v2/graph-revision.schema.json +30 -0
- package/schemas/v2/knowledge-gap.schema.json +40 -0
- package/schemas/v2/methodology-audit.schema.json +30 -0
- package/schemas/v2/outcome.schema.json +18 -0
- package/schemas/v2/project.schema.json +31 -0
- package/schemas/v2/research-intent.schema.json +24 -0
- package/schemas/v2/run.schema.json +43 -0
- package/schemas/v2/source.schema.json +24 -0
- package/schemas/v2/study-design.schema.json +67 -0
- package/schemas/v2/study.schema.json +37 -0
- package/schemas/v3/pilot-outcome.schema.json +132 -0
- package/schemas/v3/run-manifest.schema.json +193 -0
- package/schemas/v3/synthesis.schema.json +86 -0
- package/schemas/v4/drift-report.schema.json +66 -0
- package/schemas/v4/evidence-library.schema.json +42 -0
- package/schemas/v4/living-subscription.schema.json +55 -0
- package/schemas/v4/meta-analysis.schema.json +152 -0
- package/schemas/verdict.schema.json +56 -0
- package/scripts/__init__.py +0 -0
- package/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_evaluator.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_judge.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_routing.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_v2.cpython-312.pyc +0 -0
- package/scripts/__pycache__/benchmark_v3.cpython-312.pyc +0 -0
- package/scripts/__pycache__/build_result.cpython-312.pyc +0 -0
- package/scripts/__pycache__/claim_audit.cpython-312.pyc +0 -0
- package/scripts/__pycache__/complexity_gate.cpython-312.pyc +0 -0
- package/scripts/__pycache__/compute_confidence.cpython-312.pyc +0 -0
- package/scripts/__pycache__/dashboard_server.cpython-312.pyc +0 -0
- package/scripts/__pycache__/did_regression.cpython-312.pyc +0 -0
- package/scripts/__pycache__/effect_calculator.cpython-312.pyc +0 -0
- package/scripts/__pycache__/evidence_matrix.cpython-312.pyc +0 -0
- package/scripts/__pycache__/evidence_score.cpython-312.pyc +0 -0
- package/scripts/__pycache__/evidence_semantics.cpython-312.pyc +0 -0
- package/scripts/__pycache__/fetch_benchmark.cpython-312.pyc +0 -0
- package/scripts/__pycache__/lint_report_layout.cpython-312.pyc +0 -0
- package/scripts/__pycache__/orchestrator.cpython-312.pyc +0 -0
- package/scripts/__pycache__/pre_verdict_gate.cpython-312.pyc +0 -0
- package/scripts/__pycache__/recompute_demo_quality.cpython-312.pyc +0 -0
- package/scripts/__pycache__/render_report.cpython-312.pyc +0 -0
- package/scripts/__pycache__/render_report_html.cpython-312.pyc +0 -0
- package/scripts/__pycache__/run_workspace.cpython-312.pyc +0 -0
- package/scripts/__pycache__/skill_lint.cpython-312.pyc +0 -0
- package/scripts/__pycache__/startup_probe.cpython-312.pyc +0 -0
- package/scripts/__pycache__/sync_killer_demo_report.cpython-312.pyc +0 -0
- package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.0.2.pyc +0 -0
- package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.1.1.pyc +0 -0
- package/scripts/__pycache__/validate_schema.cpython-312.pyc +0 -0
- package/scripts/audit_dois.py +292 -0
- package/scripts/bake_pack.sh +37 -0
- package/scripts/benchmark.py +183 -0
- package/scripts/benchmark_evaluator.py +371 -0
- package/scripts/benchmark_judge.py +535 -0
- package/scripts/benchmark_routing.py +120 -0
- package/scripts/benchmark_v2.py +304 -0
- package/scripts/benchmark_v3.py +552 -0
- package/scripts/build_esl_artifacts.py +1921 -0
- package/scripts/build_evidence_library.py +307 -0
- package/scripts/build_killer_demo.py +295 -0
- package/scripts/build_result.py +311 -0
- package/scripts/check_version_consistency.py +96 -0
- package/scripts/citation_check.py +123 -0
- package/scripts/claim_audit.py +157 -0
- package/scripts/complexity_gate.py +180 -0
- package/scripts/compute_confidence.py +176 -0
- package/scripts/dashboard_server.py +536 -0
- package/scripts/did_regression.py +315 -0
- package/scripts/effect_calculator.py +99 -0
- package/scripts/enrich_projects_human_and_lieflat.py +315 -0
- package/scripts/evidence_matrix.py +129 -0
- package/scripts/evidence_score.py +234 -0
- package/scripts/evidence_semantics.py +87 -0
- package/scripts/fetch_benchmark.py +177 -0
- package/scripts/generate_metrics.py +99 -0
- package/scripts/generate_new_projects.py +686 -0
- package/scripts/generate_promo.py +192 -0
- package/scripts/lint_report_layout.py +182 -0
- package/scripts/orchestrator.py +1456 -0
- package/scripts/pre_verdict_gate.py +513 -0
- package/scripts/quickstart.py +121 -0
- package/scripts/rebake_all_5themes.py +88 -0
- package/scripts/recompute_demo_quality.py +205 -0
- package/scripts/render_report.py +270 -0
- package/scripts/render_report_html.py +356 -0
- package/scripts/retraction_watch.py +110 -0
- package/scripts/run_workspace.py +337 -0
- package/scripts/serve_web.py +54 -0
- package/scripts/skill_lint.py +150 -0
- package/scripts/startup_probe.py +265 -0
- package/scripts/sync_killer_demo_report.py +270 -0
- package/scripts/test_adversarial_empirical.py +541 -0
- package/scripts/validate_schema.py +256 -0
- package/skill/agents/education-planner.md +80 -0
- package/skill/agents/evaluation-designer.md +74 -0
- package/skill/agents/evidence-analyst.md +106 -0
- package/skill/agents/evidence-judge.md +111 -0
- package/skill/agents/evidence-retriever.md +80 -0
- package/skill/agents/intervention-designer.md +82 -0
- package/skill/agents/method-reviewer.md +104 -0
- package/skill/agents/skeptic.md +89 -0
- package/skill/sub-skills/aihot-trend-analysis/SKILL.md +31 -0
- package/skill/sub-skills/contradiction-analysis/SKILL.md +17 -0
- package/skill/sub-skills/data-analysis/SKILL.md +23 -0
- package/skill/sub-skills/ethics-review/SKILL.md +25 -0
- package/skill/sub-skills/evidence-extraction/SKILL.md +19 -0
- package/skill/sub-skills/evidence-review/SKILL.md +18 -0
- package/skill/sub-skills/gap-analysis/SKILL.md +25 -0
- package/skill/sub-skills/literature-review/SKILL.md +21 -0
- package/skill/sub-skills/methodology-audit/SKILL.md +20 -0
- package/skill/sub-skills/report-generation/SKILL.md +51 -0
- package/skill/sub-skills/research-planning/SKILL.md +21 -0
- package/skill/sub-skills/study-design/SKILL.md +16 -0
- package/skill/task-briefs/adjudicate.md +17 -0
- package/skill/task-briefs/audit.md +15 -0
- package/skill/task-briefs/challenge.md +15 -0
- package/skill/task-briefs/evaluate.md +13 -0
- package/skill/task-briefs/extract.md +16 -0
- package/skill/task-briefs/frame.md +17 -0
- package/skill/task-briefs/intervene.md +14 -0
- package/skill/task-briefs/present.md +16 -0
- package/skill/task-briefs/retrieve.md +15 -0
- package/visualization/eduevidence-report/assets/base.css +337 -0
- package/visualization/eduevidence-report/motion/motion.css +157 -0
- package/visualization/eduevidence-report/motion/motion.js +107 -0
- package/visualization/eduevidence-report/references/bilingual-style.md +7 -0
- package/visualization/eduevidence-report/references/component-catalog.md +145 -0
- package/visualization/eduevidence-report/references/evidence-expansion.md +65 -0
- package/visualization/eduevidence-report/references/full-report-outline.md +86 -0
- package/visualization/eduevidence-report/references/layout-constraints.md +63 -0
- package/visualization/eduevidence-report/references/lieflat-composition.md +79 -0
- package/visualization/eduevidence-report/references/motion-system.md +31 -0
- package/visualization/eduevidence-report/schemas/adapter-envelope.schema.json +22 -0
- package/visualization/eduevidence-report/schemas/visual-layout.schema.json +87 -0
- package/visualization/eduevidence-report/scripts/__pycache__/adapter_contract.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_artifact_manifest.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_charts.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_figures.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_infographics.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/build_report.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/charts_data.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/lieflat_engine.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/__pycache__/zh_labels.cpython-312.pyc +0 -0
- package/visualization/eduevidence-report/scripts/adapter_contract.py +72 -0
- package/visualization/eduevidence-report/scripts/build_artifact_manifest.py +70 -0
- package/visualization/eduevidence-report/scripts/build_charts.py +283 -0
- package/visualization/eduevidence-report/scripts/build_figures.py +515 -0
- package/visualization/eduevidence-report/scripts/build_infographics.py +268 -0
- package/visualization/eduevidence-report/scripts/build_report.py +3211 -0
- package/visualization/eduevidence-report/scripts/charts_data.py +617 -0
- package/visualization/eduevidence-report/scripts/check_mobile_layout.js +220 -0
- package/visualization/eduevidence-report/scripts/lieflat_engine.py +852 -0
- package/visualization/eduevidence-report/scripts/zh_labels.py +245 -0
- package/visualization/eduevidence-report/themes/academic.css +94 -0
- package/visualization/eduevidence-report/themes/claude.css +96 -0
- package/visualization/eduevidence-report/themes/datalab-dark.css +147 -0
- package/visualization/eduevidence-report/themes/datalab.css +151 -0
- package/visualization/eduevidence-report/themes/presentation.css +140 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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())
|