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,368 @@
1
+ """engine/pilot.py - Decision-to-Outcome Loop (v3).
2
+
3
+ Closes the v2 Full Research Cycle: a PILOT DecisionSnapshot -> PilotRun
4
+ registration -> outcome data import (provenance-safe, PII-blocked) -> analysis
5
+ link -> graph revision with pilot evidence -> tribunal re-adjudication ->
6
+ new DecisionSnapshot + machine-readable diff.
7
+
8
+ Privacy discipline (matches engine/datasets.py): student data stays local,
9
+ PII columns (names / student ids / emails / phones) are refused at import.
10
+
11
+ Contracts: schemas/v3/pilot-outcome.schema.json for the pilot record;
12
+ graph entities follow schemas/v2/*.json (study/finding/evidence-link/source).
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ from datetime import datetime, timezone
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ from engine.datasets import analysis_blocked_by_privacy, derive_csv_profile, ingest_dataset
22
+ from engine.graph_store import GraphMutation, GraphStore
23
+ from engine.ids import new_local_id, new_run_id
24
+ from engine.project import ProjectWorkspace
25
+ from engine.synthesis import synthesize_project
26
+ from engine.tribunal import adjudicate, decision_diff, save_decision_snapshot
27
+ from engine.versions import (
28
+ CONFIDENCE_POLICY_VERSION,
29
+ METHODOLOGY_POLICY_VERSION,
30
+ SOURCE_VALIDATION_POLICY_VERSION,
31
+ )
32
+
33
+ #: Outcome Taxonomy tokens that pilots may measure (mirrors outcome-taxonomy.md).
34
+ OUTCOME_TAXONOMY = {
35
+ "knowledge_gain", "concept_understanding", "retention", "transfer",
36
+ "independent_problem_solving", "completion_time", "accuracy",
37
+ "code_quality", "assignment_score", "engagement", "motivation",
38
+ "cognitive_load", "help_seeking", "metacognition", "ai_dependency",
39
+ "over_reliance", "reduced_effort", "reduced_transfer",
40
+ "academic_integrity_risk", "false_confidence",
41
+ }
42
+
43
+ PILOT_STATUSES = ("registered", "data_imported", "analyzed", "adjudicated")
44
+
45
+ #: Column names that reveal individuals; any match blocks pilot data import.
46
+ PII_COLUMN_HINTS = ("name", "student", "学号", "姓名", "email", "mail",
47
+ "phone", "手机", "电话", "id_card", "身份证", "address", "地址")
48
+
49
+ _DECISION_IMPLICATION = {"support": "support_adoption",
50
+ "contradict": "oppose_adoption", "neutral": "neutral"}
51
+
52
+ #: Outcome Taxonomy token -> graph outcome category enum (schemas/v2/outcome).
53
+ _OUTCOME_CATEGORY = {
54
+ "knowledge_gain": "learning", "concept_understanding": "learning",
55
+ "retention": "learning", "transfer": "learning",
56
+ "independent_problem_solving": "learning",
57
+ "completion_time": "task_performance", "accuracy": "task_performance",
58
+ "code_quality": "task_performance", "assignment_score": "task_performance",
59
+ "engagement": "process", "motivation": "process",
60
+ "cognitive_load": "process", "help_seeking": "process",
61
+ "metacognition": "process",
62
+ "ai_dependency": "risk", "over_reliance": "risk",
63
+ "reduced_effort": "risk", "reduced_transfer": "risk",
64
+ "academic_integrity_risk": "risk", "false_confidence": "risk",
65
+ }
66
+
67
+
68
+ def _now_iso() -> str:
69
+ return datetime.now(timezone.utc).isoformat()
70
+
71
+
72
+ def _pilot_path(project: ProjectWorkspace, pilot_id: str) -> Path:
73
+ return project.path / "pilots" / f"{pilot_id}.json"
74
+
75
+
76
+ def _load_pilot(project: ProjectWorkspace, pilot_id: str) -> dict:
77
+ path = _pilot_path(project, pilot_id)
78
+ if not path.is_file():
79
+ raise FileNotFoundError(f"pilot not found: {pilot_id} (missing {path})")
80
+ return json.loads(path.read_text(encoding="utf-8"))
81
+
82
+
83
+ def _save_pilot(project: ProjectWorkspace, pilot: dict) -> Path:
84
+ from scripts.validate_schema import SchemaError, validate # noqa: PLC0415
85
+
86
+ schema_path = (Path(__file__).resolve().parent.parent / "schemas" / "v3"
87
+ / "pilot-outcome.schema.json")
88
+ schema = json.loads(schema_path.read_text(encoding="utf-8"))
89
+ try:
90
+ validate(pilot, schema)
91
+ except SchemaError as exc:
92
+ raise ValueError(f"invalid pilot record: {exc}") from exc
93
+ path = _pilot_path(project, pilot["pilot_id"])
94
+ path.parent.mkdir(parents=True, exist_ok=True)
95
+ tmp = path.with_suffix(".json.tmp")
96
+ tmp.write_text(json.dumps(pilot, ensure_ascii=False, indent=2) + "\n",
97
+ encoding="utf-8")
98
+ tmp.replace(path)
99
+ return path
100
+
101
+
102
+ def register_pilot(project: ProjectWorkspace, *,
103
+ decision_snapshot_id: str, title: str,
104
+ start_date: str, end_date: str,
105
+ conditions: list[str], sample_size: int, design_id: str,
106
+ anon_policy: dict, outcome_columns: list[str]) -> dict:
107
+ """Register a PilotRun bound to an existing DecisionSnapshot."""
108
+ decisions_dir = project.path / "decisions"
109
+ if not (decisions_dir / f"{decision_snapshot_id}.json").is_file():
110
+ raise ValueError(
111
+ f"decision snapshot {decision_snapshot_id} not found in this project; "
112
+ "a pilot must bind to a real adjudication")
113
+ unknown = [o for o in outcome_columns if o not in OUTCOME_TAXONOMY]
114
+ if unknown:
115
+ raise ValueError(
116
+ f"outcome_columns outside Outcome Taxonomy: {sorted(unknown)}")
117
+ if not conditions or sample_size < 1:
118
+ raise ValueError("conditions must be non-empty and sample_size >= 1")
119
+ if anon_policy.get("no_pii_columns") is not True:
120
+ raise ValueError("anon_policy.no_pii_columns must be true (student data stays local)")
121
+
122
+ existing = {p.stem for p in (project.path / "pilots").glob("PIL-*.json")} \
123
+ if (project.path / "pilots").is_dir() else set()
124
+ pilot = {
125
+ "pilot_id": new_local_id("PIL", existing),
126
+ "project_id": project.project_id,
127
+ "decision_snapshot_id": decision_snapshot_id,
128
+ "title": title,
129
+ "start_date": start_date,
130
+ "end_date": end_date,
131
+ "conditions": list(conditions),
132
+ "sample_size": int(sample_size),
133
+ "design_id": design_id,
134
+ "anon_policy": dict(anon_policy),
135
+ "outcome_columns": list(outcome_columns),
136
+ "dataset_asset_id": None,
137
+ "analysis_run_id": None,
138
+ "status": "registered",
139
+ "redecide": None,
140
+ "created_at": _now_iso(),
141
+ "extensions": {},
142
+ }
143
+ _save_pilot(project, pilot)
144
+ return pilot
145
+
146
+
147
+ def import_outcomes(project: ProjectWorkspace, pilot_id: str, *,
148
+ source_path: Path, privacy: dict,
149
+ variable_dictionary: dict[str, str] | None = None) -> dict:
150
+ """Ingest pilot outcome data (CSV) with a PII column gate.
151
+
152
+ Blocks: PII column names, missing outcome columns, and datasets whose
153
+ deidentification requirements are unmet.
154
+ """
155
+ pilot = _load_pilot(project, pilot_id)
156
+ if pilot["status"] not in ("registered", "data_imported"):
157
+ raise ValueError(
158
+ f"pilot {pilot_id} status {pilot['status']!r} cannot import outcomes")
159
+
160
+ source_path = Path(source_path)
161
+ if not source_path.is_file():
162
+ raise FileNotFoundError(f"outcome data missing: {source_path}")
163
+
164
+ profile = derive_csv_profile(source_path)
165
+ columns = list((profile.get("missingness") or {}).keys())
166
+ lower = {str(c).strip().lower(): str(c).strip() for c in columns}
167
+
168
+ blocked = [c for c in lower if any(hint in c for hint in PII_COLUMN_HINTS)]
169
+ if blocked:
170
+ raise ValueError(
171
+ "PII columns detected and refused: " + ", ".join(sorted(blocked)) +
172
+ "; deidentify the file before import")
173
+
174
+ missing = [o for o in pilot["outcome_columns"]
175
+ if o.lower() not in lower and o not in columns]
176
+ if missing:
177
+ raise ValueError(
178
+ f"outcome column(s) missing from CSV: {missing}; available: {sorted(columns)}")
179
+
180
+ asset = ingest_dataset(project, design_id=pilot["design_id"],
181
+ source_path=source_path, privacy=privacy,
182
+ variable_dictionary=variable_dictionary)
183
+ privacy_reasons = analysis_blocked_by_privacy(asset)
184
+ if privacy_reasons:
185
+ raise ValueError("pilot data blocked from analysis:\n- " +
186
+ "\n- ".join(privacy_reasons))
187
+
188
+ pilot["dataset_asset_id"] = asset["dataset_id"]
189
+ pilot["status"] = "data_imported"
190
+ _save_pilot(project, pilot)
191
+ return asset
192
+
193
+
194
+ def link_analysis(project: ProjectWorkspace, pilot_id: str, *,
195
+ analysis_run_id: str) -> dict:
196
+ """Link a completed analysis run to the pilot (status -> analyzed)."""
197
+ pilot = _load_pilot(project, pilot_id)
198
+ if pilot["status"] not in ("data_imported", "analyzed"):
199
+ raise ValueError(
200
+ f"pilot {pilot_id} must import data before linking an analysis")
201
+ run_path = project.path / "analyses" / f"{analysis_run_id}.json"
202
+ if not run_path.is_file():
203
+ raise ValueError(f"analysis run not found: {analysis_run_id}")
204
+ pilot["analysis_run_id"] = analysis_run_id
205
+ pilot["status"] = "analyzed"
206
+ _save_pilot(project, pilot)
207
+ return pilot
208
+
209
+
210
+ def _ensure_outcome(store: GraphStore, outcome_id: str) -> dict:
211
+ existing = store.get("outcomes", outcome_id)
212
+ if existing:
213
+ return existing
214
+ token = outcome_id[len("OUT-"):]
215
+ return {
216
+ "outcome_id": outcome_id,
217
+ "name": token,
218
+ "outcome_type": _OUTCOME_CATEGORY.get(token, "learning"),
219
+ "extensions": {"pilot_outcome": True},
220
+ }
221
+
222
+
223
+ def redecide(project: ProjectWorkspace, pilot_id: str, *,
224
+ claim_id: str, outcome_token: str, measure: str,
225
+ effect_direction: str, raw_result_text: str,
226
+ relation_to_claim: str, effect_estimate: dict | None = None,
227
+ directness: int = 2, reasoning_note: str = "") -> dict:
228
+ """Fold pilot outcome evidence into the graph and re-adjudicate.
229
+
230
+ Creates (or reuses) a project-origin source + study for the pilot,
231
+ upserts one finding + evidence link, commits a new graph revision,
232
+ runs the tribunal and writes a new DecisionSnapshot with a diff against
233
+ the decision the pilot was bound to.
234
+ """
235
+ if effect_direction not in ("positive", "negative", "null"):
236
+ raise ValueError(f"invalid effect_direction {effect_direction!r}")
237
+ if relation_to_claim not in ("support", "contradict", "neutral"):
238
+ raise ValueError(f"invalid relation_to_claim {relation_to_claim!r}")
239
+ if outcome_token not in OUTCOME_TAXONOMY:
240
+ raise ValueError(
241
+ f"outcome_token {outcome_token!r} outside Outcome Taxonomy")
242
+ outcome_id = f"OUT-{outcome_token}"
243
+ pilot = _load_pilot(project, pilot_id)
244
+ if pilot["status"] not in ("analyzed", "data_imported"):
245
+ raise ValueError(
246
+ f"pilot {pilot_id} status {pilot['status']!r}; import data (and ideally "
247
+ "link an analysis) before re-adjudication")
248
+ # Idempotency guard (final review P1-3): a pilot that already produced a
249
+ # new DecisionSnapshot must never be re-adjudicated — a retry after a
250
+ # partial failure would silently duplicate pilot evidence in the graph.
251
+ if pilot.get("redecide") is not None:
252
+ raise ValueError(
253
+ f"pilot {pilot_id} already adjudicated into "
254
+ f"{pilot['redecide']['new_decision_snapshot_id']}; refusing re-entry "
255
+ "(create a new pilot for a new cycle)")
256
+
257
+ store = GraphStore(project)
258
+ claims = {c["claim_id"]: c for c in store.read_table("claims")}
259
+ if claim_id not in claims:
260
+ raise ValueError(f"claim {claim_id} not found in project graph")
261
+
262
+ source_id = new_local_id("SRC", {s["source_id"] for s in store.read_table("sources")})
263
+ source = {
264
+ "source_id": source_id,
265
+ "origin": "project",
266
+ "source_type": "dataset",
267
+ "canonical_locator": f"project:pilot/{pilot_id}",
268
+ "validation_status": "valid",
269
+ "content_hash": None,
270
+ "extensions": {"pilot_id": pilot_id,
271
+ "dataset_asset_id": pilot.get("dataset_asset_id")},
272
+ }
273
+ study_id = new_local_id("STU", {s["study_id"] for s in store.read_table("studies")})
274
+ study = {
275
+ "study_id": study_id,
276
+ "source_ids": [source_id],
277
+ "study_design": "pilot",
278
+ "population": f"pilot cohort (n={pilot['sample_size']})",
279
+ "sample_ids": [f"PILOT-{pilot['pilot_id']}"],
280
+ "independence_key": f"pilot:{pilot['pilot_id']}",
281
+ "identity_status": "resolved",
282
+ "extensions": {"pilot_id": pilot_id},
283
+ }
284
+ outcome = _ensure_outcome(store, outcome_id)
285
+ estimate = None
286
+ if effect_estimate is not None:
287
+ estimate = {
288
+ "metric": measure,
289
+ "value": effect_estimate.get("value"),
290
+ "raw_text": raw_result_text,
291
+ }
292
+ finding_id = new_local_id("FND", {f["finding_id"] for f in store.read_table("findings")})
293
+ finding = {
294
+ "finding_id": finding_id,
295
+ "study_id": study_id,
296
+ "finding_type": "quantitative_effect" if estimate else "descriptive",
297
+ "outcome_id": outcome_id,
298
+ "measure": measure,
299
+ "timepoint": pilot.get("end_date"),
300
+ "effect_direction": effect_direction,
301
+ "effect_estimate": estimate,
302
+ "raw_result_text": raw_result_text,
303
+ "source_locator": source["canonical_locator"],
304
+ "extensions": {"pilot_id": pilot_id,
305
+ "analysis_run_id": pilot.get("analysis_run_id")},
306
+ }
307
+ link_id = new_local_id("LNK", {l["evidence_link_id"] for l in store.read_table("evidence_links")})
308
+ link = {
309
+ "evidence_link_id": link_id,
310
+ "finding_id": finding_id,
311
+ "claim_id": claim_id,
312
+ "relation_to_claim": relation_to_claim,
313
+ "decision_implication": _DECISION_IMPLICATION[relation_to_claim],
314
+ "directness": directness,
315
+ "applicability": {"scope_match": "direct",
316
+ "target_population": f"pilot cohort (n={pilot['sample_size']})",
317
+ "context_notes": f"pilot {pilot_id}: {pilot['title']}"},
318
+ "reasoning_note": reasoning_note or f"pilot outcomes: {pilot['title']}",
319
+ "created_in_revision": store.active_revision() + 1,
320
+ "extensions": {"pilot_id": pilot_id},
321
+ }
322
+
323
+ mutation = GraphMutation(upserts={
324
+ "sources": [source],
325
+ "studies": [study],
326
+ "outcomes": [outcome] if store.get("outcomes", outcome_id) is None else [],
327
+ "findings": [finding],
328
+ "evidence_links": [link],
329
+ })
330
+ revision = store.commit(
331
+ run_id=new_run_id(), reason=f"pilot outcomes re-adjudication: {pilot_id}",
332
+ mutation=mutation)
333
+
334
+ try:
335
+ store.repair_head_mirror()
336
+ syntheses = synthesize_project(store)
337
+ snapshot = adjudicate(
338
+ store, project=project, claim_syntheses=syntheses,
339
+ policy_versions={"confidence": CONFIDENCE_POLICY_VERSION,
340
+ "methodology": METHODOLOGY_POLICY_VERSION,
341
+ "source_validation": SOURCE_VALIDATION_POLICY_VERSION})
342
+ path = save_decision_snapshot(project, snapshot)
343
+
344
+ previous = None
345
+ prev_path = project.path / "decisions" / f"{pilot['decision_snapshot_id']}.json"
346
+ if prev_path.is_file():
347
+ previous = json.loads(prev_path.read_text(encoding="utf-8"))
348
+ diff = decision_diff(previous, snapshot)
349
+ except Exception as exc: # noqa: BLE001 - graph committed, mark failure for diagnosis
350
+ # The graph revision is already committed; never let the pilot record
351
+ # claim success. Record the failure so the state machine is
352
+ # diagnosable and a human can recover (P1-3).
353
+ pilot.setdefault("extensions", {})["redecide_failed"] = {
354
+ "graph_revision": revision.revision,
355
+ "error": str(exc),
356
+ }
357
+ _save_pilot(project, pilot)
358
+ raise
359
+
360
+ pilot["redecide"] = {
361
+ "new_decision_snapshot_id": snapshot["decision_snapshot_id"],
362
+ "graph_revision": revision.revision,
363
+ "diff": diff,
364
+ }
365
+ pilot["status"] = "adjudicated"
366
+ _save_pilot(project, pilot)
367
+ return {"snapshot": snapshot, "revision": revision.revision,
368
+ "diff": diff, "snapshot_path": str(path)}
@@ -0,0 +1,126 @@
1
+ """Capability task-DAG planner.
2
+
3
+ Plans research by scientific capability, never by Agent/model names. A
4
+ `PlanStep(kind="capability")` maps 1:1 to a capability; a
5
+ `PlanStep(kind="wait", wait_state="waiting_for_user_data")` is a real state
6
+ when the user's dataset is required but not yet present — never a fake
7
+ capability.
8
+
9
+ `intervention_design` / `evaluation_design` are decision_target-dependent:
10
+ included for `teaching_pilot` / `evaluation_plan` / `research_cycle`, not
11
+ forced for `evidence_review` to satisfy a fixed report template.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass
17
+
18
+ from engine.capabilities import capability, capability_registry
19
+
20
+ _FULL_CYCLE_EXTRAS = ("study_design", "measurement_design")
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class PlanStep:
25
+ step_id: str
26
+ kind: str # capability | wait
27
+ capability_id: str | None
28
+ wait_state: str | None
29
+ depends_on: tuple[str, ...]
30
+ input_refs: tuple[str, ...]
31
+ output_contract: str | None
32
+ gate: str | None
33
+
34
+
35
+ def _step(step_id: str, capability_id: str, depends_on: tuple[str, ...]) -> PlanStep:
36
+ spec = capability(capability_id)
37
+ if spec is None:
38
+ raise ValueError(f"unknown capability {capability_id!r}")
39
+ return PlanStep(
40
+ step_id=step_id,
41
+ kind="capability",
42
+ capability_id=capability_id,
43
+ wait_state=None,
44
+ depends_on=depends_on,
45
+ input_refs=spec.input_contracts,
46
+ output_contract=spec.output_contracts[0] if spec.output_contracts else None,
47
+ gate=spec.scientific_gate,
48
+ )
49
+
50
+
51
+ _EVIDENCE_REVIEW_STEPS = (
52
+ ("research_framing", ()),
53
+ ("literature_search", ("research_framing",)),
54
+ ("counter_evidence_search", ("research_framing",)),
55
+ ("source_fetch", ("literature_search", "counter_evidence_search")),
56
+ ("source_validation", ("source_fetch",)),
57
+ ("study_extraction", ("source_validation",)),
58
+ ("finding_extraction", ("study_extraction",)),
59
+ ("methodology_appraisal", ("study_extraction",)),
60
+ ("claim_linking", ("finding_extraction",)),
61
+ ("evidence_synthesis", ("claim_linking", "methodology_appraisal")),
62
+ ("tribunal", ("evidence_synthesis",)),
63
+ ("applicability_analysis", ("claim_linking",)),
64
+ ("knowledge_gap_detection", ("research_framing", "claim_linking")),
65
+ ("report_projection", ("tribunal", "knowledge_gap_detection")),
66
+ ("report_rendering", ("report_projection",)),
67
+ )
68
+
69
+
70
+ def build_research_plan(*, mode: str, decision_target: str, depth: str,
71
+ has_grounding: bool,
72
+ has_dataset: bool) -> tuple[PlanStep, ...]:
73
+ """Build the capability task DAG for a research mode.
74
+
75
+ - evidence_review: the fixed 15-step pipeline.
76
+ - full_research_cycle: Evidence Review + study_design/measurement_design
77
+ after grounding/gap tasks; if `has_dataset=False`, emits a wait step
78
+ instead of pretending data exists.
79
+ - intervention/evaluation design depend on decision_target.
80
+ """
81
+ steps: list[PlanStep] = []
82
+ for capability_id, depends in _EVIDENCE_REVIEW_STEPS:
83
+ steps.append(_step(capability_id, capability_id, depends))
84
+
85
+ if mode == "full_research_cycle":
86
+ if not has_grounding:
87
+ # the plan must not design a study before grounding exists; the
88
+ # router already flagged requires_grounding_review, and the
89
+ # executor runs the minimum Evidence Review first. The planner
90
+ # still emits the design steps but they depend on tribunal/gap.
91
+ pass
92
+ for capability_id in _FULL_CYCLE_EXTRAS:
93
+ dep = ("knowledge_gap_detection",) if capability_id == "study_design" \
94
+ else ("study_design",)
95
+ steps.append(_step(capability_id, capability_id, dep))
96
+ if has_dataset:
97
+ steps.append(_step("data_validation", "data_validation",
98
+ ("measurement_design",)))
99
+ steps.append(_step("data_analysis", "data_analysis",
100
+ ("data_validation",)))
101
+ else:
102
+ steps.append(PlanStep(
103
+ step_id="wait_for_user_data",
104
+ kind="wait",
105
+ capability_id=None,
106
+ wait_state="waiting_for_user_data",
107
+ depends_on=("measurement_design",),
108
+ input_refs=(),
109
+ output_contract=None,
110
+ gate=None,
111
+ ))
112
+
113
+ target_design = decision_target in ("teaching_pilot", "evaluation_plan",
114
+ "research_cycle")
115
+ if target_design:
116
+ steps.append(_step("intervention_design", "intervention_design",
117
+ ("tribunal",)))
118
+ steps.append(_step("evaluation_design", "evaluation_design",
119
+ ("tribunal",)))
120
+
121
+ return tuple(steps)
122
+
123
+
124
+ def plan_capability_ids(plan: tuple[PlanStep, ...]) -> list[str]:
125
+ """Capability ids in plan order (excludes wait steps)."""
126
+ return [s.capability_id for s in plan if s.kind == "capability" and s.capability_id]
@@ -0,0 +1,118 @@
1
+ """ProjectWorkspace lifecycle — the durable unit of V2 research.
2
+
3
+ A Project owns a versioned Evidence Graph plus gap/design/dataset/analysis/
4
+ decision/projection/report/run areas. Run is one execution attempt *inside*
5
+ a Project; Project is long-lived. `project.json` is written atomically and
6
+ always mirrors `graph/HEAD` in `graph_revision`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ from dataclasses import dataclass
14
+ from datetime import datetime, timezone
15
+ from pathlib import Path
16
+
17
+ from engine.contracts import validate_record
18
+ from engine.ids import new_project_id
19
+ from engine.versions import ENGINE_VERSION, GRAPH_SCHEMA_VERSION
20
+
21
+ _SUBDIRS = (
22
+ "graph/revisions",
23
+ "gaps",
24
+ "study-designs",
25
+ "datasets/raw",
26
+ "datasets/processed",
27
+ "datasets/manifests",
28
+ "analyses",
29
+ "decisions",
30
+ "projections",
31
+ "reports",
32
+ "runs",
33
+ "pilots",
34
+ )
35
+
36
+ _MANIFEST_NAME = "project.json"
37
+
38
+
39
+ def _now_iso() -> str:
40
+ return datetime.now(timezone.utc).isoformat()
41
+
42
+
43
+ def _atomic_write_json(path: Path, record: dict) -> None:
44
+ """Write JSON via tmp file + rename so readers never see partial state."""
45
+ path.parent.mkdir(parents=True, exist_ok=True)
46
+ tmp = path.with_name(path.name + ".tmp")
47
+ tmp.write_text(
48
+ json.dumps(record, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
49
+ )
50
+ os.replace(tmp, path)
51
+
52
+
53
+ def _default_decision_target(research_mode: str) -> str:
54
+ return "research_cycle" if research_mode == "full_research_cycle" else "teaching_decision"
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class ProjectWorkspace:
59
+ home: Path
60
+ project_id: str
61
+ path: Path
62
+
63
+ @classmethod
64
+ def create(cls, home: Path, *, question: str, title: str,
65
+ research_mode: str) -> "ProjectWorkspace":
66
+ home = Path(home).expanduser().resolve()
67
+ project_id = new_project_id(question)
68
+ path = home / "projects" / project_id
69
+ for sub in _SUBDIRS:
70
+ (path / sub).mkdir(parents=True, exist_ok=True)
71
+ now = _now_iso()
72
+ manifest = {
73
+ "project_id": project_id,
74
+ "title": title,
75
+ "domain": "education",
76
+ "question": question,
77
+ "research_mode": research_mode,
78
+ "decision_target": _default_decision_target(research_mode),
79
+ "created_at": now,
80
+ "updated_at": now,
81
+ "engine_version": ENGINE_VERSION,
82
+ "schema_version": GRAPH_SCHEMA_VERSION,
83
+ "graph_revision": 0,
84
+ "status": "active",
85
+ }
86
+ errors = validate_record("project", manifest)
87
+ if errors:
88
+ raise ValueError(f"invalid project manifest: {errors}")
89
+ _atomic_write_json(path / _MANIFEST_NAME, manifest)
90
+ return cls(home=home, project_id=project_id, path=path)
91
+
92
+ @classmethod
93
+ def open(cls, home: Path, project_id: str) -> "ProjectWorkspace":
94
+ home = Path(home).expanduser().resolve()
95
+ path = home / "projects" / project_id
96
+ if not (path / _MANIFEST_NAME).is_file():
97
+ raise FileNotFoundError(f"project {project_id!r} not found under {home}")
98
+ return cls(home=home, project_id=project_id, path=path)
99
+
100
+ def manifest(self) -> dict:
101
+ return json.loads((self.path / _MANIFEST_NAME).read_text(encoding="utf-8"))
102
+
103
+ def update_manifest(self, **changes) -> dict:
104
+ """Apply changes, bump updated_at, write atomically; return the new manifest."""
105
+ manifest = self.manifest()
106
+ manifest.update(changes)
107
+ manifest["updated_at"] = _now_iso()
108
+ errors = validate_record("project", manifest)
109
+ if errors:
110
+ raise ValueError(f"invalid project manifest after update: {errors}")
111
+ _atomic_write_json(self.path / _MANIFEST_NAME, manifest)
112
+ return manifest
113
+
114
+ def current_revision(self) -> int:
115
+ return int(self.manifest()["graph_revision"])
116
+
117
+ def runs_dir(self) -> Path:
118
+ return self.path / "runs"