eduevidence 5.2.0 → 6.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 (386) hide show
  1. package/CONTRIBUTING.md +105 -0
  2. package/README.md +142 -75
  3. package/README.zh-CN.md +73 -30
  4. package/SKILL.md +397 -131
  5. package/agents/openai.yaml +4 -0
  6. package/assets/readme/controlled-execution.svg +34 -0
  7. package/assets/readme/landing-tour.gif +0 -0
  8. package/assets/readme/logo.png +0 -0
  9. package/assets/readme/research-workflow.svg +56 -0
  10. package/assets/readme/studio-graph.png +0 -0
  11. package/assets/readme/studio-overview.png +0 -0
  12. package/assets/readme/studio-reports.png +0 -0
  13. package/assets/readme/studio-tour.gif +0 -0
  14. package/autoevolve/config.yaml +17 -0
  15. package/autoevolve/program.md +25 -0
  16. package/autoevolve/protected.manifest.yaml +34 -0
  17. package/benchmarks/adversarial/cases.jsonl +7 -0
  18. package/benchmarks/evidence-library.json +5268 -0
  19. package/benchmarks/partitions.json +8 -0
  20. package/bin/eduevidence.js +2 -1
  21. package/docs/architecture.md +496 -0
  22. package/docs/autoresearch-evolution-plan.md +2903 -0
  23. package/docs/autoresearch-implementation-status.md +101 -0
  24. package/docs/demo-storyboard.md +20 -0
  25. package/docs/demo-workplace-ai.md +92 -0
  26. package/docs/demo.md +32 -0
  27. package/docs/install-guide.md +150 -0
  28. package/docs/orchestration-role-model.md +1254 -0
  29. package/docs/release-closeout/README.md +17 -0
  30. package/docs/release-closeout/frontend-acceptance.md +23 -0
  31. package/docs/release-closeout/issues.md +19 -0
  32. package/docs/release-closeout/verification.md +28 -0
  33. package/docs/release-contract.md +108 -0
  34. package/docs/research-studio-guide.zh-CN.md +166 -0
  35. package/docs/sciverse-api.md +125 -0
  36. package/eduevidence_cli.py +29 -13
  37. package/engine/_resources.py +13 -0
  38. package/engine/autoevolve/__init__.py +3 -0
  39. package/engine/autoevolve/agent_view.py +167 -0
  40. package/engine/autoevolve/core.py +357 -0
  41. package/engine/autoevolve/events.py +11 -0
  42. package/engine/autoevolve/git_workspace.py +77 -0
  43. package/engine/autoevolve/projection.py +23 -0
  44. package/engine/autoevolve/runner.py +413 -0
  45. package/engine/autoevolve/trust.py +146 -0
  46. package/engine/autoresearch/__init__.py +6 -0
  47. package/engine/autoresearch/commit.py +132 -0
  48. package/engine/autoresearch/contracts.py +126 -0
  49. package/engine/autoresearch/controller.py +207 -0
  50. package/engine/autoresearch/events.py +12 -0
  51. package/engine/autoresearch/gap_priority.py +168 -0
  52. package/engine/autoresearch/projection.py +30 -0
  53. package/engine/autoresearch/research_memory.py +59 -0
  54. package/engine/autoresearch/saturation.py +91 -0
  55. package/engine/briefs.py +2 -1
  56. package/engine/capabilities.py +1 -0
  57. package/engine/contracts.py +3 -1
  58. package/engine/decision_policy.py +96 -0
  59. package/engine/evidence_graph.py +14 -10
  60. package/engine/evidencecore.py +7 -5
  61. package/engine/gaps.py +132 -73
  62. package/engine/ids.py +2 -0
  63. package/engine/judge_pack.py +65 -0
  64. package/engine/library.py +6 -2
  65. package/engine/library_builtin.py +3 -1
  66. package/engine/living.py +36 -5
  67. package/engine/meta_synthesis.py +3 -1
  68. package/engine/migration.py +88 -3
  69. package/engine/orchestration.py +460 -0
  70. package/engine/paths.py +2 -0
  71. package/engine/pilot.py +36 -33
  72. package/engine/project.py +2 -2
  73. package/engine/research_service.py +113 -0
  74. package/engine/studio_read_model.py +400 -0
  75. package/engine/taxonomy.py +211 -0
  76. package/engine/tribunal.py +44 -33
  77. package/engine/update.py +1 -0
  78. package/engine/versions.py +1 -1
  79. package/engine/worker_result.py +109 -0
  80. package/engine/workflows.py +70 -0
  81. package/examples/ai-coding-assistant-evidence/EduEvidence_Report.html +2934 -0
  82. package/examples/ai-coding-assistant-evidence/artifact_manifest.json +15 -0
  83. package/examples/ai-coding-assistant-evidence/citation_check.json +79 -0
  84. package/examples/ai-coding-assistant-evidence/claims.jsonl +12 -0
  85. package/examples/ai-coding-assistant-evidence/evaluation.json +35 -0
  86. package/examples/ai-coding-assistant-evidence/evidence.jsonl +12 -0
  87. package/examples/ai-coding-assistant-evidence/final_verdict.json +107 -0
  88. package/examples/ai-coding-assistant-evidence/frame.json +48 -0
  89. package/examples/ai-coding-assistant-evidence/gate_report.json +101 -0
  90. package/examples/ai-coding-assistant-evidence/intervention.json +51 -0
  91. package/examples/ai-coding-assistant-evidence/methodology.json +36 -0
  92. package/examples/ai-coding-assistant-evidence/raw_verdict.json +86 -0
  93. package/examples/ai-coding-assistant-evidence/report_spec.json +230 -0
  94. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_academic.html +2934 -0
  95. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_claude.html +2934 -0
  96. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab-dark.html +2934 -0
  97. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_datalab.html +2934 -0
  98. package/examples/ai-coding-assistant-evidence/reports-5themes/EduEvidence_Report_presentation.html +2934 -0
  99. package/examples/ai-coding-assistant-evidence/reports-5themes/report_academic.html +2934 -0
  100. package/examples/ai-coding-assistant-evidence/reports-5themes/report_claude.html +2934 -0
  101. package/examples/ai-coding-assistant-evidence/reports-5themes/report_datalab-dark.html +2934 -0
  102. package/examples/ai-coding-assistant-evidence/reports-5themes/report_datalab.html +2934 -0
  103. package/examples/ai-coding-assistant-evidence/reports-5themes/report_presentation.html +2934 -0
  104. package/examples/ai-coding-assistant-evidence/result.json +1457 -0
  105. package/examples/ai-coding-assistant-evidence/result.zh.json +1457 -0
  106. package/examples/ai-coding-assistant-evidence/skeptic.json +72 -0
  107. package/examples/ai-coding-assistant-evidence/sources.jsonl +8 -0
  108. package/examples/ai-coding-assistant-evidence/verdict.json +107 -0
  109. package/examples/spaced-retrieval-practice/applicability.json +14 -0
  110. package/examples/spaced-retrieval-practice/artifact_manifest.json +15 -0
  111. package/examples/spaced-retrieval-practice/claims.jsonl +3 -0
  112. package/examples/spaced-retrieval-practice/evidence.jsonl +6 -0
  113. package/examples/spaced-retrieval-practice/final_verdict.json +93 -0
  114. package/examples/spaced-retrieval-practice/frame.json +58 -0
  115. package/examples/spaced-retrieval-practice/gate_report.json +101 -0
  116. package/examples/spaced-retrieval-practice/methodology.json +78 -0
  117. package/examples/spaced-retrieval-practice/report_spec.json +212 -0
  118. package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_academic.html +2728 -0
  119. package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_claude.html +2728 -0
  120. package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_datalab-dark.html +2728 -0
  121. package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_datalab.html +2728 -0
  122. package/examples/spaced-retrieval-practice/reports-5themes/EduEvidence_Report_presentation.html +2728 -0
  123. package/examples/spaced-retrieval-practice/reports-5themes/report_academic.html +2728 -0
  124. package/examples/spaced-retrieval-practice/reports-5themes/report_claude.html +2728 -0
  125. package/examples/spaced-retrieval-practice/reports-5themes/report_datalab-dark.html +2728 -0
  126. package/examples/spaced-retrieval-practice/reports-5themes/report_datalab.html +2728 -0
  127. package/examples/spaced-retrieval-practice/reports-5themes/report_presentation.html +2728 -0
  128. package/examples/spaced-retrieval-practice/result.json +942 -0
  129. package/examples/spaced-retrieval-practice/result.zh.json +942 -0
  130. package/examples/spaced-retrieval-practice/skeptic.json +70 -0
  131. package/examples/spaced-retrieval-practice/sources.jsonl +7 -0
  132. package/examples/spaced-retrieval-practice/verdict.json +93 -0
  133. package/examples/workplace-ai-assistant/artifact_manifest.json +15 -0
  134. package/examples/workplace-ai-assistant/claims.jsonl +4 -0
  135. package/examples/workplace-ai-assistant/evaluation.json +19 -0
  136. package/examples/workplace-ai-assistant/evidence.jsonl +4 -0
  137. package/examples/workplace-ai-assistant/evidence_graph.json +444 -0
  138. package/examples/workplace-ai-assistant/final_verdict.json +78 -0
  139. package/examples/workplace-ai-assistant/frame.json +41 -0
  140. package/examples/workplace-ai-assistant/gate_report.json +101 -0
  141. package/examples/workplace-ai-assistant/intervention.json +27 -0
  142. package/examples/workplace-ai-assistant/legacy-link-check.json +16 -0
  143. package/examples/workplace-ai-assistant/methodology.json +60 -0
  144. package/examples/workplace-ai-assistant/report_spec.json +224 -0
  145. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_academic.html +2814 -0
  146. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_claude.html +2814 -0
  147. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab-dark.html +2814 -0
  148. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_datalab.html +2814 -0
  149. package/examples/workplace-ai-assistant/reports-5themes/EduEvidence_Report_presentation.html +2814 -0
  150. package/examples/workplace-ai-assistant/reports-5themes/report_academic.html +2814 -0
  151. package/examples/workplace-ai-assistant/reports-5themes/report_claude.html +2814 -0
  152. package/examples/workplace-ai-assistant/reports-5themes/report_datalab-dark.html +2814 -0
  153. package/examples/workplace-ai-assistant/reports-5themes/report_datalab.html +2814 -0
  154. package/examples/workplace-ai-assistant/reports-5themes/report_presentation.html +2814 -0
  155. package/examples/workplace-ai-assistant/result.json +615 -0
  156. package/examples/workplace-ai-assistant/result.zh.json +615 -0
  157. package/examples/workplace-ai-assistant/search_log.json +19 -0
  158. package/examples/workplace-ai-assistant/skeptic.json +72 -0
  159. package/examples/workplace-ai-assistant/sources.jsonl +3 -0
  160. package/examples/workplace-ai-assistant/validation_result.json +9 -0
  161. package/examples/workplace-ai-assistant/verdict.json +78 -0
  162. package/install.sh +7 -7
  163. package/integrations/agent_mcp.py +2 -2
  164. package/integrations/orchestration_dispatch.py +146 -0
  165. package/package.json +46 -3
  166. package/pyproject.toml +14 -22
  167. package/references/autoresearch.md +30 -0
  168. package/references/evaluation-policy.md +24 -0
  169. package/references/orchestration.md +22 -0
  170. package/references/report-copy-style.md +67 -0
  171. package/references/retrieval-compliance.md +75 -0
  172. package/references/retrieval-protocol.md +20 -0
  173. package/references/scientific-invariants.md +19 -0
  174. package/retrieval/audit.py +178 -0
  175. package/retrieval/fetch.py +96 -0
  176. package/retrieval/sciverse.py +398 -0
  177. package/retrieval/search.py +47 -7
  178. package/schemas/applicability.schema.json +94 -0
  179. package/schemas/chart-spec.schema.json +10 -3
  180. package/schemas/evidence.schema.json +316 -43
  181. package/schemas/fetch-result.schema.json +2 -1
  182. package/schemas/intervention.schema.json +106 -21
  183. package/schemas/report-result.schema.json +12 -4
  184. package/schemas/report-spec.schema.json +98 -100
  185. package/schemas/skeptic.schema.json +86 -0
  186. package/schemas/source.schema.json +21 -2
  187. package/schemas/v2/finding.schema.json +5 -1
  188. package/schemas/v2/methodology-audit.schema.json +5 -1
  189. package/schemas/v2/outcome.schema.json +28 -5
  190. package/schemas/v2/project.schema.json +2 -2
  191. package/schemas/v2/run.schema.json +1 -1
  192. package/schemas/v2/study.schema.json +5 -1
  193. package/schemas/vNext/autoevolve-session.schema.json +34 -0
  194. package/schemas/vNext/eval-snapshot.schema.json +77 -0
  195. package/schemas/vNext/execution-plan.schema.json +50 -0
  196. package/schemas/vNext/gap-priority.schema.json +54 -0
  197. package/schemas/vNext/negative-search-record.schema.json +68 -0
  198. package/schemas/vNext/research-iteration.schema.json +87 -0
  199. package/schemas/vNext/research-strategy.schema.json +62 -0
  200. package/schemas/vNext/skill-experiment.schema.json +90 -0
  201. package/schemas/vNext/task-spec.schema.json +156 -0
  202. package/schemas/vNext/worker-result.schema.json +60 -0
  203. package/schemas/verdict.schema.json +164 -28
  204. package/scripts/benchmark_judge.py +2 -2
  205. package/scripts/benchmark_v3.py +26 -43
  206. package/scripts/build_esl_artifacts.py +4 -4
  207. package/scripts/build_evidence_library.py +2 -2
  208. package/scripts/build_gh_pages.py +98 -0
  209. package/scripts/build_readme_diagrams.py +72 -0
  210. package/scripts/build_report_variants.py +101 -0
  211. package/scripts/build_result.py +74 -9
  212. package/scripts/check_autoresearch_invariants.py +95 -0
  213. package/scripts/check_package_parity.py +85 -0
  214. package/scripts/check_protocol_alignment.py +375 -0
  215. package/scripts/check_versioned_schemas.py +254 -0
  216. package/scripts/claim_audit.py +13 -8
  217. package/scripts/compute_confidence.py +10 -0
  218. package/scripts/daily_evolve.py +30 -0
  219. package/scripts/dashboard_server.py +130 -101
  220. package/scripts/did_regression.py +17 -32
  221. package/scripts/enrich_projects_human_and_lieflat.py +1 -1
  222. package/scripts/evidence_score.py +5 -2
  223. package/scripts/generate_metrics.py +4 -3
  224. package/scripts/generate_new_projects.py +5 -5
  225. package/scripts/orchestrator.py +286 -36
  226. package/scripts/pre_verdict_gate.py +224 -26
  227. package/scripts/quickstart.py +18 -2
  228. package/scripts/rebake_all_5themes.py +1 -2
  229. package/scripts/research_auto_cli.py +475 -0
  230. package/scripts/run_workspace.py +24 -8
  231. package/scripts/search_provenance.py +64 -0
  232. package/scripts/serve_web.py +9 -10
  233. package/scripts/skill_lint.py +1 -1
  234. package/scripts/skill_payload.py +81 -0
  235. package/scripts/test_adversarial_empirical.py +26 -19
  236. package/scripts/validate_schema.py +46 -2
  237. package/scripts/vnext_cli.py +133 -0
  238. package/setup.py +12 -0
  239. package/skill/agents/evaluation-designer.md +20 -4
  240. package/skill/agents/evidence-analyst.md +19 -3
  241. package/skill/agents/evidence-judge.md +50 -2
  242. package/skill/agents/evidence-retriever.md +20 -3
  243. package/skill/agents/intervention-designer.md +20 -4
  244. package/skill/agents/method-reviewer.md +18 -2
  245. package/skill/agents/{education-planner.md → research-planner.md} +19 -3
  246. package/skill/agents/skeptic.md +18 -2
  247. package/skill/roles/registry.yaml +45 -0
  248. package/skill/sub-skills/aihot-trend-analysis/SKILL.md +28 -9
  249. package/skill/sub-skills/contradiction-analysis/SKILL.md +31 -11
  250. package/skill/sub-skills/data-analysis/SKILL.md +34 -15
  251. package/skill/sub-skills/ethics-review/SKILL.md +33 -10
  252. package/skill/sub-skills/evidence-extraction/SKILL.md +29 -11
  253. package/skill/sub-skills/evidence-review/SKILL.md +31 -12
  254. package/skill/sub-skills/gap-analysis/SKILL.md +31 -9
  255. package/skill/sub-skills/literature-review/SKILL.md +35 -14
  256. package/skill/sub-skills/methodology-audit/SKILL.md +29 -12
  257. package/skill/sub-skills/report-generation/SKILL.md +40 -6
  258. package/skill/sub-skills/research-planning/SKILL.md +41 -14
  259. package/skill/sub-skills/study-design/SKILL.md +30 -9
  260. package/skill/task-briefs/adjudicate.md +32 -7
  261. package/skill/task-briefs/applicability.md +38 -0
  262. package/skill/task-briefs/audit.md +32 -7
  263. package/skill/task-briefs/challenge.md +34 -5
  264. package/skill/task-briefs/evaluate.md +30 -5
  265. package/skill/task-briefs/extract.md +31 -8
  266. package/skill/task-briefs/frame.md +39 -10
  267. package/skill/task-briefs/intervene.md +32 -6
  268. package/skill/task-briefs/present.md +32 -8
  269. package/skill/task-briefs/projection.md +37 -0
  270. package/skill/task-briefs/retrieve.md +36 -6
  271. package/skill/workflows/decision-and-pilot.md +85 -0
  272. package/skill/workflows/evaluate-and-update.md +93 -0
  273. package/skill/workflows/evidence-review.md +117 -0
  274. package/visualization/eduevidence-report/assets/base.css +2 -2
  275. package/visualization/eduevidence-report/assets/reader.css +752 -0
  276. package/visualization/eduevidence-report/assets/reader.js +132 -0
  277. package/visualization/eduevidence-report/references/chart-selection-catalog.md +109 -0
  278. package/visualization/eduevidence-report/references/lieflat-composition.md +3 -1
  279. package/visualization/eduevidence-report/scripts/build_figures.py +25 -3
  280. package/visualization/eduevidence-report/scripts/build_infographics.py +5 -1
  281. package/visualization/eduevidence-report/scripts/build_report.py +561 -121
  282. package/visualization/eduevidence-report/scripts/charts_data.py +2 -0
  283. package/visualization/eduevidence-report/scripts/lieflat_engine.py +371 -136
  284. package/visualization/eduevidence-report/scripts/zh_labels.py +80 -1
  285. package/visualization/eduevidence-report/themes/academic.css +1 -1
  286. package/visualization/eduevidence-report/themes/claude.css +1 -1
  287. package/visualization/eduevidence-report/themes/datalab-dark.css +2 -2
  288. package/visualization/eduevidence-report/themes/datalab.css +2 -2
  289. package/visualization/eduevidence-report/themes/presentation.css +2 -2
  290. package/web/README.md +18 -0
  291. package/web/architecture.html +14885 -0
  292. package/web/index.html +53 -0
  293. package/web/studio/THIRD_PARTY_LICENSES.txt +146 -0
  294. package/web/studio/assets/index-B8tkF44Q.css +1 -0
  295. package/web/studio/assets/index-CQ6Keoyc.js +230 -0
  296. package/web/studio/config.json +1 -0
  297. package/web/studio/index.html +14 -0
  298. package/engine/__pycache__/__init__.cpython-312.pyc +0 -0
  299. package/engine/__pycache__/analysis.cpython-312.pyc +0 -0
  300. package/engine/__pycache__/bias.cpython-312.pyc +0 -0
  301. package/engine/__pycache__/briefs.cpython-312.pyc +0 -0
  302. package/engine/__pycache__/capabilities.cpython-312.pyc +0 -0
  303. package/engine/__pycache__/citation_check.cpython-312.pyc +0 -0
  304. package/engine/__pycache__/contracts.cpython-312.pyc +0 -0
  305. package/engine/__pycache__/datasets.cpython-312.pyc +0 -0
  306. package/engine/__pycache__/events.cpython-312.pyc +0 -0
  307. package/engine/__pycache__/evidence_graph.cpython-312.pyc +0 -0
  308. package/engine/__pycache__/evidence_review.cpython-312.pyc +0 -0
  309. package/engine/__pycache__/evidencecore.cpython-312.pyc +0 -0
  310. package/engine/__pycache__/gap_lens.cpython-312.pyc +0 -0
  311. package/engine/__pycache__/gaps.cpython-312.pyc +0 -0
  312. package/engine/__pycache__/graph_store.cpython-312.pyc +0 -0
  313. package/engine/__pycache__/graph_validate.cpython-312.pyc +0 -0
  314. package/engine/__pycache__/ids.cpython-312.pyc +0 -0
  315. package/engine/__pycache__/library.cpython-312.pyc +0 -0
  316. package/engine/__pycache__/library_builtin.cpython-312.pyc +0 -0
  317. package/engine/__pycache__/living.cpython-312.pyc +0 -0
  318. package/engine/__pycache__/log.cpython-312.pyc +0 -0
  319. package/engine/__pycache__/meta_analysis.cpython-312.pyc +0 -0
  320. package/engine/__pycache__/meta_synthesis.cpython-312.pyc +0 -0
  321. package/engine/__pycache__/migration.cpython-312.pyc +0 -0
  322. package/engine/__pycache__/mode_router.cpython-312.pyc +0 -0
  323. package/engine/__pycache__/paths.cpython-312.pyc +0 -0
  324. package/engine/__pycache__/pilot.cpython-312.pyc +0 -0
  325. package/engine/__pycache__/planner.cpython-312.pyc +0 -0
  326. package/engine/__pycache__/project.cpython-312.pyc +0 -0
  327. package/engine/__pycache__/projections.cpython-312.pyc +0 -0
  328. package/engine/__pycache__/robustness.cpython-312.pyc +0 -0
  329. package/engine/__pycache__/run.cpython-312.pyc +0 -0
  330. package/engine/__pycache__/semantics.cpython-312.pyc +0 -0
  331. package/engine/__pycache__/study_design.cpython-312.pyc +0 -0
  332. package/engine/__pycache__/synthesis.cpython-312.pyc +0 -0
  333. package/engine/__pycache__/tribunal.cpython-312.pyc +0 -0
  334. package/engine/__pycache__/update.cpython-312.pyc +0 -0
  335. package/engine/__pycache__/versions.cpython-312.pyc +0 -0
  336. package/integrations/__pycache__/__init__.cpython-312.pyc +0 -0
  337. package/integrations/__pycache__/agent_mcp.cpython-312.pyc +0 -0
  338. package/integrations/__pycache__/smart_web_fetch.cpython-312.pyc +0 -0
  339. package/retrieval/__pycache__/__init__.cpython-312.pyc +0 -0
  340. package/retrieval/__pycache__/corpus_store.cpython-312.pyc +0 -0
  341. package/retrieval/__pycache__/dedupe.cpython-312.pyc +0 -0
  342. package/retrieval/__pycache__/failures.cpython-312.pyc +0 -0
  343. package/retrieval/__pycache__/fetch.cpython-312.pyc +0 -0
  344. package/retrieval/__pycache__/search.cpython-312.pyc +0 -0
  345. package/retrieval/__pycache__/source.cpython-312.pyc +0 -0
  346. package/retrieval/__pycache__/validate.cpython-312.pyc +0 -0
  347. package/scripts/__pycache__/__init__.cpython-312.pyc +0 -0
  348. package/scripts/__pycache__/benchmark.cpython-312.pyc +0 -0
  349. package/scripts/__pycache__/benchmark_evaluator.cpython-312.pyc +0 -0
  350. package/scripts/__pycache__/benchmark_judge.cpython-312.pyc +0 -0
  351. package/scripts/__pycache__/benchmark_routing.cpython-312.pyc +0 -0
  352. package/scripts/__pycache__/benchmark_v2.cpython-312.pyc +0 -0
  353. package/scripts/__pycache__/benchmark_v3.cpython-312.pyc +0 -0
  354. package/scripts/__pycache__/build_result.cpython-312.pyc +0 -0
  355. package/scripts/__pycache__/claim_audit.cpython-312.pyc +0 -0
  356. package/scripts/__pycache__/complexity_gate.cpython-312.pyc +0 -0
  357. package/scripts/__pycache__/compute_confidence.cpython-312.pyc +0 -0
  358. package/scripts/__pycache__/dashboard_server.cpython-312.pyc +0 -0
  359. package/scripts/__pycache__/did_regression.cpython-312.pyc +0 -0
  360. package/scripts/__pycache__/effect_calculator.cpython-312.pyc +0 -0
  361. package/scripts/__pycache__/evidence_matrix.cpython-312.pyc +0 -0
  362. package/scripts/__pycache__/evidence_score.cpython-312.pyc +0 -0
  363. package/scripts/__pycache__/evidence_semantics.cpython-312.pyc +0 -0
  364. package/scripts/__pycache__/fetch_benchmark.cpython-312.pyc +0 -0
  365. package/scripts/__pycache__/lint_report_layout.cpython-312.pyc +0 -0
  366. package/scripts/__pycache__/orchestrator.cpython-312.pyc +0 -0
  367. package/scripts/__pycache__/pre_verdict_gate.cpython-312.pyc +0 -0
  368. package/scripts/__pycache__/recompute_demo_quality.cpython-312.pyc +0 -0
  369. package/scripts/__pycache__/render_report.cpython-312.pyc +0 -0
  370. package/scripts/__pycache__/render_report_html.cpython-312.pyc +0 -0
  371. package/scripts/__pycache__/run_workspace.cpython-312.pyc +0 -0
  372. package/scripts/__pycache__/skill_lint.cpython-312.pyc +0 -0
  373. package/scripts/__pycache__/startup_probe.cpython-312.pyc +0 -0
  374. package/scripts/__pycache__/sync_killer_demo_report.cpython-312.pyc +0 -0
  375. package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.0.2.pyc +0 -0
  376. package/scripts/__pycache__/test_adversarial_empirical.cpython-312-pytest-9.1.1.pyc +0 -0
  377. package/scripts/__pycache__/validate_schema.cpython-312.pyc +0 -0
  378. package/visualization/eduevidence-report/scripts/__pycache__/adapter_contract.cpython-312.pyc +0 -0
  379. package/visualization/eduevidence-report/scripts/__pycache__/build_artifact_manifest.cpython-312.pyc +0 -0
  380. package/visualization/eduevidence-report/scripts/__pycache__/build_charts.cpython-312.pyc +0 -0
  381. package/visualization/eduevidence-report/scripts/__pycache__/build_figures.cpython-312.pyc +0 -0
  382. package/visualization/eduevidence-report/scripts/__pycache__/build_infographics.cpython-312.pyc +0 -0
  383. package/visualization/eduevidence-report/scripts/__pycache__/build_report.cpython-312.pyc +0 -0
  384. package/visualization/eduevidence-report/scripts/__pycache__/charts_data.cpython-312.pyc +0 -0
  385. package/visualization/eduevidence-report/scripts/__pycache__/lieflat_engine.cpython-312.pyc +0 -0
  386. package/visualization/eduevidence-report/scripts/__pycache__/zh_labels.cpython-312.pyc +0 -0
@@ -0,0 +1,113 @@
1
+ """Project-scoped durable control plane.
2
+
3
+ This service is the only write API intended for CLI and console integrations.
4
+ It keeps immutable artifact bytes and replayable events in SQLite WAL, while
5
+ preserving the existing ProjectWorkspace and graph store implementations.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ import sqlite3
12
+ from datetime import datetime, timezone
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from engine.project import ProjectWorkspace
17
+ from engine.run import finish_run, start_run
18
+
19
+ RUN_STATUSES = frozenset({
20
+ "queued", "running", "waiting_for_user", "waiting_for_user_data", "waiting_for_executor",
21
+ "waiting_for_tool", "waiting_for_review", "blocked_scientific_gate", "blocked_contract_error",
22
+ "completed", "failed_recoverable", "failed_terminal", "cancelled",
23
+ })
24
+
25
+
26
+ def _now() -> str:
27
+ return datetime.now(timezone.utc).isoformat()
28
+
29
+
30
+ class ResearchService:
31
+ def __init__(self, home: Path):
32
+ self.home = Path(home).expanduser().resolve()
33
+ self.home.mkdir(parents=True, exist_ok=True)
34
+ self.db_path = self.home / "research-control.sqlite3"
35
+ self._init_db()
36
+
37
+ def _connect(self) -> sqlite3.Connection:
38
+ connection = sqlite3.connect(self.db_path)
39
+ connection.row_factory = sqlite3.Row
40
+ connection.execute("PRAGMA journal_mode=WAL")
41
+ connection.execute("PRAGMA foreign_keys=ON")
42
+ return connection
43
+
44
+ def _init_db(self) -> None:
45
+ with self._connect() as db:
46
+ db.executescript("""
47
+ CREATE TABLE IF NOT EXISTS events (seq INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT NOT NULL, run_id TEXT, type TEXT NOT NULL, payload TEXT NOT NULL, created_at TEXT NOT NULL);
48
+ CREATE TABLE IF NOT EXISTS artifacts (artifact_id TEXT PRIMARY KEY, project_id TEXT NOT NULL, run_id TEXT, artifact_type TEXT NOT NULL, sha256 TEXT NOT NULL, path TEXT NOT NULL, created_at TEXT NOT NULL, metadata TEXT NOT NULL);
49
+ """)
50
+
51
+ def _event(self, project_id: str, event_type: str, payload: dict[str, Any], run_id: str | None = None) -> int:
52
+ with self._connect() as db:
53
+ cursor = db.execute("INSERT INTO events(project_id,run_id,type,payload,created_at) VALUES(?,?,?,?,?)",
54
+ (project_id, run_id, event_type, json.dumps(payload, ensure_ascii=False, sort_keys=True), _now()))
55
+ return int(cursor.lastrowid)
56
+
57
+ def create_project(self, *, question: str, title: str, research_mode: str = "evidence_review", domain: str = "education") -> ProjectWorkspace:
58
+ project = ProjectWorkspace.create(self.home, question=question, title=title, research_mode=research_mode, domain=domain)
59
+ self._event(project.project_id, "project_created", project.manifest())
60
+ return project
61
+
62
+ def start_run(self, project_id: str, *, purpose: str, capabilities: list[str], execution_backend: str = "sequential_main_agent") -> dict:
63
+ project = ProjectWorkspace.open(self.home, project_id)
64
+ run = start_run(project, purpose=purpose, capabilities=capabilities, execution_backend=execution_backend)
65
+ self._event(project_id, "run_started", run, run["run_id"])
66
+ return run
67
+
68
+ def submit_artifact(self, project_id: str, *, artifact_type: str, content: bytes, run_id: str | None = None,
69
+ metadata: dict[str, Any] | None = None) -> dict[str, Any]:
70
+ digest = hashlib.sha256(content).hexdigest()
71
+ # Bytes are content-addressed; association identity is project/run/type scoped.
72
+ # Identical bytes in two projects must not erase the second association.
73
+ identity = json.dumps([project_id, run_id, artifact_type, digest, metadata or {}],
74
+ ensure_ascii=False, sort_keys=True, separators=(",", ":"))
75
+ artifact_id = f"ART-{hashlib.sha256(identity.encode('utf-8')).hexdigest()[:24]}"
76
+ directory = self.home / "artifacts" / digest[:2]
77
+ directory.mkdir(parents=True, exist_ok=True)
78
+ path = directory / digest
79
+ if not path.exists():
80
+ path.write_bytes(content)
81
+ record = {"artifact_id": artifact_id, "project_id": project_id, "run_id": run_id,
82
+ "artifact_type": artifact_type, "sha256": digest, "path": str(path),
83
+ "created_at": _now(), "metadata": metadata or {}}
84
+ with self._connect() as db:
85
+ db.execute("INSERT OR IGNORE INTO artifacts VALUES(?,?,?,?,?,?,?,?)",
86
+ (artifact_id, project_id, run_id, artifact_type, digest, str(path), record["created_at"], json.dumps(record["metadata"], ensure_ascii=False, sort_keys=True)))
87
+ persisted = db.execute("SELECT * FROM artifacts WHERE artifact_id=?", (artifact_id,)).fetchone()
88
+ record = dict(persisted)
89
+ record["metadata"] = json.loads(record["metadata"])
90
+ self._event(project_id, "artifact_submitted", record, run_id)
91
+ return record
92
+
93
+ def events(self, project_id: str, *, after_seq: int = 0) -> list[dict[str, Any]]:
94
+ with self._connect() as db:
95
+ rows = db.execute("SELECT * FROM events WHERE project_id=? AND seq>? ORDER BY seq", (project_id, after_seq)).fetchall()
96
+ return [{**dict(row), "payload": json.loads(row["payload"])} for row in rows]
97
+
98
+ def projects(self) -> list[dict[str, Any]]:
99
+ directory = self.home / "projects"
100
+ if not directory.is_dir():
101
+ return []
102
+ return [json.loads(manifest.read_text(encoding="utf-8"))
103
+ for manifest in sorted(directory.glob("*/project.json"))]
104
+
105
+ def runs(self, project_id: str) -> list[dict[str, Any]]:
106
+ project = ProjectWorkspace.open(self.home, project_id)
107
+ return [json.loads(path.read_text(encoding="utf-8"))
108
+ for path in sorted(project.runs_dir().glob("*/run.json"))]
109
+
110
+ def artifacts(self, project_id: str) -> list[dict[str, Any]]:
111
+ with self._connect() as db:
112
+ rows = db.execute("SELECT * FROM artifacts WHERE project_id=? ORDER BY created_at", (project_id,)).fetchall()
113
+ return [{**dict(row), "metadata": json.loads(row["metadata"])} for row in rows]
@@ -0,0 +1,400 @@
1
+ """Read-only, portable projections for Research Studio.
2
+
3
+ No ProjectWorkspace.create, GraphStore.create, service initialization or state
4
+ repair is allowed here. Missing/corrupt inputs are visible diagnostics, not
5
+ successful empty studies. Static export includes examples only.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import math
11
+ import re
12
+ import sqlite3
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+ from typing import Any
16
+ from urllib.parse import quote
17
+
18
+ THEMES = (
19
+ ('claude', 'Claude Research', 'light'),
20
+ ('academic', 'Academic Paper', 'light'),
21
+ ('datalab', 'DataLab', 'light'),
22
+ ('datalab-dark', 'DataLab Dark', 'dark'),
23
+ ('presentation', 'Presentation / Judge', 'dark'),
24
+ )
25
+ TABLES = ('sources', 'studies', 'findings', 'outcomes', 'claims', 'evidence_links', 'audits')
26
+ MAX_BYTES = 16 * 1024 * 1024
27
+
28
+
29
+ def finite(value: Any) -> float | None:
30
+ return float(value) if isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value) else None
31
+
32
+
33
+ def first_value(*values: Any) -> Any:
34
+ return next((value for value in values if value is not None), None)
35
+
36
+
37
+ def json_safe(value: Any) -> Any:
38
+ if isinstance(value, float) and not math.isfinite(value):
39
+ return None
40
+ if isinstance(value, dict):
41
+ return {str(k): json_safe(v) for k, v in value.items()}
42
+ if isinstance(value, (list, tuple)):
43
+ return [json_safe(v) for v in value]
44
+ return value
45
+
46
+
47
+ def numeric_effect(row: dict) -> dict:
48
+ effect = first_value(row.get('effect_estimate'), row.get('effect_size'))
49
+ if isinstance(effect, dict):
50
+ value = finite(effect.get('value'))
51
+ lo = finite(first_value(effect.get('ci_lower'), effect.get('ci_lo'), effect.get('ci_low')))
52
+ hi = finite(first_value(effect.get('ci_upper'), effect.get('ci_hi'), effect.get('ci_high')))
53
+ metric = effect.get('metric') or effect.get('type') or row.get('effect_metric')
54
+ else:
55
+ value = finite(first_value(effect, row.get('hedges_g'), row.get('g'), row.get('effect_size_value')))
56
+ lo, hi = finite(row.get('ci_lower')), finite(row.get('ci_upper'))
57
+ metric = row.get('effect_metric') or row.get('metric')
58
+ # Never fabricate an interval or erase a legitimate zero endpoint.
59
+ interval = 'not_reported'
60
+ if lo is not None and hi is not None:
61
+ interval = 'reported' if lo <= hi and (value is None or lo <= value <= hi) else 'invalid'
62
+ elif lo is not None or hi is not None:
63
+ interval = 'incomplete'
64
+ return {'value': value, 'ci_lower': lo, 'ci_upper': hi, 'interval_status': interval,
65
+ 'metric': str(metric or 'unspecified')}
66
+
67
+
68
+ class StudioReader:
69
+ """Build a bounded public DTO from committed, project-scoped inputs."""
70
+
71
+ def __init__(self, examples: Path, home: Path, *, static: bool = False):
72
+ self.examples = Path(examples).resolve()
73
+ self.home = Path(home).expanduser().resolve()
74
+ self.static = static
75
+ self.issues: list[dict] = []
76
+
77
+ def _read(self, path: Path, default: Any = None) -> Any:
78
+ if not path.exists():
79
+ return default
80
+ try:
81
+ if not path.resolve().is_relative_to(self.examples) and not path.resolve().is_relative_to(self.home):
82
+ raise ValueError('path escapes the read scope')
83
+ if path.stat().st_size > MAX_BYTES:
84
+ raise ValueError('file exceeds the Studio read limit')
85
+ if path.suffix == '.jsonl':
86
+ return [json.loads(line) for line in path.read_text(encoding='utf-8').splitlines() if line.strip()]
87
+ return json.loads(path.read_text(encoding='utf-8'))
88
+ except (OSError, ValueError) as exc:
89
+ self.issues.append({'code': 'unreadable_artifact', 'artifact': path.name, 'message': str(exc).split(':')[0]})
90
+ return default
91
+
92
+ def _directory(self, key: str) -> tuple[Path, str]:
93
+ if key.startswith('example--'):
94
+ name = key[len('example--'):]
95
+ base, kind = self.examples, 'example'
96
+ elif key.startswith('project--') and not self.static:
97
+ name = key[len('project--'):]
98
+ base, kind = self.home / 'projects', 'project'
99
+ if not re.fullmatch(r'PRJ-[A-Za-z0-9_-]+', name):
100
+ raise FileNotFoundError('unknown project')
101
+ else:
102
+ raise FileNotFoundError('unknown project')
103
+ if not re.fullmatch(r'[A-Za-z0-9_-]+', name):
104
+ raise FileNotFoundError('unknown project')
105
+ path = base / name
106
+ if path.is_symlink() or not path.is_dir() or not path.resolve().is_relative_to(base.resolve()):
107
+ raise FileNotFoundError('unknown project')
108
+ return path, kind
109
+
110
+ def _catalog_keys(self) -> list[str]:
111
+ keys = []
112
+ if self.examples.is_dir():
113
+ keys.extend('example--' + p.name for p in sorted(self.examples.iterdir())
114
+ if p.is_dir() and not p.is_symlink() and (p / 'result.json').is_file())
115
+ if not self.static and (self.home / 'projects').is_dir():
116
+ keys.extend('project--' + p.name for p in sorted((self.home / 'projects').iterdir())
117
+ if p.is_dir() and not p.is_symlink() and (p / 'project.json').is_file())
118
+ return keys
119
+
120
+ def _db_rows(self, table: str, project_id: str) -> list[dict]:
121
+ if table not in {'artifacts', 'events'}:
122
+ raise ValueError('unknown read table')
123
+ path = self.home / 'research-control.sqlite3'
124
+ if not path.is_file():
125
+ return []
126
+ if path.is_symlink():
127
+ self.issues.append({'code': 'database_unavailable', 'message': 'symlink database refused'})
128
+ return []
129
+ try:
130
+ uri = path.as_uri() + '?mode=ro'
131
+ with sqlite3.connect(uri, uri=True, timeout=2) as connection:
132
+ connection.row_factory = sqlite3.Row
133
+ order = 'seq' if table == 'events' else 'created_at'
134
+ rows = connection.execute(f'SELECT * FROM {table} WHERE project_id=? ORDER BY {order} DESC LIMIT 500', (project_id,)).fetchall()
135
+ output = []
136
+ for record in rows:
137
+ item = dict(record)
138
+ item.pop('path', None) # Never expose host filesystem paths.
139
+ for field in ('payload', 'metadata'):
140
+ if isinstance(item.get(field), str):
141
+ item[field] = json.loads(item[field])
142
+ if isinstance(item.get('payload'), dict):
143
+ item['payload'].pop('path', None)
144
+ output.append(item)
145
+ return output
146
+ except (sqlite3.Error, ValueError):
147
+ self.issues.append({'code': 'database_unavailable', 'message': 'Research event index is unavailable'})
148
+ return []
149
+
150
+ def _reports(self, directory: Path, key: str, kind: str) -> list[dict]:
151
+ rows = []
152
+ default = directory / 'EduEvidence_Report.html'
153
+ default_theme = None
154
+ if default.is_file() and not default.is_symlink():
155
+ with default.open(encoding='utf-8', errors='replace') as handle:
156
+ match = re.search(r'<html[^>]*data-theme=[\"\']([a-z-]+)', handle.read(4096))
157
+ default_theme = match.group(1) if match else None
158
+ for theme, title, tone in THEMES:
159
+ path = directory / 'reports-5themes' / f'EduEvidence_Report_{theme}.html'
160
+ if not path.is_file() and theme == default_theme:
161
+ path = directory / 'EduEvidence_Report.html'
162
+ available = path.is_file() and path.resolve().is_relative_to(directory.resolve())
163
+ url = None
164
+ if available:
165
+ url = (f'../reports/{directory.name}/{path.name}' if self.static else
166
+ f'/api/studio/projects/{quote(key, safe="")}/report?theme={theme}')
167
+ rows.append({'theme': theme, 'title': title, 'tone': tone, 'available': available, 'url': url})
168
+ if kind == 'project':
169
+ rows = []
170
+ report_dir = directory / 'reports'
171
+ if not report_dir.resolve().is_relative_to(directory.resolve()):
172
+ return rows
173
+ for path in sorted(report_dir.glob('*.html')):
174
+ if path.is_symlink():
175
+ continue
176
+ rows.append({'theme': path.stem, 'title': path.stem, 'tone': 'light', 'available': True,
177
+ 'url': f'/api/studio/projects/{quote(key, safe="")}/report?file={quote(path.name)}'})
178
+ return rows
179
+
180
+ def report_path(self, key: str, *, theme: str = 'claude', filename: str | None = None) -> Path:
181
+ directory, kind = self._directory(key)
182
+ if kind == 'example':
183
+ if theme not in {row[0] for row in THEMES}:
184
+ raise FileNotFoundError('unknown report theme')
185
+ path = directory / 'reports-5themes' / f'EduEvidence_Report_{theme}.html'
186
+ if not path.is_file():
187
+ available = {row['theme'] for row in self._reports(directory, key, kind) if row['available']}
188
+ if theme in available:
189
+ path = directory / 'EduEvidence_Report.html'
190
+ else:
191
+ if not filename or not re.fullmatch(r'[A-Za-z0-9_.-]+\.html', filename):
192
+ raise FileNotFoundError('unknown report')
193
+ path = directory / 'reports' / filename
194
+ if not path.is_file() or path.is_symlink() or not path.resolve().is_relative_to(directory):
195
+ raise FileNotFoundError('report not generated')
196
+ return path
197
+
198
+ def detail(self, key: str) -> dict:
199
+ self.issues = []
200
+ directory, kind = self._directory(key)
201
+ en = self._read(directory / 'result.json', {}) if kind == 'example' else {}
202
+ zh = self._read(directory / 'result.zh.json', {}) if kind == 'example' else {}
203
+ if not isinstance(en, dict) or not isinstance(zh, dict):
204
+ raise ValueError('invalid result object')
205
+ result = en or {}
206
+ manifest = self._read(directory / 'project.json', {}) if kind == 'project' else {}
207
+ meta = result.get('meta') or {}
208
+ sources = result.get('sources') or []
209
+ findings = result.get('evidence') or []
210
+ claims = result.get('claims') or []
211
+ outcomes = result.get('outcomes') or []
212
+ audits, designs, analyses = [], [], []
213
+ studies, links, revisions, decisions, runs, artifacts, events, gaps, iterations = [], [], [], [], [], [], [], [], []
214
+ active_revision = None
215
+ if kind == 'project':
216
+ head = directory / 'graph' / 'HEAD'
217
+ if head.is_file():
218
+ try:
219
+ active_revision = int(head.read_text().strip())
220
+ if active_revision < 0:
221
+ raise ValueError('negative revision')
222
+ except (OSError, ValueError):
223
+ self.issues.append({'code': 'invalid_graph_head', 'message': 'Graph HEAD cannot be read'})
224
+ if active_revision is not None and active_revision != manifest.get('graph_revision'):
225
+ self.issues.append({'code': 'revision_mirror_mismatch', 'message': 'Project revision mirror differs from Graph HEAD'})
226
+ if active_revision:
227
+ revdir = directory / 'graph' / 'revisions' / f'rev-{active_revision:06d}'
228
+ tables = {t: self._read(revdir / f'{t}.jsonl', []) for t in TABLES}
229
+ for t in TABLES:
230
+ if not (revdir / f'{t}.jsonl').exists():
231
+ self.issues.append({'code': 'missing_graph_table', 'artifact': t, 'message': 'Committed graph table missing'})
232
+ sources, studies, findings, claims, links, outcomes, audits = (tables[t] for t in ('sources', 'studies', 'findings', 'claims', 'evidence_links', 'outcomes', 'audits'))
233
+ # Follow only the committed ancestry. Orphan directories are not history.
234
+ revision = active_revision
235
+ visited: set[int] = set()
236
+ while revision and revision not in visited:
237
+ visited.add(revision)
238
+ revision_meta = self._read(directory / 'graph' / 'revisions' / f'rev-{revision:06d}' / 'manifest.json', {})
239
+ if not isinstance(revision_meta, dict) or revision_meta.get('revision') != revision:
240
+ self.issues.append({'code': 'invalid_revision_manifest', 'message': 'Revision ancestry is incomplete'})
241
+ break
242
+ revisions.append({**revision_meta, 'active': revision == active_revision})
243
+ parent = revision_meta.get('parent_revision')
244
+ if type(parent) is not int or not 0 <= parent < revision:
245
+ self.issues.append({'code': 'invalid_revision_parent', 'message': 'Revision parent is invalid'})
246
+ break
247
+ revision = parent
248
+ revisions.reverse()
249
+ decisions = [d for p in sorted((directory / 'decisions').glob('DEC-*.json')) if isinstance((d := self._read(p)), dict)]
250
+ decisions.sort(key=lambda d: (d.get('graph_revision', 0), d.get('created_at', '')), reverse=True)
251
+ runs = [d for p in sorted((directory / 'runs').glob('*/run.json')) if isinstance((d := self._read(p)), dict)]
252
+ runs.sort(key=lambda d: d.get('started_at', ''), reverse=True)
253
+ for run in runs:
254
+ run_id = str(run.get('run_id', ''))
255
+ if re.fullmatch(r'[A-Za-z0-9_-]+', run_id):
256
+ run['stage_state'] = self._read(directory / 'runs' / run_id / 'state.json', {})
257
+ run['execution_plan'] = self._read(directory / 'runs' / run_id / 'execution_plan.json', {})
258
+ run['gate_report'] = self._read(directory / 'runs' / run_id / 'gate_report.json', {})
259
+ artifacts = self._db_rows('artifacts', manifest.get('project_id', directory.name))
260
+ events = self._db_rows('events', manifest.get('project_id', directory.name))
261
+ if active_revision is not None:
262
+ gaps = self._read(directory / 'gaps' / f'gaps-rev-{active_revision:06d}.jsonl', [])
263
+ iterations = self._read(directory / 'autoresearch' / 'research-iterations.jsonl', [])
264
+ designs = [d for p in sorted((directory / 'study-designs').glob('DSN-*.json')) if isinstance((d := self._read(p)), dict)]
265
+ analyses = [d for p in sorted((directory / 'analyses').glob('*.json')) if isinstance((d := self._read(p)), dict)]
266
+ for records in (sources, studies, findings, claims, links, outcomes, runs, events, artifacts, gaps, iterations):
267
+ if not isinstance(records, list) or any(not isinstance(r, dict) for r in records):
268
+ raise ValueError('invalid record collection')
269
+ sources = [{**r, 'title': r.get('title') or (r.get('extensions') or {}).get('title') or r.get('source_id'),
270
+ 'canonical_url': r.get('canonical_url') or r.get('canonical_locator') or r.get('source_location')}
271
+ for r in sources]
272
+ localized_findings = {r.get('evidence_id'): r for r in zh.get('evidence', [])}
273
+ evidence = []
274
+ seen = set()
275
+ studies_by_id = {r.get('study_id'): r for r in studies}
276
+ outcomes_by_id = {r.get('outcome_id'): r for r in outcomes}
277
+
278
+ for row in findings:
279
+ eid = str(row.get('finding_id') or row.get('evidence_id') or '')
280
+ if not eid or eid in seen:
281
+ continue
282
+ seen.add(eid)
283
+ local = localized_findings.get(eid, {})
284
+ # A Finding's observed effect is NOT its relation to a Claim.
285
+ study = studies_by_id.get(row.get('study_id'), {})
286
+ outcome = outcomes_by_id.get(row.get('outcome_id'), {})
287
+ claim_links = [l for l in links if l.get('finding_id') == eid]
288
+ relationships = sorted({l.get('relation_to_claim') or l.get('relation') or 'unassigned' for l in claim_links})
289
+ relation = (relationships[0] if len(relationships) == 1 else 'multiple') if relationships else row.get('relation_to_claim') or row.get('direction') or 'unassigned'
290
+ source_ids = ([row['source_id']] if row.get('source_id') else study.get('source_ids', []))
291
+ evidence.append({**row, 'id': eid,
292
+ 'title': row.get('title') or row.get('measure') or row.get('finding') or eid,
293
+ 'title_zh': local.get('title'), 'claim_zh': local.get('claim'),
294
+ 'claim': row.get('claim') or row.get('raw_result_text'),
295
+ 'source_id': source_ids[0] if source_ids else None, 'source_ids': source_ids,
296
+ 'study_type': row.get('study_type') or study.get('study_design'),
297
+ 'sample_size': first_value(row.get('sample_size'), study.get('sample_size')),
298
+ 'audits': [a for a in audits if a.get('study_id') == row.get('study_id')],
299
+ 'outcome_type': row.get('outcome_type') or outcome.get('outcome_type') or row.get('measure'),
300
+ 'relation': relation, 'claim_links': claim_links,
301
+ 'effect_direction': row.get('effect_direction') or 'not_reported',
302
+ 'numeric': numeric_effect(row)})
303
+ decision = decisions[0] if decisions else result.get('decision') or {}
304
+ bound_revision = decision.get('graph_revision')
305
+ stale = bool(kind == 'project' and decision and bound_revision != active_revision)
306
+ decision_view = {
307
+ **decision,
308
+ 'action': decision.get('decision') or decision.get('recommended_action'),
309
+ 'confidence': decision.get('confidence_label') or decision.get('confidence'),
310
+ 'graph_revision': bound_revision,
311
+ 'stale': stale,
312
+ 'supported': decision.get('supported_claims') or [],
313
+ 'uncertain': decision.get('uncertain_claims') or decision.get('missing_evidence') or [],
314
+ 'contradicted': decision.get('contradicted_claims') or [],
315
+ 'rationale': decision.get('decision_rationale') or decision.get('rationale') or '',
316
+ }
317
+ if stale:
318
+ self.issues.append({'code': 'stale_decision', 'message': 'Latest decision is not bound to active GraphRevision'})
319
+ claim_nodes = [{**r, 'id': str(r.get('claim_id')), 'label': r.get('claim') or r.get('text') or r.get('claim_id'), 'kind': 'claim'} for r in claims if r.get('claim_id')]
320
+ nodes = [{'id': str(r.get('source_id')), 'label': r.get('title') or r.get('source_id'), 'kind': 'source'} for r in sources if r.get('source_id')]
321
+ nodes += [{'id': r['id'], 'label': r['title'], 'kind': 'finding'} for r in evidence]
322
+ nodes += claim_nodes
323
+ edges = []
324
+ studies_by_id = {r.get('study_id'): r for r in studies}
325
+ for r in evidence:
326
+ study = studies_by_id.get(r.get('study_id'), {})
327
+ source_ids = r.get('source_ids') or study.get('source_ids', [])
328
+ for sid in source_ids:
329
+ edges.append({'source': sid, 'target': r['id'], 'relation': 'provenance'})
330
+ for c in claims:
331
+ for eid in c.get('evidence_ids', []):
332
+ ev = next((r for r in evidence if r['id'] == eid), {})
333
+ edges.append({'source': eid, 'target': c.get('claim_id'), 'relation': ev.get('relation', 'unassigned')})
334
+ for link in links:
335
+ edges.append({'source': link.get('finding_id'), 'target': link.get('claim_id'), 'relation': link.get('relation') or link.get('relation_to_claim') or 'unassigned'})
336
+ node_ids = {n['id'] for n in nodes}
337
+ invalid_edges = [e for e in edges if e['source'] not in node_ids or e['target'] not in node_ids]
338
+ if invalid_edges:
339
+ self.issues.append({'code': 'unresolved_graph_links', 'message': f'{len(invalid_edges)} graph relationships have missing endpoints'})
340
+ edges = [e for e in edges if e not in invalid_edges]
341
+ reports = self._reports(directory, key, kind)
342
+ info = {
343
+ 'id': key, 'project_id': manifest.get('project_id'), 'kind': kind,
344
+ 'title': manifest.get('title') or (zh.get('meta') or {}).get('question') or (zh.get('decision') or {}).get('decision_question') or meta.get('question') or directory.name,
345
+ 'title_en': manifest.get('title') or decision.get('decision_question') or meta.get('question') or directory.name,
346
+ 'question': manifest.get('question') or meta.get('question') or '',
347
+ 'domain': manifest.get('domain') or meta.get('domain') or result.get('research_frame', {}).get('extensions', {}).get('domain') or 'education',
348
+ 'status': (runs[0].get('status') if runs else None) or manifest.get('status') or ('example' if kind == 'example' else 'not_started'),
349
+ 'data_origin': meta.get('data_origin') or ('local_project' if kind == 'project' else 'not_reported'),
350
+ 'updated_at': manifest.get('updated_at') or meta.get('generated_at'),
351
+ 'active_revision': active_revision, 'decision': decision_view,
352
+ 'counts': {'sources': len(sources), 'findings': len(evidence), 'claims': len(claim_nodes),
353
+ 'studies': len({r.get('study_id') for r in findings if r.get('study_id')}),
354
+ 'runs': len(runs)},
355
+ 'reports': reports,
356
+ }
357
+ if kind == 'project' and active_revision is not None:
358
+ try:
359
+ if int((directory / 'graph' / 'HEAD').read_text().strip()) != active_revision:
360
+ raise ValueError('Graph HEAD changed during projection; retry')
361
+ except OSError as exc:
362
+ raise ValueError('Graph HEAD unavailable after read') from exc
363
+ return json_safe({
364
+ 'schema_version': 1, 'project': info, 'sources': sources, 'studies': studies,
365
+ 'evidence': evidence, 'claims': claims, 'graph': {'nodes': nodes, 'edges': edges, 'origin': 'canonical_revision' if kind == 'project' else 'result_projection'},
366
+ 'decisions': decisions, 'revisions': revisions, 'runs': runs, 'events': events,
367
+ 'artifacts': artifacts, 'gaps': gaps, 'iterations': iterations,
368
+ 'applicability': decision.get('applicability_boundary') or result.get('applicability') or decision.get('applicability') or {},
369
+ 'intervention': result.get('intervention') or ({'study_designs': designs} if designs else {}),
370
+ 'evaluation': result.get('evaluation') or ({'analyses': analyses} if analyses else {}),
371
+ 'decision_zh': zh.get('decision') or {}, 'issues': list(self.issues),
372
+ 'snapshot_taken_at': datetime.now(timezone.utc).isoformat(),
373
+ 'measurement_policy': 'No pooled effect or mean is computed by Studio. Missing values remain null.',
374
+ })
375
+
376
+ def catalog(self) -> dict:
377
+ projects, issues = [], []
378
+ for key in self._catalog_keys():
379
+ try:
380
+ detail = self.detail(key)
381
+ projects.append(detail['project'])
382
+ issues.extend({'project': key, **issue} for issue in detail['issues'])
383
+ except (OSError, ValueError, TypeError, AttributeError):
384
+ issues.append({'project': key, 'code': 'invalid_project', 'message': 'Project could not be read'})
385
+ return {'schema_version': 1, 'mode': 'static' if self.static else 'local', 'readonly': True,
386
+ 'generated_at': datetime.now(timezone.utc).isoformat(), 'projects': projects, 'issues': issues}
387
+
388
+ def evolution(self) -> dict:
389
+ # Deliberately distinct from project ResearchIterations; no arbitrary
390
+ # session directory or rejected candidate files are exposed.
391
+ rows = []
392
+ if not self.static:
393
+ path = self.examples.parent / 'autoevolve' / 'experiments.jsonl'
394
+ if path.is_file() and not path.is_symlink() and path.stat().st_size <= MAX_BYTES:
395
+ try:
396
+ allowed = ('experiment_id', 'session_id', 'hypothesis', 'status', 'promotion_reason', 'candidate_commit', 'parent_skill_revision')
397
+ rows = [{k: r.get(k) for k in allowed} for line in path.read_text().splitlines() if line.strip() for r in [json.loads(line)]]
398
+ except (ValueError, OSError):
399
+ return {'experiments': [], 'status': 'unavailable'}
400
+ return {'experiments': rows[-100:], 'status': 'recorded' if rows else 'not_recorded'}