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,187 @@
1
+ """Claim synthesis with independent-study semantics.
2
+
3
+ Status rule (frozen baseline in code + tests): a usable Study requires
4
+ valid/accepted_partial Source provenance, resolvable Study identity, latest
5
+ MethodologyAudit != `fail`, and valid active Finding/Link. Multiple Findings
6
+ from one Study never create multiple votes.
7
+
8
+ support-only usable Studies → supported
9
+ contradiction-only → refuted
10
+ both independent directions → contested
11
+ no decisive usable Study / neutral-only → insufficient
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass
17
+
18
+ from engine.contracts import validate_record
19
+ from engine.graph_store import GraphStore
20
+ from engine.semantics import claim_relation, decision_implication
21
+
22
+ VALID_SOURCE_STATUSES = ("valid", "accepted_partial")
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class ClaimSynthesis:
27
+ claim_id: str
28
+ status: str
29
+ study_ids: tuple[str, ...]
30
+ independent_sample_keys: tuple[str, ...]
31
+ supporting_link_ids: tuple[str, ...]
32
+ contradicting_link_ids: tuple[str, ...]
33
+ neutral_link_ids: tuple[str, ...]
34
+ methodology_summary: dict
35
+ directness_summary: dict
36
+ unresolved_conflicts: tuple[str, ...]
37
+ missing_evidence: tuple[str, ...]
38
+
39
+
40
+ def _usable_studies(store: GraphStore) -> dict[str, dict]:
41
+ """Study id → Study record for usable studies.
42
+
43
+ Usable = resolvable identity + at least one valid/accepted_partial source.
44
+ """
45
+ sources = {s["source_id"]: s for s in store.read_table("sources")}
46
+ usable: dict[str, dict] = {}
47
+ for s in store.read_table("studies"):
48
+ if s.get("identity_status") == "unresolved":
49
+ continue
50
+ if not any(sid in sources and sources[sid]["validation_status"] in VALID_SOURCE_STATUSES
51
+ for sid in s.get("source_ids", [])):
52
+ continue
53
+ usable[s["study_id"]] = s
54
+ return usable
55
+
56
+
57
+ def _latest_audits(store: GraphStore) -> dict[str, dict]:
58
+ """Study id → latest MethodologyAudit (by audited_at)."""
59
+ latest: dict[str, dict] = {}
60
+ for a in store.read_table("audits"):
61
+ cur = latest.get(a["study_id"])
62
+ if cur is None or a["audited_at"] >= cur["audited_at"]:
63
+ latest[a["study_id"]] = a
64
+ return latest
65
+
66
+
67
+ def synthesize_claim(store: GraphStore, claim_id: str) -> ClaimSynthesis:
68
+ usable_studies = _usable_studies(store)
69
+ audits = _latest_audits(store)
70
+ findings = {f["finding_id"]: f for f in store.read_table("findings")}
71
+ links = store.read_table("evidence_links")
72
+ claims = {c["claim_id"]: c for c in store.read_table("claims")}
73
+ if claim_id not in claims:
74
+ raise KeyError(f"claim {claim_id} not in graph")
75
+
76
+ claim = claims[claim_id]
77
+ claim_links = [l for l in links if l["claim_id"] == claim_id]
78
+
79
+ # group links per usable study
80
+ study_links: dict[str, list[dict]] = {}
81
+ for link in claim_links:
82
+ fnd = findings.get(link["finding_id"])
83
+ if fnd is None:
84
+ continue
85
+ sid = fnd["study_id"]
86
+ study = usable_studies.get(sid)
87
+ if study is None:
88
+ continue
89
+ audit = audits.get(sid)
90
+ if audit is not None and audit.get("overall_status") == "fail":
91
+ continue
92
+ study_links.setdefault(sid, []).append(link)
93
+
94
+ # per-study decision relation (independent votes)
95
+ from collections import Counter
96
+ support_votes: list[str] = []
97
+ oppose_votes: list[str] = []
98
+ neutral_only: list[str] = []
99
+ independent_samples: set[str] = set()
100
+
101
+ study_ids: list[str] = []
102
+ supporting_link_ids: list[str] = []
103
+ contradicting_link_ids: list[str] = []
104
+ neutral_link_ids: list[str] = []
105
+ unresolved: list[str] = []
106
+
107
+ seen_keys: set[str] = set()
108
+ for sid, links_for_study in sorted(study_links.items()):
109
+ study = usable_studies[sid]
110
+ independent_samples.add(study["independence_key"])
111
+ relations = [claim_relation(l) for l in links_for_study]
112
+ implications = [decision_implication(l) for l in links_for_study]
113
+ has_support = any(r == "support" for r in relations)
114
+ has_contradict = any(r == "contradict" for r in relations)
115
+ has_conditional = any(i == "conditional" for i in implications)
116
+ if has_conditional:
117
+ unresolved.append(f"{sid}: conditional implication")
118
+ # independent-study votes: two Studies sharing an independence_key
119
+ # are one independent unit — only the first votes
120
+ key = study["independence_key"]
121
+ is_first_with_key = key not in seen_keys
122
+ seen_keys.add(key)
123
+ if has_support and not has_contradict and is_first_with_key:
124
+ support_votes.append(sid)
125
+ elif has_contradict and not has_support and is_first_with_key:
126
+ oppose_votes.append(sid)
127
+ elif has_support and has_contradict:
128
+ # within-study conflict is a real unresolved conflict, not a vote
129
+ unresolved.append(f"{sid}: within-study support+contradict links")
130
+ elif not is_first_with_key:
131
+ unresolved.append(
132
+ f"{sid}: shares independence_key {key!r} with another study; "
133
+ f"not counted as an independent vote")
134
+ else:
135
+ neutral_only.append(sid)
136
+ study_ids.append(sid)
137
+ for l in links_for_study:
138
+ rel = claim_relation(l)
139
+ if rel == "support":
140
+ supporting_link_ids.append(l["evidence_link_id"])
141
+ elif rel == "contradict":
142
+ contradicting_link_ids.append(l["evidence_link_id"])
143
+ else:
144
+ neutral_link_ids.append(l["evidence_link_id"])
145
+
146
+ if support_votes and not oppose_votes:
147
+ status = "supported"
148
+ elif oppose_votes and not support_votes:
149
+ status = "refuted"
150
+ elif support_votes and oppose_votes:
151
+ status = "contested"
152
+ else:
153
+ status = "insufficient"
154
+
155
+ # methodology + directness summaries
156
+ meth_summary: dict = {"studies": len(study_ids), "audit_fail": 0, "concern": 0, "pass": 0}
157
+ directness_values: list[int] = []
158
+ for sid in study_ids:
159
+ a = audits.get(sid)
160
+ if a is None:
161
+ continue
162
+ status_ = a.get("overall_status")
163
+ meth_summary[status_ if status_ in meth_summary else "concern"] += 1
164
+ for l in study_links.get(sid, []):
165
+ directness_values.append(int(l.get("directness", 0)))
166
+
167
+ return ClaimSynthesis(
168
+ claim_id=claim_id,
169
+ status=status,
170
+ study_ids=tuple(study_ids),
171
+ independent_sample_keys=tuple(sorted(independent_samples)),
172
+ supporting_link_ids=tuple(supporting_link_ids),
173
+ contradicting_link_ids=tuple(contradicting_link_ids),
174
+ neutral_link_ids=tuple(neutral_link_ids),
175
+ methodology_summary=meth_summary,
176
+ directness_summary={"mean": (sum(directness_values) / len(directness_values)
177
+ if directness_values else 0.0),
178
+ "count": len(directness_values)},
179
+ unresolved_conflicts=tuple(unresolved),
180
+ missing_evidence=(),
181
+ )
182
+
183
+
184
+ def synthesize_project(store: GraphStore) -> tuple[ClaimSynthesis, ...]:
185
+ claims = store.read_table("claims")
186
+ return tuple(synthesize_claim(store, c["claim_id"]) for c in sorted(
187
+ claims, key=lambda c: c["claim_id"]))
@@ -0,0 +1,408 @@
1
+ """V2 Evidence Tribunal — revision-bound DecisionSnapshot generation.
2
+
3
+ Confidence policy (migration baseline, CONFIDENCE_POLICY_VERSION
4
+ 2026-08-12.v3):
5
+
6
+ quality_term = mean per usable independent Study of
7
+ (design+sample+measurement+temporal) / 8
8
+ study relation = collapse active relevant links in one Study:
9
+ support-only → support_adoption; oppose-only →
10
+ oppose_adoption; both/conditional → conditional;
11
+ neutral-only → neutral
12
+ consistency_term = majority proportion over decisive Study relations
13
+ directness_term = mean across Studies of (mean link directness / 2)
14
+ count_term = min(1.0, usable independent studies / 4)
15
+ conflict_penalty = 0.15 only when independent support_adoption AND
16
+ oppose_adoption Studies both exist
17
+ uncertainty_penalty = min(0.20, 0.05 * critical_uncertainty_units)
18
+
19
+ score = 0.30*q + 0.25*c + 0.20*d + 0.25*n - conflict - uncertainty
20
+
21
+ High >= .72 | Moderate >= .45 | Low >= .20 | else Insufficient
22
+
23
+ Directness is NOT double-counted (it is excluded from the quality term's
24
+ methodology scope by construction here: quality reads only the four audit
25
+ dimensions). The score is an auditable internal index, never a probability.
26
+
27
+ Decision action is gate-enforced:
28
+ Low/Insufficient cannot yield ADOPT
29
+ REJECT requires usable direct negative/opposition evidence
30
+ unresolved but promising evidence may yield PILOT
31
+ otherwise INSUFFICIENT_EVIDENCE
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import json
37
+ from datetime import datetime, timezone
38
+ from pathlib import Path
39
+
40
+ from engine.contracts import validate_record
41
+ from engine.graph_store import GraphStore
42
+ from engine.ids import new_local_id
43
+ from engine.semantics import claim_relation, decision_implication
44
+ from engine.synthesis import ClaimSynthesis, synthesize_project
45
+ from engine.versions import (
46
+ CONFIDENCE_POLICY_VERSION,
47
+ METHODOLOGY_POLICY_VERSION,
48
+ SOURCE_VALIDATION_POLICY_VERSION,
49
+ )
50
+
51
+ THRESHOLD_HIGH = 0.72
52
+ THRESHOLD_MODERATE = 0.45
53
+ THRESHOLD_LOW = 0.20
54
+ CONFLICT_PENALTY = 0.15
55
+ UNCERTAINTY_PER_UNIT = 0.05
56
+ UNCERTAINTY_CAP = 0.20
57
+
58
+
59
+ def _now_iso() -> str:
60
+ return datetime.now(timezone.utc).isoformat()
61
+
62
+
63
+ def _label(score: float) -> str:
64
+ if score >= THRESHOLD_HIGH:
65
+ return "High"
66
+ if score >= THRESHOLD_MODERATE:
67
+ return "Moderate"
68
+ if score >= THRESHOLD_LOW:
69
+ return "Low"
70
+ return "Insufficient"
71
+
72
+
73
+ def _study_relation(store: GraphStore, claim_syn: ClaimSynthesis,
74
+ study_id: str, findings: dict, links: list[dict]) -> str:
75
+ """Collapse all active relevant links of one Study into one relation."""
76
+ implications = []
77
+ for link in links:
78
+ if link["claim_id"] != claim_syn.claim_id:
79
+ continue
80
+ fnd = findings.get(link["finding_id"])
81
+ if fnd is None or fnd["study_id"] != study_id:
82
+ continue
83
+ implications.append(decision_implication(link))
84
+ if not implications:
85
+ return "neutral"
86
+ support = any(i == "support_adoption" for i in implications)
87
+ oppose = any(i == "oppose_adoption" for i in implications)
88
+ conditional = any(i == "conditional" for i in implications)
89
+ if support and oppose:
90
+ return "conditional"
91
+ if conditional:
92
+ return "conditional"
93
+ if support:
94
+ return "support_adoption"
95
+ if oppose:
96
+ return "oppose_adoption"
97
+ return "neutral"
98
+
99
+
100
+ def _usable_studies(store: GraphStore) -> dict[str, dict]:
101
+ sources = {s["source_id"]: s for s in store.read_table("sources")}
102
+ usable: dict[str, dict] = {}
103
+ for s in store.read_table("studies"):
104
+ if s.get("identity_status") == "unresolved":
105
+ continue
106
+ if not any(sid in sources and sources[sid]["validation_status"] in
107
+ ("valid", "accepted_partial") for sid in s.get("source_ids", [])):
108
+ continue
109
+ usable[s["study_id"]] = s
110
+ return usable
111
+
112
+
113
+ def _latest_audits(store: GraphStore) -> dict[str, dict]:
114
+ latest: dict[str, dict] = {}
115
+ for a in store.read_table("audits"):
116
+ cur = latest.get(a["study_id"])
117
+ if cur is None or a["audited_at"] >= cur["audited_at"]:
118
+ latest[a["study_id"]] = a
119
+ return latest
120
+
121
+
122
+ def _study_implication(implications: list[str]) -> str:
123
+ """Collapse all active links of one Study into one decision relation.
124
+
125
+ support-only → support_adoption; oppose-only → oppose_adoption;
126
+ any conditional or both directions → conditional; neutral-only → neutral.
127
+ This is Study-level folding: every Study is folded exactly once across
128
+ ALL its links, so a multi-claim Study can never vote twice or be
129
+ overwritten by a later Claim.
130
+ """
131
+ if not implications:
132
+ return "neutral"
133
+ support = any(i == "support_adoption" for i in implications)
134
+ oppose = any(i == "oppose_adoption" for i in implications)
135
+ conditional = any(i == "conditional" for i in implications)
136
+ if conditional or (support and oppose):
137
+ return "conditional"
138
+ if support:
139
+ return "support_adoption"
140
+ if oppose:
141
+ return "oppose_adoption"
142
+ return "neutral"
143
+
144
+
145
+ def _confidence(store: GraphStore, syntheses: tuple[ClaimSynthesis, ...]) -> dict:
146
+ """Deterministic confidence over usable independent studies.
147
+
148
+ Each usable Study (valid source provenance, resolved identity, latest
149
+ audit != fail) is folded exactly once across ALL its links, independent
150
+ of Claim: support_adoption / oppose_adoption are decisive votes,
151
+ conditional counts as a critical-uncertainty unit, neutral is ignored.
152
+
153
+ quality_term = mean over usable studies of
154
+ (design+sample+measurement+temporal) / 8
155
+ consistency_term = majority proportion over decisive relations
156
+ directness_term = mean across studies of (mean link directness / 2)
157
+ count_term = min(1.0, usable_independent_studies / 4)
158
+ conflict_penalty = 0.15 only when independent support_adoption AND
159
+ oppose_adoption studies both exist
160
+ uncertainty = min(0.20, 0.05 * critical_uncertainty_units)
161
+
162
+ Directness is not double-counted (quality reads only the four audit
163
+ dimensions). The score is an auditable internal index, never a
164
+ probability.
165
+ """
166
+ usable = _usable_studies(store)
167
+ audits = _latest_audits(store)
168
+ findings = {f["finding_id"]: f for f in store.read_table("findings")}
169
+ links = store.read_table("evidence_links")
170
+
171
+ # group links per study (one fold per study across all claims)
172
+ study_link_map: dict[str, list[dict]] = {}
173
+ for link in links:
174
+ fnd = findings.get(link["finding_id"])
175
+ if fnd is not None:
176
+ study_link_map.setdefault(fnd["study_id"], []).append(link)
177
+
178
+ decisive: dict[str, str] = {} # study_id -> support_adoption|oppose_adoption
179
+ usable_studies: list[str] = []
180
+ quality_sum = 0.0
181
+ directness_sum = 0.0
182
+ directness_count = 0
183
+ critical_uncertainty_units = 0
184
+
185
+ for sid, study in sorted(usable.items()):
186
+ audit = audits.get(sid)
187
+ if audit is None or audit.get("overall_status") == "fail":
188
+ continue
189
+ usable_studies.append(sid)
190
+ impls = [decision_implication(l) for l in study_link_map.get(sid, [])]
191
+ relation = _study_implication(impls)
192
+ if relation in ("support_adoption", "oppose_adoption"):
193
+ decisive[sid] = relation
194
+ elif relation == "conditional":
195
+ critical_uncertainty_units += 1
196
+ quality_sum += (
197
+ audit.get("design_quality", 0) + audit.get("sample_quality", 0)
198
+ + audit.get("measurement_validity", 0)
199
+ + audit.get("temporal_strength", 0)) / 8.0
200
+ dirs = [int(l.get("directness", 0)) for l in study_link_map.get(sid, [])]
201
+ if dirs:
202
+ directness_sum += sum(dirs) / len(dirs) / 2.0
203
+ directness_count += 1
204
+
205
+ n_decisive = len(decisive)
206
+ n_usable = len(usable_studies)
207
+ if n_decisive == 0:
208
+ return {"score": 0.0, "label": "Insufficient",
209
+ "decisive_studies": 0, "usable_studies": n_usable,
210
+ "decisive_relations": {}}
211
+
212
+ quality_term = quality_sum / n_usable if n_usable else 0.0
213
+ counts: dict[str, int] = {}
214
+ for r in decisive.values():
215
+ counts[r] = counts.get(r, 0) + 1
216
+ majority = max(counts.values())
217
+ consistency_term = majority / n_decisive
218
+ directness_term = (directness_sum / directness_count) if directness_count else 0.0
219
+ count_term = min(1.0, n_usable / 4.0)
220
+ conflict = CONFLICT_PENALTY if (counts.get("support_adoption", 0) > 0
221
+ and counts.get("oppose_adoption", 0) > 0) else 0.0
222
+ uncertainty = min(UNCERTAINTY_CAP, UNCERTAINTY_PER_UNIT * critical_uncertainty_units)
223
+ score = (0.30 * quality_term + 0.25 * consistency_term
224
+ + 0.20 * directness_term + 0.25 * count_term
225
+ - conflict - uncertainty)
226
+ score = max(0.0, min(1.0, score))
227
+ return {"score": round(score, 4), "label": _label(score),
228
+ "decisive_studies": n_decisive, "usable_studies": n_usable,
229
+ "decisive_relations": dict(decisive)}
230
+
231
+
232
+ def _has_direct_learning_evidence(store: GraphStore,
233
+ decisive_relations: dict[str, str]) -> bool:
234
+ """True when at least one decisive support_adoption Study measures a
235
+ learning outcome directly.
236
+
237
+ Learning evidence means the finding's outcome is declared
238
+ outcome_type == "learning" AND its evidence link carries directness == 2.
239
+ Task performance / process / risk outcomes never qualify, and a missing
240
+ outcome record or missing directness fails closed (False).
241
+ """
242
+ findings = {f["finding_id"]: f for f in store.read_table("findings")}
243
+ outcomes = {o["outcome_id"]: o for o in store.read_table("outcomes")}
244
+ links_by_finding: dict[str, list[dict]] = {}
245
+ for link in store.read_table("evidence_links"):
246
+ links_by_finding.setdefault(link["finding_id"], []).append(link)
247
+
248
+ support_studies = {sid for sid, rel in decisive_relations.items()
249
+ if rel == "support_adoption"}
250
+ if not support_studies:
251
+ return False
252
+ for fid, fnd in findings.items():
253
+ if fnd.get("study_id") not in support_studies:
254
+ continue
255
+ outcome = outcomes.get(fnd.get("outcome_id"))
256
+ if outcome is None or outcome.get("outcome_type") != "learning":
257
+ continue
258
+ for link in links_by_finding.get(fid, []):
259
+ directness = link.get("directness")
260
+ if isinstance(directness, (int, float)) and int(directness) == 2:
261
+ return True
262
+ return False
263
+
264
+
265
+ def _decision_action(syn_statuses: dict[str, str], confidence: dict,
266
+ decisive_relations: dict[str, str],
267
+ has_direct_learning_evidence: bool = False) -> str:
268
+ """Gate-enforced decision action.
269
+
270
+ REJECT requires usable direct opposition evidence (an independent Study
271
+ folded to oppose_adoption). Low/Insufficient can never yield ADOPT.
272
+ ADOPT additionally requires direct learning/transfer evidence: High +
273
+ decisive support WITHOUT a direct learning outcome downgrades to PILOT
274
+ (task performance / procedural efficiency is not learning). Moderate +
275
+ decisive support → PILOT; otherwise INSUFFICIENT_EVIDENCE.
276
+ """
277
+ label = confidence["label"]
278
+ has_oppose = any(r == "oppose_adoption" for r in decisive_relations.values())
279
+ has_support = any(r == "support_adoption" for r in decisive_relations.values())
280
+ if has_oppose:
281
+ return "REJECT"
282
+ if label == "High" and has_support and has_direct_learning_evidence:
283
+ return "ADOPT"
284
+ if label in ("High", "Moderate") and has_support:
285
+ return "PILOT"
286
+ return "INSUFFICIENT_EVIDENCE"
287
+
288
+
289
+
290
+
291
+
292
+ def adjudicate(store: GraphStore, *, project: ProjectWorkspace,
293
+ claim_syntheses: tuple[ClaimSynthesis, ...] | None = None,
294
+ applicability: dict | None = None,
295
+ policy_versions: dict[str, str] | None = None) -> dict:
296
+ """Generate a DecisionSnapshot dict (not yet persisted)."""
297
+ syntheses = claim_syntheses if claim_syntheses is not None else synthesize_project(store)
298
+ confidence = _confidence(store, syntheses)
299
+ applicability = applicability or {"boundary": "evidence scope", "notes": ""}
300
+
301
+ syn_statuses = {s.claim_id: s.status for s in syntheses}
302
+ decisive_relations = confidence.get("decisive_relations", {})
303
+ direct_learning = _has_direct_learning_evidence(store, decisive_relations)
304
+
305
+ decision = _decision_action(syn_statuses, confidence, decisive_relations,
306
+ has_direct_learning_evidence=direct_learning)
307
+
308
+ key_links: list[str] = []
309
+ for syn in syntheses:
310
+ key_links.extend(syn.supporting_link_ids)
311
+ key_links.extend(syn.contradicting_link_ids)
312
+
313
+ risks: list[str] = []
314
+ for syn in syntheses:
315
+ risks.extend(syn.unresolved_conflicts)
316
+ if not risks and decision == "ADOPT":
317
+ risks.append("long-term retention/transfer may still be untested")
318
+
319
+ missing: list[str] = []
320
+ for syn in syntheses:
321
+ missing.extend(syn.missing_evidence)
322
+ if not missing:
323
+ missing.append("no explicit knowledge-gap analysis in this snapshot")
324
+
325
+ claim_assessments = {}
326
+ for syn in syntheses:
327
+ claim_assessments[syn.claim_id] = {
328
+ "status": syn.status,
329
+ "independent_studies": len(syn.study_ids),
330
+ "independent_samples": len(syn.independent_sample_keys),
331
+ "supporting_links": list(syn.supporting_link_ids),
332
+ "contradicting_links": list(syn.contradicting_link_ids),
333
+ }
334
+
335
+ snapshot = {
336
+ "decision_snapshot_id": new_local_id("DEC", set()),
337
+ "decision": decision,
338
+ "confidence_label": confidence["label"],
339
+ "confidence_score_internal": confidence["score"],
340
+ "claim_assessments": claim_assessments,
341
+ "key_evidence_links": sorted(set(key_links)),
342
+ "key_risks": sorted(set(risks)),
343
+ "applicability_boundary": applicability.get("boundary", ""),
344
+ "missing_evidence": sorted(set(missing)),
345
+ "graph_revision": store.active_revision(),
346
+ "policy_versions": policy_versions or {
347
+ "confidence": CONFIDENCE_POLICY_VERSION,
348
+ "methodology": METHODOLOGY_POLICY_VERSION,
349
+ "source_validation": SOURCE_VALIDATION_POLICY_VERSION,
350
+ },
351
+ "created_at": _now_iso(),
352
+ "extensions": {"confidence_components": {
353
+ "decisive_studies": confidence.get("decisive_studies", 0),
354
+ "usable_studies": confidence.get("usable_studies", 0),
355
+ "has_direct_learning_evidence": direct_learning,
356
+ }},
357
+ }
358
+ errors = validate_record("decision-snapshot", snapshot)
359
+ if errors:
360
+ raise ValueError(f"invalid decision snapshot: {errors}")
361
+ return snapshot
362
+
363
+
364
+ def save_decision_snapshot(project: ProjectWorkspace, snapshot: dict) -> Path:
365
+ """Persist a snapshot under decisions/ (immutable; never rewritten)."""
366
+ path = project.path / "decisions" / f"{snapshot['decision_snapshot_id']}.json"
367
+ if path.exists():
368
+ raise FileExistsError(f"decision snapshot already exists: {path}")
369
+ path.write_text(json.dumps(snapshot, ensure_ascii=False, indent=2) + "\n",
370
+ encoding="utf-8")
371
+ return path
372
+
373
+
374
+ def decision_diff(previous: dict | None, current: dict, *,
375
+ previous_gaps: tuple[dict, ...] = (),
376
+ current_gaps: tuple[dict, ...] = ()) -> dict:
377
+ """Machine-readable diff between two revision-bound DecisionSnapshots.
378
+
379
+ `resolved_gaps` / `new_gaps` are computed from the GAP inputs, never
380
+ claimed by the report layer. The diff never attributes graph change to
381
+ the report.
382
+ """
383
+ prev_links = set(previous.get("key_evidence_links", [])) if previous else set()
384
+ cur_links = set(current.get("key_evidence_links", []))
385
+ prev_claims = dict(previous.get("claim_assessments", {})) if previous else {}
386
+ cur_claims = dict(current.get("claim_assessments", {}))
387
+
388
+ prev_gap_ids = {g["gap_id"] for g in previous_gaps}
389
+ cur_gap_ids = {g["gap_id"] for g in current_gaps}
390
+
391
+ changed_claims = []
392
+ if previous:
393
+ for cid in sorted(prev_claims | cur_claims):
394
+ if prev_claims.get(cid) != cur_claims.get(cid):
395
+ changed_claims.append(cid)
396
+
397
+ return {
398
+ "from_decision_snapshot_id": (previous or {}).get("decision_snapshot_id"),
399
+ "to_decision_snapshot_id": current.get("decision_snapshot_id"),
400
+ "from_graph_revision": (previous or {}).get("graph_revision"),
401
+ "to_graph_revision": current.get("graph_revision"),
402
+ "action_changed": (previous or {}).get("decision") != current.get("decision"),
403
+ "confidence_changed": (previous or {}).get("confidence_label") != current.get("confidence_label"),
404
+ "changed_claims": changed_claims,
405
+ "new_key_evidence_links": sorted(cur_links - prev_links),
406
+ "resolved_gaps": sorted(prev_gap_ids - cur_gap_ids),
407
+ "new_gaps": sorted(cur_gap_ids - prev_gap_ids),
408
+ }