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,308 @@
1
+ """AnalysisPlan/AnalysisRun + Native Core descriptive analysis.
2
+
3
+ Native Core (stdlib only — no scipy/pandas/statsmodels) supports
4
+ deterministic: row/column/type profile, missingness, group counts,
5
+ mean/median/min/max, explicit pre/post descriptive difference, explicit
6
+ between-group descriptive difference. Advanced analysis (regression,
7
+ multilevel, meta-analysis, thematic analysis) is capability-discovered; an
8
+ unavailable capability returns `ANALYSIS_CAPABILITY_UNAVAILABLE` and never
9
+ fabricates p-values. Advanced results enter only through
10
+ `record_external_analysis()` with explicit provider/software provenance.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import csv
16
+ import json
17
+ import statistics
18
+ from dataclasses import dataclass
19
+ from datetime import datetime, timezone
20
+ from pathlib import Path
21
+
22
+ from engine.contracts import validate_record
23
+ from engine.datasets import analysis_blocked_by_privacy
24
+ from engine.ids import new_local_id
25
+ from engine.project import ProjectWorkspace
26
+
27
+
28
+ def _now_iso() -> str:
29
+ return datetime.now(timezone.utc).isoformat()
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class AnalysisCapabilityResult:
34
+ status: str
35
+ capability_id: str
36
+ output: dict
37
+ warnings: tuple[str, ...]
38
+
39
+
40
+ NATIVE_CAPABILITIES = frozenset({
41
+ "row_column_type_profile", "missingness", "group_counts",
42
+ "descriptive_statistics", "pre_post_descriptive_difference",
43
+ "between_group_descriptive_difference",
44
+ })
45
+
46
+ ADVANCED_CAPABILITIES = frozenset({
47
+ "regression", "multilevel_analysis", "meta_analysis",
48
+ "qualitative_thematic_analysis", "structural_equation_modeling",
49
+ })
50
+
51
+
52
+ def _load_rows(path: Path) -> tuple[list[str], list[list[str]]]:
53
+ with path.open("r", encoding="utf-8-sig", newline="") as f:
54
+ reader = csv.reader(f)
55
+ rows = list(reader)
56
+ if not rows:
57
+ return [], []
58
+ return rows[0], rows[1:]
59
+
60
+
61
+ def _num(value: str) -> float | None:
62
+ try:
63
+ return float(value)
64
+ except (TypeError, ValueError):
65
+ return None
66
+
67
+
68
+ def validate_analysis_plan(project: ProjectWorkspace, plan: dict) -> list[str]:
69
+ errors = validate_record("analysis-plan", plan)
70
+ return errors
71
+
72
+
73
+ def run_native_descriptive(project: ProjectWorkspace, plan: dict) -> dict:
74
+ """Run deterministic Native Core descriptive analyses for the plan.
75
+
76
+ `plan` carries dataset_ids + primary_analysis; analysis_requirements
77
+ from the design select which native capabilities run. Returns an
78
+ AnalysisRun record (status completed or capability_unavailable).
79
+ """
80
+ # privacy gate: block on required-but-undone deidentification
81
+ from engine.datasets import ingest_dataset # noqa: F401 (registry use)
82
+ manifests = {}
83
+ for dsid in plan.get("dataset_ids", []):
84
+ manifest = project.path / "datasets" / "raw" / dsid / "manifest.json"
85
+ if not manifest.is_file():
86
+ return {
87
+ "analysis_run_id": new_local_id("ANL", set()),
88
+ "analysis_plan_id": plan["analysis_plan_id"],
89
+ "dataset_ids": plan.get("dataset_ids", []),
90
+ "status": "failed",
91
+ "outputs": {"error": f"dataset {dsid} not found in project"},
92
+ "assumption_checks": [],
93
+ "created_at": _now_iso(),
94
+ "extensions": {},
95
+ }
96
+ asset = json.loads(manifest.read_text(encoding="utf-8"))
97
+ reasons = analysis_blocked_by_privacy(asset)
98
+ if reasons:
99
+ return {
100
+ "analysis_run_id": new_local_id("ANL", set()),
101
+ "analysis_plan_id": plan["analysis_plan_id"],
102
+ "dataset_ids": plan.get("dataset_ids", []),
103
+ "status": "failed",
104
+ "outputs": {"privacy_block": reasons},
105
+ "assumption_checks": [],
106
+ "created_at": _now_iso(),
107
+ "extensions": {},
108
+ }
109
+ manifests[dsid] = asset
110
+
111
+ outputs: dict = {}
112
+ warnings: list[str] = []
113
+ for dsid, asset in manifests.items():
114
+ path = Path(asset["path"])
115
+ cols, rows = _load_rows(path)
116
+
117
+ # type profile
118
+ types: dict[str, str] = {}
119
+ numeric_cols: dict[str, list[float]] = {c: [] for c in cols}
120
+ for row in rows:
121
+ for idx, c in enumerate(cols):
122
+ if idx < len(row):
123
+ v = row[idx].strip()
124
+ if v == "":
125
+ continue
126
+ if _num(v) is not None:
127
+ types.setdefault(c, "number")
128
+ numeric_cols[c].append(_num(v))
129
+ else:
130
+ types[c] = "string"
131
+ missing = {
132
+ c: sum(1 for row in rows if len(row) <= idx or row[idx].strip() == "")
133
+ for idx, c in enumerate(cols)
134
+ }
135
+
136
+ # group counts (all columns with <= 10 distinct values)
137
+ group_counts: dict[str, dict] = {}
138
+ for idx, c in enumerate(cols):
139
+ counts: dict[str, int] = {}
140
+ for row in rows:
141
+ if idx < len(row) and row[idx].strip():
142
+ counts[row[idx].strip()] = counts.get(row[idx].strip(), 0) + 1
143
+ if 0 < len(counts) <= 10:
144
+ group_counts[c] = counts
145
+
146
+ # descriptive statistics: ONLY for columns whose type is fully
147
+ # 'number'; a mixed column (some non-numeric cells) is marked
148
+ # explicitly instead of silently summarizing the numeric subset
149
+ desc: dict[str, dict] = {}
150
+ for c, vals in numeric_cols.items():
151
+ if types.get(c) == "number" and vals:
152
+ desc[c] = {
153
+ "count": len(vals),
154
+ "mean": round(statistics.fmean(vals), 4),
155
+ "median": statistics.median(vals),
156
+ "min": min(vals),
157
+ "max": max(vals),
158
+ }
159
+
160
+ outputs[dsid] = {
161
+ "columns": cols,
162
+ "row_count": len(rows),
163
+ "types": types,
164
+ "missingness": missing,
165
+ "group_counts": group_counts,
166
+ "descriptive_statistics": desc,
167
+ }
168
+
169
+ # explicit pre/post descriptive difference (same dataset, mapped columns)
170
+ pre_post = (plan.get("extensions") or {}).get("pre_post_mapping")
171
+ if pre_post:
172
+ dsid = pre_post.get("dataset_id")
173
+ if dsid in outputs:
174
+ pre_col = pre_post["pre_column"]
175
+ post_col = pre_post["post_column"]
176
+ pre_vals = outputs[dsid]["descriptive_statistics"].get(pre_col, {})
177
+ post_vals = outputs[dsid]["descriptive_statistics"].get(post_col, {})
178
+ if pre_vals and post_vals:
179
+ outputs[dsid]["pre_post_descriptive_difference"] = {
180
+ "pre_mean": pre_vals["mean"],
181
+ "post_mean": post_vals["mean"],
182
+ "mean_difference": round(post_vals["mean"] - pre_vals["mean"], 4),
183
+ "note": "descriptive only; no p-value inferred",
184
+ }
185
+
186
+ # explicit between-group descriptive difference
187
+ between = (plan.get("extensions") or {}).get("between_group_mapping")
188
+ if between:
189
+ dsid = between.get("dataset_id")
190
+ if dsid in outputs:
191
+ group_col = between["group_column"]
192
+ value_col = between["value_column"]
193
+ path = Path(manifests[dsid]["path"])
194
+ cols2, rows2 = _load_rows(path)
195
+ if group_col in cols2 and value_col in cols2:
196
+ gi = cols2.index(group_col)
197
+ vi = cols2.index(value_col)
198
+ groups: dict[str, list[float]] = {}
199
+ for row in rows2:
200
+ if gi < len(row) and vi < len(row):
201
+ v = _num(row[vi])
202
+ g = row[gi].strip()
203
+ if v is not None and g:
204
+ groups.setdefault(g, []).append(v)
205
+ means = {g: round(statistics.fmean(vs), 4) for g, vs in groups.items() if vs}
206
+ outputs[dsid]["between_group_descriptive_difference"] = {
207
+ "group_means": means,
208
+ "note": "descriptive only; no p-value inferred",
209
+ }
210
+
211
+ if warnings:
212
+ outputs["warnings"] = list(warnings)
213
+
214
+ run = {
215
+ "analysis_run_id": new_local_id("ANL", set()),
216
+ "analysis_plan_id": plan["analysis_plan_id"],
217
+ "dataset_ids": plan.get("dataset_ids", []),
218
+ "status": "completed",
219
+ "outputs": outputs,
220
+ "assumption_checks": [],
221
+ "created_at": _now_iso(),
222
+ "extensions": {"engine": "native_core", "capabilities": sorted(NATIVE_CAPABILITIES)},
223
+ }
224
+ errors = validate_record("analysis-run", run)
225
+ if errors:
226
+ raise ValueError(f"invalid analysis run: {errors}")
227
+ return run
228
+
229
+
230
+ def record_external_analysis(project: ProjectWorkspace, *, plan: dict,
231
+ provider: str, software: dict,
232
+ outputs: dict, assumption_checks: list[dict],
233
+ status: str) -> dict:
234
+ """Record an analysis produced by a discovered external capability."""
235
+ run = {
236
+ "analysis_run_id": new_local_id("ANL", set()),
237
+ "analysis_plan_id": plan["analysis_plan_id"],
238
+ "dataset_ids": plan.get("dataset_ids", []),
239
+ "status": "completed" if status == "completed" else "failed",
240
+ "outputs": outputs,
241
+ "assumption_checks": assumption_checks,
242
+ "created_at": _now_iso(),
243
+ "extensions": {
244
+ "provider": provider,
245
+ "software": software,
246
+ "external": True,
247
+ },
248
+ }
249
+ errors = validate_record("analysis-run", run)
250
+ if errors:
251
+ raise ValueError(f"invalid external analysis run: {errors}")
252
+ return run
253
+
254
+
255
+ def save_analysis_run(project: ProjectWorkspace, run: dict) -> Path:
256
+ """Persist an AnalysisRun under analyses/ (atomic; never rewritten)."""
257
+ errors = validate_record("analysis-run", run)
258
+ if errors:
259
+ raise ValueError(f"invalid analysis run: {errors}")
260
+ path = project.path / "analyses" / f"{run['analysis_run_id']}.json"
261
+ if path.exists():
262
+ raise FileExistsError(f"analysis run already exists: {path}")
263
+ path.parent.mkdir(parents=True, exist_ok=True)
264
+ tmp = path.with_suffix(".tmp")
265
+ tmp.write_text(__import__("json").dumps(run, ensure_ascii=False, indent=2) + "\n",
266
+ encoding="utf-8")
267
+ tmp.replace(path)
268
+ return path
269
+
270
+
271
+ def mark_analysis_validated(project: ProjectWorkspace, run_id: str) -> dict:
272
+ """Promote a persisted AnalysisRun to `validated`.
273
+
274
+ `validated` is the ONLY status that closes the Full Research Cycle gate
275
+ in engine/update.py (ANALYSIS_INVALID otherwise). A human or automated
276
+ review gate calls this after checking the run's outputs.
277
+ """
278
+ path = project.path / "analyses" / f"{run_id}.json"
279
+ if not path.is_file():
280
+ raise FileNotFoundError(f"analysis run {run_id} not persisted under {path}")
281
+ run = json.loads(path.read_text(encoding="utf-8"))
282
+ if run.get("status") not in ("completed", "validated"):
283
+ raise ValueError(
284
+ f"cannot validate analysis run {run_id} with status "
285
+ f"{run.get('status')!r}; only completed runs may be validated"
286
+ )
287
+ run["status"] = "validated"
288
+ errors = validate_record("analysis-run", run)
289
+ if errors:
290
+ raise ValueError(f"invalid validated run: {errors}")
291
+ tmp = path.with_suffix(".tmp")
292
+ tmp.write_text(json.dumps(run, ensure_ascii=False, indent=2) + "\n",
293
+ encoding="utf-8")
294
+ tmp.replace(path)
295
+ return run
296
+
297
+
298
+ def capability_unavailable(capability_id: str, plan: dict) -> AnalysisCapabilityResult:
299
+ """Honest degradation for undiscovered advanced capabilities."""
300
+ return AnalysisCapabilityResult(
301
+ status="ANALYSIS_CAPABILITY_UNAVAILABLE",
302
+ capability_id=capability_id,
303
+ output={},
304
+ warnings=(
305
+ f"capability {capability_id!r} not discovered; no p-values or "
306
+ f"effect estimates were fabricated",
307
+ ),
308
+ )
package/engine/bias.py ADDED
@@ -0,0 +1,178 @@
1
+ """engine/bias.py - publication-bias diagnostics for v4 meta-analysis.
2
+
3
+ egger_regression Egger (1997) funnel-asymmetry test: regress the
4
+ standard normal deviate SND = d/se on precision
5
+ prec = 1/se; the intercept estimates small-study bias
6
+ (Borenstein 2009, ch. 41, eq. 41.1-41.3).
7
+ fail_safe_n Rosenthal (1979) fail-safe N: how many null-effect
8
+ studies would push the combined z below significance
9
+ (Borenstein 2009, ch. 42): N_fs = (ΣZ)² / 2.706 − k,
10
+ with 2.706 = 1.645² (one-tailed z at α = 0.05).
11
+
12
+ Pure stdlib (math only). Egger's intercept is tested with a two-tailed
13
+ Student t (df = k − 2); the t CDF uses the regularized incomplete beta
14
+ function (Numerical Recipes betacf) so no scipy is required.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import math
19
+ from typing import Any
20
+
21
+ _Z_CRIT_ONE_TAIL = 1.6448536269514722 # z at α = 0.05, one-tailed
22
+ _Z_CRIT_SQUARED = _Z_CRIT_ONE_TAIL ** 2 # 2.7055434540954042
23
+
24
+
25
+ def _is_number(value: Any) -> bool:
26
+ return isinstance(value, (int, float)) and not isinstance(value, bool)
27
+
28
+
29
+ def _usable(rows: list[dict]) -> list[dict]:
30
+ import math as _m
31
+ return [r for r in rows or []
32
+ if _is_number(r.get("d")) and _is_number(r.get("se")) and r["se"] > 0
33
+ and _m.isfinite(float(r["d"])) and _m.isfinite(float(r["se"]))]
34
+
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Student-t survival via regularized incomplete beta (stdlib only)
38
+ # ---------------------------------------------------------------------------
39
+
40
+ def _betacf(a: float, b: float, x: float, max_iter: int = 200,
41
+ eps: float = 3.0e-12) -> float:
42
+ """Continued-fraction evaluation of the incomplete beta function."""
43
+ tiny = 1.0e-30
44
+ qab, qap, qam = a + b, a + 1.0, a - 1.0
45
+ c = 1.0
46
+ d = 1.0 - qab * x / qap
47
+ if abs(d) < tiny:
48
+ d = tiny
49
+ d = 1.0 / d
50
+ h = d
51
+ for m in range(1, max_iter + 1):
52
+ m2 = 2 * m
53
+ aa = m * (b - m) * x / ((qam + m2) * (a + m2))
54
+ d = 1.0 + aa * d
55
+ if abs(d) < tiny:
56
+ d = tiny
57
+ c = 1.0 + aa / c
58
+ if abs(c) < tiny:
59
+ c = tiny
60
+ d = 1.0 / d
61
+ h *= d * c
62
+ aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2))
63
+ d = 1.0 + aa * d
64
+ if abs(d) < tiny:
65
+ d = tiny
66
+ c = 1.0 + aa / c
67
+ if abs(c) < tiny:
68
+ c = tiny
69
+ d = 1.0 / d
70
+ delta = d * c
71
+ h *= delta
72
+ if abs(delta - 1.0) < eps:
73
+ break
74
+ return h
75
+
76
+
77
+ def _betai(a: float, b: float, x: float) -> float:
78
+ """Regularized incomplete beta I_x(a, b)."""
79
+ if x <= 0.0:
80
+ return 0.0
81
+ if x >= 1.0:
82
+ return 1.0
83
+ bt = math.exp(math.lgamma(a + b) - math.lgamma(a) - math.lgamma(b)
84
+ + a * math.log(x) + b * math.log1p(-x))
85
+ if x < (a + 1.0) / (a + b + 2.0):
86
+ return bt * _betacf(a, b, x) / a
87
+ return 1.0 - bt * _betacf(b, a, 1.0 - x) / b
88
+
89
+
90
+ def _t_two_tailed_p(t: float, df: int) -> float:
91
+ """Two-tailed p for a Student t with ``df`` degrees of freedom.
92
+
93
+ Uses the identity P(T > t) = 0.5 · I_{df/(df+t²)}(df/2, 1/2).
94
+ """
95
+ if df <= 0:
96
+ return 1.0
97
+ if not math.isfinite(t):
98
+ return 0.0
99
+ x = df / (df + t * t)
100
+ return 2.0 * 0.5 * _betai(df / 2.0, 0.5, x)
101
+
102
+
103
+ # ---------------------------------------------------------------------------
104
+ # Egger's regression test
105
+ # ---------------------------------------------------------------------------
106
+
107
+ def egger_regression(rows: list[dict]) -> dict:
108
+ """Egger (1997) funnel-asymmetry test via OLS of SND on precision.
109
+
110
+ Symmetric data (effect independent of precision) yields intercept ≈ 0;
111
+ a nonzero intercept signals small-study bias. Significance: two-tailed
112
+ t-test on the intercept, df = k − 2. With k < 3, or constant precision
113
+ across studies, the test is not applicable. A perfect fit (RSS = 0) is
114
+ treated as no detectable bias (t = 0, p = 1).
115
+ """
116
+ usable = _usable(rows)
117
+ k = len(usable)
118
+ if k < 3:
119
+ return {"applicable": False, "reason": "need >= 3 studies",
120
+ "k": k}
121
+ x = [1.0 / float(r["se"]) for r in usable] # precision
122
+ y = [float(r["d"]) / float(r["se"]) for r in usable] # SND
123
+ x_bar = sum(x) / k
124
+ y_bar = sum(y) / k
125
+ sxx = sum((xi - x_bar) ** 2 for xi in x)
126
+ if sxx == 0:
127
+ return {"applicable": False, "reason": "constant precision",
128
+ "k": k}
129
+ sxy = sum((xi - x_bar) * (yi - y_bar) for xi, yi in zip(x, y))
130
+ slope = sxy / sxx
131
+ intercept = y_bar - slope * x_bar
132
+ residuals = [yi - (intercept + slope * xi) for xi, yi in zip(x, y)]
133
+ rss = sum(rr * rr for rr in residuals)
134
+ mse = rss / (k - 2) if k > 2 else 0.0
135
+ se_intercept = math.sqrt(mse * (1.0 / k + x_bar * x_bar / sxx))
136
+ if se_intercept == 0 or not math.isfinite(se_intercept): # perfect fit
137
+ t, p = 0.0, 1.0
138
+ else:
139
+ t = intercept / se_intercept
140
+ p = _t_two_tailed_p(t, k - 2)
141
+ return {
142
+ "applicable": True,
143
+ "k": k,
144
+ "intercept": intercept,
145
+ "se_intercept": se_intercept,
146
+ "slope": slope,
147
+ "t": t,
148
+ "p_value": p,
149
+ "significant": p < 0.05,
150
+ "bias_detected": p < 0.05,
151
+ }
152
+
153
+
154
+ # ---------------------------------------------------------------------------
155
+ # Rosenthal's fail-safe N
156
+ # ---------------------------------------------------------------------------
157
+
158
+ def fail_safe_n(rows: list[dict]) -> dict:
159
+ """Rosenthal (1979) fail-safe N: null studies needed to nullify the result.
160
+
161
+ N_fs = (ΣZ)² / 2.706 − k with Z = d/se (floored at 0). ``tolerates_5``
162
+ reports whether the finding survives the addition of 5 null studies
163
+ (N_fs ≥ 5) — the task's minimum robustness bar.
164
+ """
165
+ usable = _usable(rows)
166
+ k = len(usable)
167
+ zs = [float(r["d"]) / float(r["se"]) for r in usable]
168
+ sum_z = sum(zs)
169
+ n_fail_safe = max(0.0, (sum_z * sum_z) / _Z_CRIT_SQUARED - k)
170
+ return {
171
+ "method": "rosenthal",
172
+ "k": k,
173
+ "sum_z": sum_z,
174
+ "z_crit": _Z_CRIT_ONE_TAIL,
175
+ "z_crit_squared": _Z_CRIT_SQUARED,
176
+ "n_fail_safe": n_fail_safe,
177
+ "tolerates_5": n_fail_safe >= 5.0,
178
+ }
@@ -0,0 +1,106 @@
1
+ """Schema-bound task briefs.
2
+
3
+ A task brief is generated from the registered schema metadata — required
4
+ fields and enums are read from the schema, never hard-coded twice in this
5
+ module. `build_task_brief()` accepts only `PlanStep(kind="capability")`;
6
+ calling it on a wait step raises a clear ValueError instead of generating a
7
+ fake agent task.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from pathlib import Path
14
+
15
+ from engine.contracts import load_schema, schema_path
16
+ from engine.planner import PlanStep
17
+
18
+
19
+ def _schema_section(schema: dict) -> str:
20
+ lines: list[str] = []
21
+ props = schema.get("properties", {})
22
+ required = set(schema.get("required", []))
23
+ for key in sorted(props):
24
+ prop = props[key]
25
+ line = f"- `{key}`"
26
+ if key in required:
27
+ line += " (REQUIRED)"
28
+ if "enum" in prop:
29
+ line += f" enum={prop['enum']}"
30
+ if "pattern" in prop:
31
+ line += f" pattern={prop['pattern']!r}"
32
+ if "const" in prop:
33
+ line += f" const={prop['const']!r}"
34
+ lines.append(line)
35
+ return "\n".join(lines) if lines else "(no properties)"
36
+
37
+
38
+ def build_task_brief(step: PlanStep, *, project: ProjectWorkspace,
39
+ input_payload: dict, output_path: Path) -> str:
40
+ """Render a capability step into an executable agent brief.
41
+
42
+ `project` is the ProjectWorkspace (for context ids); `input_payload`
43
+ carries the input entity ids/records for this step; `output_path` is
44
+ where the step's output artifact must be written.
45
+ """
46
+ if step.kind != "capability" or step.capability_id is None:
47
+ raise ValueError(
48
+ f"cannot build a task brief for non-capability step "
49
+ f"{step.step_id!r} (kind={step.kind}); wait steps are real "
50
+ f"states, not fake agent tasks"
51
+ )
52
+ cap = step.capability_id
53
+ schema_name = step.output_contract
54
+ if schema_name is None:
55
+ raise ValueError(f"capability {cap} has no output contract")
56
+
57
+ sch = load_schema(schema_name)
58
+ sch_file = schema_path(schema_name)
59
+
60
+ lines = [
61
+ f"# Task brief: {cap}",
62
+ "",
63
+ f"Project: {project.project_id}",
64
+ f"Output schema: {sch_file.relative_to(Path.cwd()) if sch_file.is_relative_to(Path.cwd()) else sch_file}",
65
+ "",
66
+ "## Output contract (schema-derived)",
67
+ _schema_section(sch),
68
+ "",
69
+ "## Required fields",
70
+ ", ".join(sch.get("required", [])) or "(none)",
71
+ "",
72
+ "## Validation",
73
+ f"Validate the output with `engine.contracts.validate_record({schema_name!r}, record)` — "
74
+ f"it must return [] (empty errors).",
75
+ "",
76
+ f"## Output path",
77
+ str(output_path),
78
+ "",
79
+ "## Inputs",
80
+ json.dumps(input_payload, ensure_ascii=False, indent=2),
81
+ ]
82
+
83
+ # capability-specific hard rules
84
+ rules: dict[str, str] = {
85
+ "finding_extraction": (
86
+ "HARD RULE: `relation_to_claim` does NOT belong in a Finding. "
87
+ "Findings record only what the study observed (`effect_direction`); "
88
+ "the relation to a Claim lives on the EvidenceLink."),
89
+ "source_validation": (
90
+ "HARD RULE: a search snippet is never evidence content (RULE 2). "
91
+ "Only fetched + validated source content may be extracted."),
92
+ "counter_evidence_search": (
93
+ "HARD RULE: seek null/negative/contradictory evidence, AI "
94
+ "dependency, reduced transfer, novelty effects, self-selection "
95
+ "bias, alternative explanations. Never fabricate counter-evidence."),
96
+ "evidence_synthesis": (
97
+ "HARD RULE: independent-study counting only. 5 Findings from one "
98
+ "Study = 1 independent study; never count Findings as Studies."),
99
+ "tribunal": (
100
+ "HARD RULE: pass the Pre-Verdict Gate before adjudicating; "
101
+ "critical failures forbid high-confidence verdicts."),
102
+ }
103
+ if cap in rules:
104
+ lines += ["", "## Hard rules", rules[cap]]
105
+
106
+ return "\n".join(lines) + "\n"
@@ -0,0 +1,99 @@
1
+ """Capability registry — the scientific capabilities a research plan can use.
2
+
3
+ The registry is independent of Agent names and model names: planning routes
4
+ by capability, execution routing (Agent MCP vs native) happens later and
5
+ orthogonally. A capability may be deterministic-local (Native Core) or
6
+ capability-discovered (SCP), and may carry a scientific gate.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class CapabilitySpec:
16
+ capability_id: str
17
+ input_contracts: tuple[str, ...]
18
+ output_contracts: tuple[str, ...]
19
+ deterministic_local: bool
20
+ scientific_gate: str | None
21
+
22
+
23
+ _REGISTRY: dict[str, CapabilitySpec] = {}
24
+
25
+
26
+ def _register(capability_id: str, *, input_contracts: tuple[str, ...],
27
+ output_contracts: tuple[str, ...], deterministic_local: bool,
28
+ scientific_gate: str | None = None) -> None:
29
+ _REGISTRY[capability_id] = CapabilitySpec(
30
+ capability_id=capability_id,
31
+ input_contracts=input_contracts,
32
+ output_contracts=output_contracts,
33
+ deterministic_local=deterministic_local,
34
+ scientific_gate=scientific_gate,
35
+ )
36
+
37
+
38
+ # Evidence Review pipeline
39
+ _register("research_framing", input_contracts=("research-intent",),
40
+ output_contracts=("education-frame",), deterministic_local=False)
41
+ _register("literature_search", input_contracts=("education-frame",),
42
+ output_contracts=("source",), deterministic_local=False)
43
+ _register("counter_evidence_search", input_contracts=("education-frame",),
44
+ output_contracts=("source",), deterministic_local=False,
45
+ scientific_gate="RULE 4: independent counter-evidence search")
46
+ _register("source_fetch", input_contracts=("source",),
47
+ output_contracts=("fetch-result",), deterministic_local=False)
48
+ _register("source_validation", input_contracts=("source", "fetch-result"),
49
+ output_contracts=("source",), deterministic_local=True,
50
+ scientific_gate="snippet != evidence (RULE 2)")
51
+ _register("study_extraction", input_contracts=("source",),
52
+ output_contracts=("study",), deterministic_local=False)
53
+ _register("finding_extraction", input_contracts=("study",),
54
+ output_contracts=("finding",), deterministic_local=False)
55
+ _register("methodology_appraisal", input_contracts=("study",),
56
+ output_contracts=("methodology-audit",), deterministic_local=False)
57
+ _register("claim_linking", input_contracts=("finding", "claim"),
58
+ output_contracts=("evidence-link",), deterministic_local=False,
59
+ scientific_gate="relation_to_claim lives on the link, never the finding")
60
+ _register("evidence_synthesis", input_contracts=("evidence-link", "methodology-audit"),
61
+ output_contracts=("synthesis",), deterministic_local=True,
62
+ scientific_gate="independent Study counting, never Finding counting")
63
+ _register("tribunal", input_contracts=("synthesis",),
64
+ output_contracts=("decision-snapshot",), deterministic_local=False,
65
+ scientific_gate="Pre-Verdict Gate before Tribunal")
66
+ _register("applicability_analysis", input_contracts=("evidence-link",),
67
+ output_contracts=("applicability",), deterministic_local=False)
68
+ _register("knowledge_gap_detection", input_contracts=("education-frame", "claim"),
69
+ output_contracts=("knowledge-gap",), deterministic_local=True)
70
+ _register("report_projection", input_contracts=("graph-revision",),
71
+ output_contracts=("projection",), deterministic_local=True)
72
+ _register("report_rendering", input_contracts=("projection",),
73
+ output_contracts=("report",), deterministic_local=True)
74
+
75
+ # Full Research Cycle additions
76
+ _register("study_design", input_contracts=("knowledge-gap",),
77
+ output_contracts=("study-design",), deterministic_local=False,
78
+ scientific_gate="No new study design without evidence grounding")
79
+ _register("measurement_design", input_contracts=("study-design",),
80
+ output_contracts=("analysis-plan",), deterministic_local=False)
81
+ _register("data_validation", input_contracts=("dataset-asset",),
82
+ output_contracts=("dataset-manifest",), deterministic_local=True,
83
+ scientific_gate="provenance/hash/missingness before analysis")
84
+ _register("data_analysis", input_contracts=("dataset-manifest", "analysis-plan"),
85
+ output_contracts=("analysis-run",), deterministic_local=True,
86
+ scientific_gate="never fabricate p-values; ANALYSIS_CAPABILITY_UNAVAILABLE")
87
+ _register("intervention_design", input_contracts=("decision-snapshot",),
88
+ output_contracts=("intervention",), deterministic_local=False)
89
+ _register("evaluation_design", input_contracts=("decision-snapshot",),
90
+ output_contracts=("evaluation",), deterministic_local=False)
91
+
92
+
93
+ def capability_registry() -> dict[str, CapabilitySpec]:
94
+ """Return the frozen capability registry."""
95
+ return dict(_REGISTRY)
96
+
97
+
98
+ def capability(capability_id: str) -> CapabilitySpec | None:
99
+ return _REGISTRY.get(capability_id)