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,311 @@
1
+ #!/usr/bin/env python3
2
+ """build_result.py — Assemble result.json from an example research pack.
3
+
4
+ result.json is the validated research-core output (总体实施计划 §27) consumed by
5
+ the visualization layer. It aggregates the pack files:
6
+
7
+ frame.json + evidence.jsonl + methodology.json + verdict.json
8
+ + intervention.json + evaluation.json (+ optional sources.jsonl)
9
+
10
+ and computes the outcome-level aggregation and claim rows (Claim-Evidence
11
+ Contract). The report layer consumes ONLY this file and never edits it.
12
+
13
+ Usage:
14
+ python3 scripts/build_result.py --pack examples/ai-coding-assistant \
15
+ --out examples/ai-coding-assistant/result.json
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import json
21
+ import sys
22
+ from datetime import datetime, timezone
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ # Repo-root bootstrap so `python3 scripts/build_result.py` works without venv
27
+ # install (removed by the src-layout migration, plan item E1).
28
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
29
+
30
+ from evidence_semantics import effect_direction
31
+ from engine.versions import ENGINE_VERSION
32
+
33
+ OUTCOME_ORDER = [
34
+ "knowledge_gain", "concept_understanding", "retention", "transfer",
35
+ "independent_problem_solving", "completion_time", "accuracy",
36
+ "code_quality", "assignment_score", "engagement", "motivation",
37
+ "cognitive_load", "help_seeking", "metacognition", "ai_dependency",
38
+ "over_reliance", "reduced_effort", "reduced_transfer",
39
+ "academic_integrity_risk", "false_confidence",
40
+ ]
41
+
42
+
43
+ def _load_json(path: Path) -> dict[str, Any] | None:
44
+ if path.exists():
45
+ return json.loads(path.read_text(encoding="utf-8"))
46
+ return None
47
+
48
+
49
+ def _load_jsonl(path: Path) -> list[dict[str, Any]]:
50
+ if not path.exists():
51
+ return []
52
+ return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
53
+
54
+
55
+ def _derive_source_from_evidence(ev: dict[str, Any]) -> dict[str, Any] | None:
56
+ """Honest source fallback (review P1-2): a real DOI in the location
57
+ yields tier1; otherwise the lowest authority is assumed and the record
58
+ is flagged source_metadata_incomplete. Never fabricates a canonical URL."""
59
+ from retrieval.source import parse_doi_from_url # noqa: PLC0415
60
+
61
+ loc = (ev.get("source_location") or "").strip()
62
+ if not loc:
63
+ return None
64
+ doi = parse_doi_from_url(loc)
65
+ authority = "tier1_paper_doi" if doi else "tier5_general_web"
66
+ return {
67
+ "source_id": ev.get("source_id", ""),
68
+ "title": ev.get("title", ""),
69
+ "year": ev.get("year"),
70
+ "canonical_url": loc,
71
+ "authority_level": authority,
72
+ "source_location": loc,
73
+ "extensions": {"source_metadata_incomplete": not doi},
74
+ }
75
+
76
+
77
+ NOT_CAPTURED_USAGE: dict[str, Any] = {
78
+ "measurement_status": "NOT_CAPTURED",
79
+ "input_tokens": None,
80
+ "output_tokens": None,
81
+ "cost_usd": None,
82
+ "latency_s": None,
83
+ }
84
+
85
+
86
+ def derive_provenance(sources: list[dict[str, Any]]) -> dict[str, Any]:
87
+ """Aggregate provenance from real Source.fetch records (P1-3).
88
+
89
+ fetched_at only appears when an actual fetch recorded it (sources[].fetch.
90
+ fetched_at); the result-assembly time is reported as meta.generated_at and
91
+ must never be passed off as a fetch time. Unknown values stay unknown.
92
+ """
93
+ fetched = [
94
+ s["fetch"]["fetched_at"] for s in sources
95
+ if isinstance(s.get("fetch"), dict) and s["fetch"].get("fetched_at")
96
+ ]
97
+ providers = sorted({
98
+ s["fetch"]["fetch_provider"] for s in sources
99
+ if isinstance(s.get("fetch"), dict) and s["fetch"].get("fetch_provider")
100
+ })
101
+ provenance: dict[str, Any] = {"search_provider": ", ".join(providers) if providers else "n/a"}
102
+ if fetched:
103
+ provenance["fetched_at"] = min(fetched)
104
+ return provenance
105
+
106
+
107
+ def aggregate_outcomes(evidence: list[dict[str, Any]]) -> list[dict[str, Any]]:
108
+ """Outcome-level aggregation keyed by effect_direction (C-1).
109
+
110
+ Outcome rows carry positive_count / negative_count / null_count (based on
111
+ effect_direction) — not support/contradict counts, which are claim-level
112
+ semantics and only belong in the claim trace (relation_to_claim).
113
+ """
114
+ by_outcome: dict[str, dict[str, Any]] = {}
115
+ for ev in evidence:
116
+ outcome = ev.get("outcome_type", "unknown")
117
+ row = by_outcome.setdefault(outcome, {
118
+ "outcome_type": outcome, "positive_count": 0,
119
+ "negative_count": 0, "null_count": 0, "evidence_ids": [],
120
+ })
121
+ effect = effect_direction(ev)
122
+ if effect == "positive":
123
+ row["positive_count"] += 1
124
+ elif effect == "negative":
125
+ row["negative_count"] += 1
126
+ else:
127
+ row["null_count"] += 1
128
+ row["evidence_ids"].append(ev.get("evidence_id", ""))
129
+ return [by_outcome[o] for o in OUTCOME_ORDER if o in by_outcome]
130
+
131
+
132
+ def build_outcome_mapping(evidence: list[dict[str, Any]],
133
+ frame: dict[str, Any] | None = None) -> dict[str, Any]:
134
+ """Frame-declared vs evidence-covered outcome map (OPEN-2).
135
+
136
+ Every outcome type seen in the frame or in evidence gets exactly one
137
+ entry with directional counts (support / contradict / neutral -- never
138
+ mixed, consistent with the three-column Evidence Matrix) and an explicit
139
+ status:
140
+
141
+ supported / contested / contradicted / null_evidence_only / no_evidence
142
+
143
+ Frame-declared outcomes with no covering evidence are surfaced in
144
+ declared_without_evidence so a decision must disclose its gaps.
145
+ """
146
+ declared: set[str] = set()
147
+ for group in ("primary", "secondary", "risk"):
148
+ declared.update((frame or {}).get("outcomes", {}).get(group, []) or [])
149
+ declared.discard("")
150
+
151
+ by_outcome: dict[str, dict[str, Any]] = {}
152
+ for ev in evidence:
153
+ outcome = ev.get("outcome_type") or "unknown"
154
+ row = by_outcome.setdefault(outcome, {
155
+ "outcome_type": outcome,
156
+ "support_count": 0, "contradict_count": 0, "neutral_count": 0,
157
+ "evidence_ids": [],
158
+ })
159
+ direction = ev.get("direction", "neutral")
160
+ if direction == "support":
161
+ row["support_count"] += 1
162
+ elif direction == "contradict":
163
+ row["contradict_count"] += 1
164
+ else:
165
+ row["neutral_count"] += 1
166
+ if ev.get("evidence_id"):
167
+ row["evidence_ids"].append(ev["evidence_id"])
168
+
169
+ entries: list[dict[str, Any]] = []
170
+ for outcome in sorted(set(declared) | set(by_outcome)):
171
+ row = by_outcome.get(outcome) or {
172
+ "outcome_type": outcome, "support_count": 0,
173
+ "contradict_count": 0, "neutral_count": 0, "evidence_ids": []}
174
+ s, c, n = row["support_count"], row["contradict_count"], row["neutral_count"]
175
+ if s > 0 and c > 0:
176
+ status = "contested"
177
+ elif s > 0:
178
+ status = "supported"
179
+ elif c > 0:
180
+ status = "contradicted"
181
+ elif n > 0:
182
+ status = "null_evidence_only"
183
+ else:
184
+ status = "no_evidence"
185
+ entries.append({
186
+ "outcome_type": outcome,
187
+ "declared_in_frame": outcome in declared,
188
+ "status": status,
189
+ "support_count": s,
190
+ "contradict_count": c,
191
+ "neutral_count": n,
192
+ "evidence_ids": list(row["evidence_ids"]),
193
+ })
194
+
195
+ declared_without_evidence = sorted(d for d in declared if d not in by_outcome)
196
+ return {"entries": entries, "declared_without_evidence": declared_without_evidence}
197
+
198
+ def build_claims(evidence: list[dict[str, Any]]) -> list[dict[str, Any]]:
199
+ """Claim-level rows bound to evidence (Claim-Evidence Contract, A-3).
200
+
201
+ claim_id comes from the evidence objects themselves (first-class field of
202
+ the Evidence Contract), so Claim -> Evidence -> Source never depends on
203
+ render-time renumbering. Legacy evidence without claim_id falls back to a
204
+ deterministic C-{idx:03d} sequence.
205
+ """
206
+ claims: dict[str, dict[str, Any]] = {}
207
+ for idx, ev in enumerate(evidence, 1):
208
+ claim = ev.get("claim", "")
209
+ outcome = ev.get("outcome_type", "")
210
+ key = (claim, outcome)
211
+ row = claims.setdefault(key, {
212
+ "claim": claim, "outcome_type": outcome,
213
+ "claim_id": ev.get("claim_id") or f"C-{idx:03d}",
214
+ "evidence_ids": [], "status": "SUPPORTED",
215
+ })
216
+ row["evidence_ids"].append(ev.get("evidence_id", ""))
217
+ status = ev.get("status")
218
+ rank = {"CONTRADICT": 3, "UNSUPPORTED": 2, "DOWNGRADE_CONFIDENCE": 1}.get(status, 0)
219
+ if rank > {"CONTRADICT": 3, "UNSUPPORTED": 2, "DOWNGRADE_CONFIDENCE": 1}.get(row["status"], 0):
220
+ row["status"] = status
221
+ return list(claims.values())
222
+
223
+
224
+ def build_result(pack_dir: Path, *, mode: str = "platform_native") -> dict[str, Any]:
225
+ frame = _load_json(pack_dir / "frame.json") or {}
226
+ evidence = _load_jsonl(pack_dir / "evidence.jsonl")
227
+ # methodology.json is a single MethodologyAudit object (or a JSONL list)
228
+ methodology_single = _load_json(pack_dir / "methodology.json")
229
+ methodology = [methodology_single] if methodology_single else _load_jsonl(pack_dir / "methodology.jsonl")
230
+ verdict = _load_json(pack_dir / "verdict.json") or {}
231
+ intervention = _load_json(pack_dir / "intervention.json") or {}
232
+ evaluation = _load_json(pack_dir / "evaluation.json") or {}
233
+ sources = _load_jsonl(pack_dir / "sources.jsonl")
234
+
235
+ if not sources:
236
+ seen: dict[str, dict[str, Any]] = {}
237
+ for ev in evidence:
238
+ sid = ev.get("source_id", "")
239
+ if sid and sid not in seen:
240
+ derived = _derive_source_from_evidence(ev)
241
+ if derived is not None:
242
+ seen[sid] = derived
243
+ sources = list(seen.values())
244
+
245
+ decision = verdict
246
+ # execution summary derived from pack (complexity via frame target/depth is optional)
247
+ execution = {
248
+ "complexity": frame.get("complexity", "M"),
249
+ "mode": mode,
250
+ "agents": [],
251
+ "usage": dict(NOT_CAPTURED_USAGE),
252
+ }
253
+
254
+ result = {
255
+ "meta": {
256
+ "skill": "eduevidence",
257
+ "version": ENGINE_VERSION,
258
+ "generated_at": datetime.now(timezone.utc).isoformat(),
259
+ "mode": mode,
260
+ "question": frame.get("question", ""),
261
+ },
262
+ "execution": execution,
263
+ "research_frame": frame,
264
+ "decision": decision,
265
+ "outcomes": aggregate_outcomes(evidence),
266
+ "outcome_mapping": build_outcome_mapping(evidence, frame),
267
+ "claims": build_claims(evidence),
268
+ "sources": sources,
269
+ "evidence": evidence,
270
+ "methodology_reviews": methodology,
271
+ "conflicts": [{"reason_for_disagreement": verdict.get("reason_for_disagreement", "")}]
272
+ if verdict.get("reason_for_disagreement") else [],
273
+ "applicability": verdict.get("applicability", {}),
274
+ "intervention": intervention,
275
+ "evaluation": evaluation,
276
+ "benchmark": {},
277
+ "provenance": derive_provenance(sources),
278
+ }
279
+ return result
280
+
281
+
282
+ def main() -> int:
283
+ parser = argparse.ArgumentParser(description="Assemble result.json from an example pack")
284
+ parser.add_argument("--pack", required=True, help="example pack directory")
285
+ parser.add_argument("--out", required=True, help="output result.json path")
286
+ parser.add_argument("--mode", choices=["platform_native", "agent_mcp_enhanced"],
287
+ default="platform_native")
288
+ parser.add_argument("--data-origin", choices=["real", "synthetic", "hybrid", "manual_curated"],
289
+ default=None,
290
+ help="provenance of the pack data (plan R4); rendered as a badge in reports")
291
+ args = parser.parse_args()
292
+
293
+ result = build_result(Path(args.pack), mode=args.mode)
294
+ if args.data_origin:
295
+ result["meta"]["data_origin"] = args.data_origin
296
+ # keep the bilingual projection in lockstep with the English source
297
+ out = Path(args.out)
298
+ zh_path = out.parent / "result.zh.json"
299
+ if zh_path.exists():
300
+ zh_result = json.loads(zh_path.read_text(encoding="utf-8"))
301
+ zh_result.setdefault("meta", {})["data_origin"] = args.data_origin
302
+ zh_path.write_text(json.dumps(zh_result, ensure_ascii=False, indent=2), encoding="utf-8")
303
+ out = Path(args.out)
304
+ out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
305
+ print(f"wrote {args.out} (sources={len(result['sources'])}, evidence={len(result['evidence'])}, "
306
+ f"claims={len(result['claims'])}, outcomes={len(result['outcomes'])})")
307
+ return 0
308
+
309
+
310
+ if __name__ == "__main__":
311
+ sys.exit(main())
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env python3
2
+ """Version consistency gate: engine/versions.py is the single version authority.
3
+
4
+ Fails (exit 1) when the declared versions drift apart:
5
+
6
+ - engine/versions.py ENGINE_VERSION <- authority
7
+ - pyproject.toml [project] version
8
+ - package.json version
9
+ - CHANGELOG.md first `## [x.y.z]` heading
10
+ - SKILL.md `# EduEvidence X.Y` title (major.minor only)
11
+
12
+ Stdlib only; run in CI before tests.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import re
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ REPO_ROOT = Path(__file__).resolve().parent.parent
22
+
23
+ VERSION_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)$")
24
+ VERSIONS_PY_RE = re.compile(r'^ENGINE_VERSION\s*=\s*"(\d+\.\d+\.\d+)"', re.M)
25
+ PYPROJECT_RE = re.compile(r'^version\s*=\s*"(\d+\.\d+\.\d+)"', re.M)
26
+ PACKAGE_JSON_RE = re.compile(r'"version"\s*:\s*"(\d+\.\d+\.\d+)"')
27
+ CHANGELOG_RE = re.compile(r"^##\s*\[(\d+\.\d+\.\d+)\]", re.M)
28
+ SKILL_TITLE_RE = re.compile(r"^#\s*EduEvidence\s+(\d+\.\d+)", re.M)
29
+
30
+
31
+ def _read(rel: str) -> str:
32
+ path = REPO_ROOT / rel
33
+ if not path.exists():
34
+ return ""
35
+ return path.read_text(encoding="utf-8")
36
+
37
+
38
+ def main() -> int:
39
+ errors: list[str] = []
40
+
41
+ m = VERSIONS_PY_RE.search(_read("engine/versions.py"))
42
+ if not m:
43
+ print("FAIL: cannot parse ENGINE_VERSION from engine/versions.py")
44
+ return 1
45
+ authoritative = m.group(1)
46
+ if not VERSION_RE.match(authoritative):
47
+ errors.append(f"engine/versions.py ENGINE_VERSION not semver: {authoritative!r}")
48
+
49
+ def check_full(label: str, found: str) -> None:
50
+ if found != authoritative:
51
+ errors.append(f"{label} = {found} != engine/versions.py {authoritative}")
52
+
53
+ def check_minor(label: str, found: str) -> None:
54
+ prefix = authoritative.rsplit(".", 1)[0]
55
+ if found != prefix:
56
+ errors.append(f"{label} = {found} != {prefix} (from {authoritative})")
57
+
58
+ # -- pyproject.toml ------------------------------------------------------
59
+ pp = PYPROJECT_RE.search(_read("pyproject.toml"))
60
+ if pp:
61
+ check_full("pyproject.toml", pp.group(1))
62
+ else:
63
+ errors.append("cannot parse version from pyproject.toml")
64
+
65
+ # -- package.json --------------------------------------------------------
66
+ pj = PACKAGE_JSON_RE.search(_read("package.json"))
67
+ if pj:
68
+ check_full("package.json", pj.group(1))
69
+ else:
70
+ errors.append("cannot parse version from package.json")
71
+
72
+ # -- CHANGELOG.md ---------------------------------------------------------
73
+ cl = CHANGELOG_RE.search(_read("CHANGELOG.md"))
74
+ if cl:
75
+ check_full("CHANGELOG.md head entry", cl.group(1))
76
+ else:
77
+ errors.append("cannot parse leading '## [x.y.z]' from CHANGELOG.md")
78
+
79
+ # -- SKILL.md (major.minor only) ------------------------------------------
80
+ sk = SKILL_TITLE_RE.search(_read("SKILL.md"))
81
+ if sk:
82
+ check_minor("SKILL.md title", sk.group(1))
83
+ else:
84
+ errors.append("cannot parse '# EduEvidence X.Y' title from SKILL.md")
85
+
86
+ if errors:
87
+ for e in errors:
88
+ print(f"FAIL: {e}")
89
+ print(f"Version authority: engine/versions.py = {authoritative}")
90
+ return 1
91
+ print(f"version consistency OK ({authoritative})")
92
+ return 0
93
+
94
+
95
+ if __name__ == "__main__":
96
+ sys.exit(main())
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env python3
2
+ """CLI: verify every DOI in a pack's sources/evidence against registries (E6).
3
+
4
+ Usage:
5
+ python3 scripts/citation_check.py --pack examples/ai-coding-assistant-evidence
6
+ python3 scripts/citation_check.py --sources examples/X/sources.jsonl --out /tmp/cite.json
7
+
8
+ Writes citation_check.json (+ .md) into the pack dir by default and exits 1
9
+ when any not_found/mismatch/retracted is found unless --no-fail.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import sys
17
+ import time
18
+ from pathlib import Path
19
+
20
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
21
+
22
+ from engine.citation_check import RegistryClient, extract_dois # noqa: E402
23
+
24
+
25
+ def load_records(path: Path) -> list[dict]:
26
+ text = path.read_text(encoding="utf-8")
27
+ if path.suffix == ".jsonl":
28
+ return [json.loads(line) for line in text.splitlines() if line.strip()]
29
+ return [json.loads(text)]
30
+
31
+
32
+ def main() -> int:
33
+ parser = argparse.ArgumentParser(description=__doc__)
34
+ parser.add_argument("--pack", help="example pack directory")
35
+ parser.add_argument("--sources", help="single sources.jsonl / evidence.jsonl")
36
+ parser.add_argument("--out", help="output JSON path (default <pack>/citation_check.json)")
37
+ parser.add_argument("--no-fail", action="store_true")
38
+ parser.add_argument("--write-back", action="store_true",
39
+ help="inject doi_verified/retracted into the pack's sources.jsonl")
40
+ args = parser.parse_args()
41
+
42
+ targets: list[Path] = []
43
+ out_path: Path | None = None
44
+ if args.pack:
45
+ pack = Path(args.pack)
46
+ for name in ("sources.jsonl", "evidence.jsonl"):
47
+ f = pack / name
48
+ if f.exists():
49
+ targets.append(f)
50
+ out_path = pack / "citation_check.json"
51
+ elif args.sources:
52
+ targets.append(Path(args.sources))
53
+ out_path = Path(args.out) if args.out else Path("citation_check.json")
54
+ else:
55
+ parser.error("pass --pack or --sources")
56
+ if not targets:
57
+ print("no sources/evidence files found; nothing to check")
58
+ return 0
59
+
60
+ records: list[tuple[str, dict]] = []
61
+ for t in targets:
62
+ records.extend((t.name, rec) for rec in load_records(t))
63
+
64
+ client = RegistryClient()
65
+ seen: set[str] = set()
66
+ rows = []
67
+ for fname, rec in records:
68
+ for doi, title in extract_dois(rec):
69
+ if doi in seen:
70
+ continue
71
+ seen.add(doi)
72
+ verdict = client.check(doi, title)
73
+ rows.append({"file": fname, **verdict})
74
+ print(f"{verdict['status']:9s} retracted={verdict['retracted']!s:5s} {doi}")
75
+
76
+ bad = [r for r in rows if r["status"] != "ok" or r["retracted"]]
77
+ report = {
78
+ "generated_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
79
+ "checked": len(rows),
80
+ "bad": len(bad),
81
+ "results": rows,
82
+ }
83
+ assert out_path is not None
84
+ out_path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
85
+ md = out_path.with_suffix(".md")
86
+ lines = ["# Citation Check", "",
87
+ f"生成时间:{report['generated_at']};核验 DOI:{len(rows)};异常:{len(bad)}", ""]
88
+ if bad:
89
+ lines += ["| DOI | 状态 | 撤稿 | 注册表标题 |", "|---|---|---|---|"]
90
+ for r in bad:
91
+ t = (r.get("registry_title") or "")[:70].replace("|", "\\|")
92
+ lines.append(f"| `{r['doi']}` | {r['status']} | {'⚠️ 是' if r['retracted'] else '否'} | {t} |")
93
+ md.write_text("\n".join(lines) + "\n", encoding="utf-8")
94
+
95
+ print(f"\ncitation check -> {out_path} (bad={len(bad)})")
96
+
97
+ if args.write_back and args.pack:
98
+ by_doi = {r["doi"]: r for r in rows}
99
+ src_file = Path(args.pack) / "sources.jsonl"
100
+ if src_file.exists():
101
+ lines = []
102
+ changed = 0
103
+ for line in src_file.read_text(encoding="utf-8").splitlines():
104
+ if not line.strip():
105
+ continue
106
+ rec = json.loads(line)
107
+ for doi, v in by_doi.items():
108
+ rec_doi = (rec.get("doi") or "").strip()
109
+ if rec_doi == doi or doi in json.dumps(rec):
110
+ new_flags = {"doi_verified": v["doi_verified"], "retracted": v["retracted"]}
111
+ if any(rec.get(k) != val for k, val in new_flags.items()):
112
+ rec.update(new_flags)
113
+ changed += 1
114
+ break
115
+ lines.append(json.dumps(rec, ensure_ascii=False))
116
+ src_file.write_text("\n".join(lines) + "\n", encoding="utf-8")
117
+ print(f"write-back: {changed} source rows updated in {src_file}")
118
+
119
+ return (1 if bad else 0) if not args.no_fail else 0
120
+
121
+
122
+ if __name__ == "__main__":
123
+ sys.exit(main())
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env python3
2
+ """claim_audit.py — Citation Audit (plan section 11).
3
+
4
+ Runs before final report generation. For each claim-evidence pair:
5
+
6
+ Claim exists? -> Evidence exists? -> Source exists? -> Source supports claim?
7
+ -> Outcome matches? -> Scope exceeds source?
8
+
9
+ Failures are marked:
10
+ UNSUPPORTED evidence cannot be bound to a verifiable source
11
+ CONTRADICT bound evidence contradicts the claim (relation_to_claim)
12
+ DOWNGRADE_CONFIDENCE claim overstates the source (scope/outcome mismatch)
13
+
14
+ Citation relation is judged ONLY by relation_to_claim (via
15
+ evidence_semantics.claim_relation) — never by effect_direction or the legacy
16
+ direction field: a negative effect can support a negative claim.
17
+
18
+ Usage:
19
+ python scripts/claim_audit.py --claims claims.jsonl --evidence evidence.jsonl
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import json
25
+ import sys
26
+ from pathlib import Path
27
+
28
+ from evidence_semantics import claim_relation
29
+
30
+ SUPPORTED_OUTCOMES = {
31
+ "knowledge_gain", "concept_understanding", "retention", "transfer",
32
+ "independent_problem_solving", "completion_time", "accuracy",
33
+ "code_quality", "assignment_score", "engagement", "motivation",
34
+ "cognitive_load", "help_seeking", "metacognition", "ai_dependency",
35
+ "over_reliance", "reduced_effort", "reduced_transfer",
36
+ "academic_integrity_risk", "false_confidence",
37
+ }
38
+
39
+
40
+ def load_records(path: Path) -> list[dict]:
41
+ records = []
42
+ for line in path.read_text(encoding="utf-8").splitlines():
43
+ line = line.strip()
44
+ if line:
45
+ records.append(json.loads(line))
46
+ return records
47
+
48
+
49
+ def audit_claim(claim: dict, evidence_by_id: dict[str, dict]) -> dict:
50
+ """Audit a single claim. Returns a status record."""
51
+ result = {
52
+ "claim": claim.get("claim", ""),
53
+ "status": "SUPPORTED",
54
+ "issues": [],
55
+ }
56
+
57
+ # 1. Claim exists?
58
+ if not claim.get("claim"):
59
+ result["status"] = "UNSUPPORTED"
60
+ result["issues"].append("claim is empty")
61
+ return result
62
+
63
+ # 2. Evidence exists?
64
+ evidence_ids = claim.get("evidence_ids", [])
65
+ if not evidence_ids:
66
+ result["status"] = "UNSUPPORTED"
67
+ result["issues"].append("no evidence_ids bound to claim")
68
+ return result
69
+
70
+ bound = []
71
+ for eid in evidence_ids:
72
+ ev = evidence_by_id.get(eid)
73
+ if ev is None:
74
+ result["issues"].append(f"evidence {eid} not found")
75
+ continue
76
+ bound.append(ev)
77
+
78
+ if not bound:
79
+ result["status"] = "UNSUPPORTED"
80
+ result["issues"].append("no bound evidence found in corpus")
81
+ return result
82
+
83
+ # 3. Source exists (source_id + source_location)
84
+ for ev in bound:
85
+ if not ev.get("source_id"):
86
+ result["issues"].append(f"{ev.get('evidence_id')}: missing source_id")
87
+ if not ev.get("source_location"):
88
+ result["issues"].append(f"{ev.get('evidence_id')}: missing source_location")
89
+
90
+ # 4. Source supports claim (judged ONLY by relation_to_claim; a negative
91
+ # effect can support a negative claim — never use effect_direction or
92
+ # the legacy direction field as a citation relation)
93
+ for ev in bound:
94
+ if claim_relation(ev) == "contradict":
95
+ result["issues"].append(
96
+ f"{ev.get('evidence_id')}: evidence contradicts claim (relation_to_claim=contradict)")
97
+
98
+ # 5. Outcome matches
99
+ for ev in bound:
100
+ outcome = ev.get("outcome_type", "")
101
+ if outcome not in SUPPORTED_OUTCOMES:
102
+ result["issues"].append(
103
+ f"{ev.get('evidence_id')}: unknown outcome_type {outcome!r}")
104
+ claimed_outcome = claim.get("outcome_type")
105
+ if claimed_outcome and outcome and claimed_outcome != outcome:
106
+ result["issues"].append(
107
+ f"{ev.get('evidence_id')}: outcome mismatch (claim={claimed_outcome}, evidence={outcome})")
108
+
109
+ # 6. Scope exceeds source?
110
+ claim_scope = claim.get("scope")
111
+ if claim_scope:
112
+ for ev in bound:
113
+ ev_scope = ev.get("applicability", {}).get("scope", "")
114
+ if ev_scope and claim_scope not in ev_scope:
115
+ result["issues"].append(
116
+ f"{ev.get('evidence_id')}: claim scope {claim_scope!r} exceeds source scope {ev_scope!r}")
117
+
118
+ if result["issues"]:
119
+ if any("contradicts claim" in i for i in result["issues"]):
120
+ # Contradiction is the dominant signal: the claim is contradicted
121
+ # by its own evidence, distinct from being unverifiable.
122
+ result["status"] = "CONTRADICT"
123
+ else:
124
+ severe = any("missing source" in i or "not found" in i
125
+ for i in result["issues"])
126
+ result["status"] = "UNSUPPORTED" if severe else "DOWNGRADE_CONFIDENCE"
127
+ return result
128
+
129
+
130
+ def audit_claims(claims: list[dict], evidence_list: list[dict]) -> list[dict]:
131
+ evidence_by_id = {ev.get("evidence_id"): ev for ev in evidence_list}
132
+ return [audit_claim(c, evidence_by_id) for c in claims]
133
+
134
+
135
+ def main() -> int:
136
+ parser = argparse.ArgumentParser(description="Run the Citation Audit over claims and evidence")
137
+ parser.add_argument("--claims", required=True, help="claims.jsonl (each line: claim, evidence_ids, outcome_type, scope)")
138
+ parser.add_argument("--evidence", required=True, help="evidence.jsonl")
139
+ args = parser.parse_args()
140
+
141
+ claims = load_records(Path(args.claims))
142
+ evidence = load_records(Path(args.evidence))
143
+ results = audit_claims(claims, evidence)
144
+
145
+ summary = {"SUPPORTED": 0, "UNSUPPORTED": 0, "DOWNGRADE_CONFIDENCE": 0, "CONTRADICT": 0}
146
+ for r in results:
147
+ summary[r["status"]] += 1
148
+ if r["issues"]:
149
+ print(f"[{r['status']}] {r['claim']}")
150
+ for issue in r["issues"]:
151
+ print(f" - {issue}")
152
+ print(json.dumps(summary, ensure_ascii=False))
153
+ return 0 if summary["UNSUPPORTED"] == 0 else 1
154
+
155
+
156
+ if __name__ == "__main__":
157
+ sys.exit(main())